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.
TypeScript fornece uma variedade de ferramentas adicionais para aprimorar a experiência e a produtividade do desenvolvedor:
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:
parse
. Quando você digita data.
em seu editor, você não receberá sugestões corretas sobre os métodos disponíveis para data
.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.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.
É 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:
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.
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
.
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.
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.
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.
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);
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
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:
authenticate
pode ser inseguro, pois podemos esquecer de passar argumentos importantes para a função.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); }
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)); }
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.
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!