TypeScript afirma ser un lenguaje de programación fuertemente tipado construido sobre JavaScript, que proporciona mejores herramientas a cualquier escala. Sin embargo, TypeScript incluye any
tipo, lo que a menudo puede colarse implícitamente en una base de código y provocar la pérdida de muchas de las ventajas de TypeScript.
Este artículo explora formas de tomar el control de any
tipo en proyectos TypeScript. Prepárese para liberar el poder de TypeScript, logrando la máxima seguridad de tipos y mejorando la calidad del código.
TypeScript proporciona una variedad de herramientas adicionales para mejorar la experiencia y la productividad del desarrollador:
Sin embargo, tan pronto como comience a utilizar any
tipo en su código base, perderá todos los beneficios enumerados anteriormente. any
tipo es una laguna peligrosa en el sistema de tipos, y su uso desactiva todas las capacidades de verificación de tipos, así como todas las herramientas que dependen de la verificación de tipos. Como resultado, se pierden todos los beneficios de TypeScript: se pasan por alto los errores, los editores de código se vuelven menos útiles y más.
Por ejemplo, considere el siguiente ejemplo:
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
En el código anterior:
parse
. Cuando escribes data.
en su editor, no recibirá sugerencias correctas sobre los métodos disponibles para data
.TypeError: data.split is not a function
porque pasamos un número en lugar de una cadena. TypeScript no puede resaltar el error porque any
deshabilita la verificación de tipos.res2
también tiene el tipo any
. Esto significa que un solo uso de any
puede tener un efecto en cascada en una gran parte de una base de código.
Usar any
está bien sólo en casos extremos o para necesidades de creación de prototipos. En general, es mejor evitar el uso any
para aprovechar TypeScript al máximo.
Es importante conocer las fuentes de any
tipo en una base de código porque escribir explícitamente any
no es la única opción. A pesar de nuestros mejores esfuerzos para evitar el uso de any
tipo, a veces puede colarse implícitamente en una base de código.
Hay cuatro fuentes principales de any
tipo en un código base:
any
en una base de código.
Ya escribí artículos sobre Consideraciones clave en tsconfig y Mejora de los tipos de biblioteca estándar para los dos primeros puntos. Échales un vistazo si quieres mejorar la seguridad tipográfica en tus proyectos.
Esta vez, nos centraremos en las herramientas automáticas para controlar la apariencia de any
tipo en una base de código.
ESLint es una popular herramienta de análisis estático utilizada por los desarrolladores web para garantizar las mejores prácticas y el formato del código. Se puede utilizar para aplicar estilos de codificación y encontrar código que no cumpla con ciertas pautas.
ESLint también se puede utilizar con proyectos TypeScript, gracias al complemento Typesctipt-eslint . Lo más probable es que este complemento ya esté instalado en su proyecto. Pero si no, puedes seguir la guía oficial de introducción .
La configuración más común para typescript-eslint
es la siguiente:
module.exports = { extends: [ 'eslint:recommended', 'plugin:@typescript-eslint/recommended', ], plugins: ['@typescript-eslint'], parser: '@typescript-eslint/parser', root: true, };
Esta configuración permite eslint
comprender TypeScript a nivel de sintaxis, lo que le permite escribir reglas de eslint simples que se aplican a tipos escritos manualmente en un código. Por ejemplo, puedes prohibir el uso explícito de any
.
El ajuste preestablecido recommended
contiene un conjunto cuidadosamente seleccionado de reglas ESLint destinadas a mejorar la corrección del código. Si bien se recomienda utilizar todo el ajuste preestablecido, para los fines de este artículo, nos centraremos únicamente en la regla no-explicit-any
.
El modo estricto de TypeScript impide el uso de any
implícito, pero no impide que any
se utilice explícitamente. La regla no-explicit-any
ayuda a prohibir la escritura manual any
lugar de una 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 {}
El objetivo principal de esta regla es evitar el uso de any
en todo el equipo. Esta es una forma de fortalecer el acuerdo del equipo de que se desaconseja el uso de any
en el proyecto.
Este es un objetivo crucial porque incluso un solo uso de any
puede tener un impacto en cascada en una parte importante del código base debido a la inferencia de tipos . Sin embargo, esto todavía está lejos de alcanzar la seguridad de tipo definitiva.
Aunque hemos tratado any
utilizado explícitamente, todavía hay muchos any
implícitos dentro de las dependencias de un proyecto, incluidos los paquetes npm y la biblioteca estándar de TypeScript.
Considere el siguiente código, que probablemente se verá en cualquier proyecto:
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
Tanto pokemons
variables como settings
recibieron implícitamente any
tipo. Ni no-explicit-any
ni el modo estricto de TypeScript nos avisarán en este caso. Aún no.
Esto sucede porque los tipos de response.json()
y JSON.parse()
provienen de la biblioteca estándar de TypeScript, donde estos métodos tienen una anotación any
explícita. Todavía podemos especificar manualmente un tipo mejor para nuestras variables, pero hay casi 1200 apariciones de any
de ellas en la biblioteca estándar. Es casi imposible recordar todos los casos en los que any
puede colarse en nuestro código base desde la biblioteca estándar.
Lo mismo ocurre con las dependencias externas. Hay muchas bibliotecas mal escritas en npm y la mayoría todavía está escrita en JavaScript. Como resultado, el uso de dichas bibliotecas puede generar fácilmente una gran cantidad de any
implícito en el código base.
Generalmente todavía existen muchas formas para que any
pueda colarse en nuestro código.
Idealmente, nos gustaría tener una configuración en TypeScript que haga que el compilador se queje de cualquier variable que haya recibido any
tipo por cualquier motivo. Desafortunadamente, dicha configuración no existe actualmente y no se espera que se agregue.
Podemos lograr este comportamiento utilizando el modo de verificación de tipo del complemento typescript-eslint
. Este modo funciona junto con TypeScript para proporcionar información de tipo completa desde el compilador de TypeScript a las reglas de ESLint. Con esta información, es posible escribir reglas ESLint más complejas que esencialmente amplían las capacidades de verificación de tipos de TypeScript. Por ejemplo, una regla puede encontrar todas las variables con any
tipo, independientemente de any
se obtuvo.
Para utilizar reglas con reconocimiento de tipos, debe ajustar ligeramente la configuración de 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 la inferencia de tipos para typescript-eslint
, agregue parserOptions
a la configuración de ESLint. Luego, reemplace el ajuste preestablecido recommended
con recommended-type-checked
. Este último ajuste preestablecido agrega alrededor de 17 nuevas y poderosas reglas. A los efectos de este artículo, nos centraremos en sólo cinco de ellos.
La regla no-unsafe-argument
busca llamadas a funciones en las que se pasa una variable de tipo any
como parámetro. Cuando esto sucede, se pierde la verificación de tipos y también se pierden todos los beneficios de la escritura segura.
Por ejemplo, consideremos una función saveForm
que requiere un objeto como parámetro. Supongamos que recibimos JSON, lo analizamos y obtenemos 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`.
Cuando llamamos a la función saveForm
con este parámetro, la regla no-unsafe-argument
la marca como insegura y requiere que especifiquemos el tipo apropiado para la variable value
.
Esta regla es lo suficientemente potente como para inspeccionar en profundidad las estructuras de datos anidadas dentro de los argumentos de una función. Por lo tanto, puede estar seguro de que pasar objetos como argumentos de función nunca contendrá datos sin tipo.
// ❌ Incorrect saveForm({ name: 'John', address: JSON.parse(addressJson), // ^ Unsafe assignment of an `any` value. });
La mejor manera de corregir el error es utilizar la reducción de tipos de TypeScript o una biblioteca de validación como Zod o Superstruct . Por ejemplo, escribamos la función parseFormValues
que limita el tipo preciso de datos analizados.
// ✅ 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);
Tenga en cuenta que está permitido pasar any
tipo como argumento a una función que acepte unknown
, ya que no existen problemas de seguridad asociados con hacerlo.
Escribir funciones de validación de datos puede ser una tarea tediosa, especialmente cuando se trata de grandes cantidades de datos. Por lo tanto, vale la pena considerar el uso de una biblioteca de validación de datos. Por ejemplo, con Zod, el código se vería así:
// ✅ 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);
La regla no-unsafe-assignment
busca asignaciones de variables en las que un valor tenga any
tipo. Estas asignaciones pueden inducir a error al compilador haciéndole creer que una variable tiene un tipo determinado, mientras que los datos pueden tener en realidad un tipo diferente.
Considere el ejemplo anterior de análisis JSON:
// ❌ Incorrect const formValues = JSON.parse(userInput); // ^ Unsafe assignment of an `any` value
Gracias a la regla no-unsafe-assignment
, podemos detectar any
tipo incluso antes de pasar formValues
a otra parte. La estrategia de fijación sigue siendo la misma: podemos utilizar la reducción de tipos para proporcionar un tipo específico al valor de la variable.
// ✅ Correct const formValues = parseFormValues(JSON.parse(userInput)); // ^? FormValues
Estas dos reglas se activan con mucha menos frecuencia. Sin embargo, según mi experiencia, son realmente útiles cuando intentas utilizar dependencias de terceros mal escritas.
La regla no-unsafe-member-access
nos impide acceder a las propiedades del objeto si una variable tiene any
tipo, ya que puede ser null
o undefined
.
La regla no-unsafe-call
nos impide llamar a una variable con any
tipo como función, ya que puede que no sea una función.
Imaginemos que tenemos una biblioteca de terceros mal escrita llamada 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.
El linter destaca dos cuestiones:
authenticate
puede ser inseguro, ya que podemos olvidarnos de pasar argumentos importantes a la función.name
del objeto userInfo
no es seguro, ya que será null
si falla la autenticación.
La mejor manera de corregir estos errores es considerar el uso de una biblioteca con una API fuertemente tipada. Pero si esto no es una opción, usted mismo puedeaumentar los tipos de biblioteca . Un ejemplo con los tipos de biblioteca fijos se vería así:
// ✅ 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); }
La regla no-unsafe-return
ayuda a no devolver accidentalmente any
tipo de una función que debería devolver algo más específico. Estos casos pueden inducir a error al compilador haciéndole creer que un valor devuelto tiene un tipo determinado, mientras que los datos pueden tener en realidad un tipo diferente.
Por ejemplo, supongamos que tenemos una función que analiza JSON y devuelve un objeto con dos propiedades.
// ❌ 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
La función parseForm
puede provocar errores de tiempo de ejecución en cualquier parte del programa donde se utilice, ya que el valor analizado no se verifica. La regla no-unsafe-return
evita estos problemas de tiempo de ejecución.
Solucionar esto es fácil agregando validación para garantizar que el JSON analizado coincida con el tipo esperado. Usemos la biblioteca Zod esta 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)); }
El uso de reglas de verificación de tipos conlleva una penalización de rendimiento para ESLint, ya que debe invocar el compilador de TypeScript para inferir todos los tipos. Esta desaceleración se nota principalmente cuando se ejecuta linter en ganchos de confirmación previa y en CI, pero no se nota cuando se trabaja en un IDE. La verificación de tipos se realiza una vez al iniciar el IDE y luego actualiza los tipos a medida que cambia el código.
Vale la pena señalar que simplemente inferir los tipos funciona más rápido que la invocación habitual del compilador tsc
. Por ejemplo, en nuestro proyecto más reciente con aproximadamente 1,5 millones de líneas de código TypeScript, la verificación de tipos a través tsc
demora aproximadamente 11 minutos, mientras que el tiempo adicional requerido para que se inicien las reglas de reconocimiento de tipos de ESLint es de solo aproximadamente 2 minutos.
Para nuestro equipo, la seguridad adicional que proporciona el uso de reglas de análisis estático con reconocimiento de tipo vale la pena. En proyectos más pequeños, esta decisión es aún más fácil de tomar.
Controlar el uso de any
en proyectos TypeScript es crucial para lograr una seguridad de tipos y una calidad del código óptimas. Al utilizar el complemento typescript-eslint
, los desarrolladores pueden identificar y eliminar cualquier aparición de any
tipo en su código base, lo que da como resultado un código base más sólido y fácil de mantener.
Al utilizar reglas eslint con reconocimiento de tipos, cualquier aparición de la palabra clave any
en nuestro código base será una decisión deliberada en lugar de un error o descuido. Este enfoque nos protege del any
de nuestro propio código, así como de la biblioteca estándar y dependencias de terceros.
En general, un linter con reconocimiento de tipos nos permite alcanzar un nivel de seguridad de tipos similar al de los lenguajes de programación de tipos estáticos como Java, Go, Rust y otros. Esto simplifica enormemente el desarrollo y mantenimiento de grandes proyectos.
Espero que hayas aprendido algo nuevo de este artículo. ¡Gracias por leer!