paint-brush
Tornando o TypeScript verdadeiramente "fortemente digitado"by@nodge
15,793
15,793

Tornando o TypeScript verdadeiramente "fortemente digitado"

Maksim Zemskov12m2023/09/10
Read on Terminal Reader

TypeScript fornece o tipo "Any" para situações em que a forma dos dados não é conhecida antecipadamente. No entanto, o uso excessivo desse tipo pode levar a problemas de segurança de tipo, qualidade do código e experiência do desenvolvedor. Este artigo explora os riscos associados ao tipo "Qualquer", identifica fontes potenciais de sua inclusão em uma base de código e fornece estratégias para controlar seu uso ao longo de um projeto.
featured image - Tornando o TypeScript verdadeiramente "fortemente digitado"
Maksim Zemskov HackerNoon profile picture
0-item
1-item

TypeScript afirma ser uma linguagem de programação fortemente tipada construída sobre JavaScript, fornecendo melhores ferramentas em qualquer escala. No entanto, o TypeScript inclui any tipo, que muitas vezes pode entrar implicitamente em uma base de código e levar à perda de muitas das vantagens do TypeScript.


Este artigo explora maneiras de assumir o controle de any tipo em projetos TypeScript. Prepare-se para liberar o poder do TypeScript, alcançando o máximo em segurança de tipo e melhorando a qualidade do código.

Desvantagens de usar qualquer um no TypeScript

TypeScript fornece uma variedade de ferramentas adicionais para aprimorar a experiência e a produtividade do desenvolvedor:


  • Ajuda a detectar erros no início do estágio de desenvolvimento.
  • Oferece excelente preenchimento automático para editores de código e IDEs.
  • Ele permite a refatoração fácil de grandes bases de código por meio de fantásticas ferramentas de navegação de código e refatoração automática.
  • Ele simplifica a compreensão de uma base de código, fornecendo semântica adicional e estruturas de dados explícitas por meio de tipos.


No entanto, assim que você começar a usar any tipo em sua base de código, você perderá todos os benefícios listados acima. O any type é uma brecha perigosa no sistema de tipos e usá-lo desativa todos os recursos de verificação de tipo, bem como todas as ferramentas que dependem da verificação de tipo. Como resultado, todos os benefícios do TypeScript são perdidos: erros são perdidos, editores de código tornam-se menos úteis e muito mais.


Por exemplo, considere o seguinte exemplo:


 function parse(data: any) { return data.split(''); } // Case 1 const res1 = parse(42); // ^ TypeError: data.split is not a function // Case 2 const res2 = parse('hello'); // ^ any


No código acima:


  • Você perderá o preenchimento automático dentro da função parse . Quando você digita data. em seu editor, você não receberá sugestões corretas sobre os métodos disponíveis para data .
  • No primeiro caso, há um erro TypeError: data.split is not a function porque passamos um número em vez de uma string. O TypeScript não consegue destacar o erro porque any desativa a verificação de tipo.
  • No segundo caso, a variável res2 também possui o tipo any . Isso significa que um único uso de any pode ter um efeito cascata em uma grande parte de uma base de código.


Usar any é aceitável apenas em casos extremos ou para necessidades de prototipagem. Em geral, é melhor evitar o uso any para aproveitar ao máximo o TypeScript.

De onde vem qualquer tipo

É importante estar ciente das fontes do tipo any em uma base de código porque escrever any explicitamente não é a única opção. Apesar de nossos melhores esforços para evitar o uso de any tipo, às vezes ele pode entrar implicitamente em uma base de código.


Existem quatro fontes principais de any tipo em uma base de código:

  1. Opções do compilador em tsconfig.
  2. Biblioteca padrão do TypeScript.
  3. Dependências do projeto.
  4. Uso explícito de any em uma base de código.


Já escrevi artigos sobre Considerações Chave em tsconfig e Melhorando Tipos de Biblioteca Padrão para os dois primeiros pontos. Verifique-os se quiser melhorar a segurança de tipo em seus projetos.


Desta vez, focaremos em ferramentas automáticas para controlar a aparência de any tipo em uma base de código.

Etapa 1: usando ESLint

ESLint é uma ferramenta popular de análise estática usada por desenvolvedores web para garantir melhores práticas e formatação de código. Ele pode ser usado para impor estilos de codificação e localizar códigos que não atendem a determinadas diretrizes.


ESLint também pode ser usado com projetos TypeScript, graças ao plugin typesctipt-eslint . Muito provavelmente, este plugin já foi instalado no seu projeto. Mas se não, você pode seguir o guia oficial de primeiros passos .


A configuração mais comum para typescript-eslint é a seguinte:


 module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };


Essa configuração permite que eslint entenda o TypeScript no nível da sintaxe, permitindo que você escreva regras eslint simples que se aplicam a tipos escritos manualmente em um código. Por exemplo, você pode proibir o uso explícito de any .


A predefinição recommended contém um conjunto cuidadosamente selecionado de regras ESLint destinadas a melhorar a correção do código. Embora seja recomendado usar a predefinição inteira, para os fins deste artigo, nos concentraremos apenas na regra no-explicit-any .

não-explícito-qualquer

O modo estrito do TypeScript impede o uso de any implícito, mas não impede que any seja usado explicitamente. A regra no-explicit-any ajuda a proibir a gravação manual any qualquer lugar em uma base de código.


 // ❌ Incorrect function loadPokemons(): any {} // ✅ Correct function loadPokemons(): unknown {} // ❌ Incorrect function parsePokemons(data: Response<any>): Array<Pokemon> {} // ✅ Correct function parsePokemons(data: Response<unknown>): Array<Pokemon> {} // ❌ Incorrect function reverse<T extends Array<any>>(array: T): T {} // ✅ Correct function reverse<T extends Array<unknown>>(array: T): T {}


O objetivo principal desta regra é evitar o uso de any em toda a equipe. Este é um meio de fortalecer o acordo da equipe de que o uso de any no projeto é desencorajado.


Este é um objetivo crucial porque mesmo um único uso de any pode ter um impacto em cascata em uma parte significativa da base de código devido à inferência de tipo . No entanto, isso ainda está longe de alcançar a segurança máxima do tipo.

Por que nada explícito não é suficiente

Embora tenhamos lidado com any explicitamente usado, ainda há muitos any implícitos nas dependências de um projeto, incluindo pacotes npm e a biblioteca padrão do TypeScript.


Considere o código a seguir, que provavelmente será visto em qualquer projeto:


 const response = await fetch('https://pokeapi.co/api/v2/pokemon'); const pokemons = await response.json(); // ^? any const settings = JSON.parse(localStorage.getItem('user-settings')); // ^? any


Ambas as variáveis pokemons e settings receberam implicitamente any tipo. Nem no-explicit-any nem o modo estrito do TypeScript nos avisarão neste caso. Ainda não.


Isso acontece porque os tipos para response.json() e JSON.parse() vêm da biblioteca padrão do TypeScript, onde esses métodos possuem uma anotação any explícita. Ainda podemos especificar manualmente um tipo melhor para nossas variáveis, mas existem quase 1.200 ocorrências de any na biblioteca padrão. É quase impossível lembrar de todos os casos em que any pode entrar furtivamente em nossa base de código a partir da biblioteca padrão.


O mesmo vale para dependências externas. Existem muitas bibliotecas mal digitadas no npm, e a maioria ainda é escrita em JavaScript. Como resultado, o uso de tais bibliotecas pode facilmente levar a muitos any implícitos em uma base de código.


Geralmente, ainda existem muitas maneiras de any entrar furtivamente em nosso código.

Estágio 2: Aprimorando os recursos de verificação de tipo

Idealmente, gostaríamos de ter uma configuração no TypeScript que fizesse o compilador reclamar de qualquer variável que tenha recebido any tipo por qualquer motivo. Infelizmente, tal configuração não existe atualmente e não se espera que seja adicionada.


Podemos conseguir esse comportamento usando o modo de verificação de tipo do plugin typescript-eslint . Este modo funciona em conjunto com TypeScript para fornecer informações completas de tipo do compilador TypeScript para regras ESLint. Com essas informações, é possível escrever regras ESLint mais complexas que essencialmente estendem os recursos de verificação de tipo do TypeScript. Por exemplo, uma regra pode encontrar todas as variáveis com any tipo, independentemente de como any foi obtida.


Para usar regras de reconhecimento de tipo, você precisa ajustar ligeiramente a configuração do ESLint:


 module.exports = { extends: [ 'eslint:recommended', - 'plugin:@typescript-eslint/recommended', + 'plugin:@typescript-eslint/recommended-type-checked', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', + parserOptions: { + project: true, + tsconfigRootDir: __dirname, + }, root: true, };


Para habilitar a inferência de tipo para typescript-eslint , adicione parserOptions à configuração do ESLint. Em seguida, substitua a predefinição recommended por recommended-type-checked . A última predefinição adiciona cerca de 17 novas regras poderosas. Para os fins deste artigo, nos concentraremos em apenas 5 deles.

argumento sem insegurança

A regra no-unsafe-argument procura chamadas de função nas quais uma variável do tipo any é passada como parâmetro. Quando isso acontece, a verificação de tipo é perdida e todos os benefícios da digitação forte também são perdidos.


Por exemplo, vamos considerar uma função saveForm que requer um objeto como parâmetro. Suponha que recebemos JSON, analisamos e obtemos any tipo.


 // ❌ Incorrect function saveForm(values: FormValues) { console.log(values); } const formValues = JSON.parse(userInput); // ^? any saveForm(formValues); // ^ Unsafe argument of type `any` assigned // to a parameter of type `FormValues`.


Quando chamamos a função saveForm com este parâmetro, a regra no-unsafe-argument sinaliza-a como insegura e exige que especifiquemos o tipo apropriado para a variável value .


Esta regra é poderosa o suficiente para inspecionar profundamente estruturas de dados aninhadas em argumentos de função. Portanto, você pode ter certeza de que passar objetos como argumentos de função nunca conterá dados não digitados.


 // ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });


A melhor maneira de corrigir o erro é usar o estreitamento de tipo do TypeScript ou uma biblioteca de validação como Zod ou Superstruct . Por exemplo, vamos escrever a função parseFormValues que restringe o tipo preciso de dados analisados.


 // ✅ Correct function parseFormValues(data: unknown): FormValues { if ( typeof data === 'object' && data !== null && 'name' in data && typeof data['name'] === 'string' && 'address' in data && typeof data.address === 'string' ) { const { name, address } = data; return { name, address }; } throw new Error('Failed to parse form values'); } const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues saveForm(formValues);


Observe que é permitido passar any tipo como argumento para uma função que aceita unknown , pois não há preocupações de segurança associadas a isso.


Escrever funções de validação de dados pode ser uma tarefa tediosa, especialmente quando se lida com grandes quantidades de dados. Portanto, vale a pena considerar a utilização de uma biblioteca de validação de dados. Por exemplo, com Zod, o código ficaria assim:


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); const formValues = schema.parse(JSON.parse(userInput)); // ^? { name: string, address: string } saveForm(formValues);


atribuição não-insegura

A regra no-unsafe-assignment procura atribuições de variáveis nas quais um valor tenha any tipo. Tais atribuições podem induzir o compilador a pensar que uma variável tem um determinado tipo, enquanto os dados podem, na verdade, ter um tipo diferente.


Considere o exemplo anterior de análise JSON:


 // ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value


Graças à regra no-unsafe-assignment , podemos capturar any tipo antes mesmo de passar formValues para outro lugar. A estratégia de fixação permanece a mesma: podemos usar o estreitamento de tipo para fornecer um tipo específico ao valor da variável.


 // ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues


acesso sem membro inseguro e chamada sem insegurança

Essas duas regras são acionadas com muito menos frequência. No entanto, com base na minha experiência, eles são realmente úteis quando você tenta usar dependências de terceiros mal digitadas.


A regra no-unsafe-member-access nos impede de acessar as propriedades do objeto se uma variável tiver o tipo any , pois pode ser null ou undefined .


A regra no-unsafe-call nos impede de chamar uma variável com any tipo como uma função, pois pode não ser uma função.


Vamos imaginar que temos uma biblioteca de terceiros mal digitada chamada untyped-auth :


 // ❌ Incorrect import { authenticate } from 'untyped-auth'; // ^? any const userInfo = authenticate(); // ^? any ^ Unsafe call of an `any` typed value. console.log(userInfo.name); // ^ Unsafe member access .name on an `any` value.


O linter destaca dois problemas:

  • Chamar a função authenticate pode ser inseguro, pois podemos esquecer de passar argumentos importantes para a função.
  • Ler a propriedade name do objeto userInfo não é seguro, pois será null se a autenticação falhar.


A melhor maneira de corrigir esses erros é considerar o uso de uma biblioteca com uma API fortemente tipada. Mas se isso não for uma opção, você mesmo poderáaumentar os tipos de biblioteca . Um exemplo com os tipos de biblioteca fixos seria assim:


 // ✅ Correct import { authenticate } from 'untyped-auth'; // ^? (login: string, password: string) => Promise<UserInfo | null> const userInfo = await authenticate('test', 'pwd'); // ^? UserInfo | null if (userInfo) { console.log(userInfo.name); }


sem retorno inseguro

A regra no-unsafe-return ajuda a não retornar acidentalmente any tipo de uma função que deveria retornar algo mais específico. Esses casos podem induzir o compilador a pensar que um valor retornado tem um determinado tipo, enquanto os dados podem, na verdade, ter um tipo diferente.


Por exemplo, suponha que temos uma função que analisa JSON e retorna um objeto com duas propriedades.


 // ❌ Incorrect interface FormValues { name: string; address: string; } function parseForm(json: string): FormValues { return JSON.parse(json); // ^ Unsafe return of an `any` typed value. } const form = parseForm('null'); console.log(form.name); // ^ TypeError: Cannot read properties of null


A função parseForm pode levar a erros de execução em qualquer parte do programa onde for utilizada, pois o valor analisado não é verificado. A regra no-unsafe-return evita tais problemas de tempo de execução.


É fácil corrigir isso adicionando validação para garantir que o JSON analisado corresponda ao tipo esperado. Vamos usar a biblioteca Zod desta vez:


 // ✅ Correct import { z } from 'zod'; const schema = z.object({ name: z.string(), address: z.string(), }); function parseForm(json: string): FormValues { return schema.parse(JSON.parse(json)); }


Uma nota sobre desempenho

O uso de regras de verificação de tipo acarreta uma penalidade de desempenho para o ESLint, pois ele deve invocar o compilador do TypeScript para inferir todos os tipos. Essa lentidão é perceptível principalmente ao executar o linter em ganchos de pré-confirmação e em CI, mas não é perceptível ao trabalhar em um IDE. A verificação de tipo é executada uma vez na inicialização do IDE e, em seguida, atualiza os tipos conforme você altera o código.


É importante notar que apenas inferir os tipos funciona mais rápido do que a invocação normal do compilador tsc . Por exemplo, em nosso projeto mais recente com cerca de 1,5 milhão de linhas de código TypeScript, a verificação de tipo por meio tsc leva cerca de 11 minutos, enquanto o tempo adicional necessário para que as regras de reconhecimento de tipo do ESLint sejam inicializadas é de apenas cerca de 2 minutos.


Para nossa equipe, a segurança adicional fornecida pelo uso de regras de análise estática com reconhecimento de tipo vale a pena. Em projetos menores, esta decisão é ainda mais fácil de tomar.

Conclusão

Controlar o uso de any em projetos TypeScript é crucial para obter segurança de tipo e qualidade de código ideais. Ao utilizar o plugin typescript-eslint , os desenvolvedores podem identificar e eliminar quaisquer ocorrências de any tipo em sua base de código, resultando em uma base de código mais robusta e de fácil manutenção.


Ao usar regras eslint com reconhecimento de tipo, qualquer aparecimento da palavra-chave any em nossa base de código será uma decisão deliberada, e não um erro ou descuido. Essa abordagem nos protege de usar any código em nosso próprio código, bem como na biblioteca padrão e em dependências de terceiros.


No geral, um linter com reconhecimento de tipo nos permite atingir um nível de segurança de tipo semelhante ao de linguagens de programação de tipo estaticamente, como Java, Go, Rust e outras. Isso simplifica muito o desenvolvimento e a manutenção de grandes projetos.


Espero que você tenha aprendido algo novo com este artigo. Obrigado por ler!

Links Úteis