paint-brush
Hacer que TypeScript sea verdaderamente "fuertemente tipificado"by@nodge
15,793
15,793

Hacer que TypeScript sea verdaderamente "fuertemente tipificado"

Maksim Zemskov12m2023/09/10
Read on Terminal Reader

TypeScript proporciona el tipo "Cualquiera" para situaciones en las que la forma de los datos no se conoce de antemano. Sin embargo, el uso excesivo de este tipo puede generar problemas con la seguridad de tipos, la calidad del código y la experiencia del desarrollador. Este artículo explora los riesgos asociados con el tipo "Cualquiera", identifica fuentes potenciales de su inclusión en una base de código y proporciona estrategias para controlar su uso a lo largo de un proyecto.
featured image - Hacer que TypeScript sea verdaderamente "fuertemente tipificado"
Maksim Zemskov HackerNoon profile picture
0-item
1-item

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.

Desventajas de usar Any en TypeScript

TypeScript proporciona una variedad de herramientas adicionales para mejorar la experiencia y la productividad del desarrollador:


  • Ayuda a detectar errores en las primeras etapas de la etapa de desarrollo.
  • Ofrece un excelente autocompletado para editores de código e IDE.
  • Permite una refactorización sencilla de grandes bases de código a través de fantásticas herramientas de navegación de código y refactorización automática.
  • Simplifica la comprensión de una base de código al proporcionar semántica adicional y estructuras de datos explícitas a través de tipos.


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:


  • Se perderá la finalización automática dentro de la función parse . Cuando escribes data. en su editor, no recibirá sugerencias correctas sobre los métodos disponibles para data .
  • En el primer caso, hay un 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.
  • En el segundo caso, la variable 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.

De dónde viene el tipo Any

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:

  1. Opciones del compilador en tsconfig.
  2. Biblioteca estándar de TypeScript.
  3. Dependencias del proyecto.
  4. Uso explícito de 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.

Etapa 1: uso de ESLint

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 .

no-explícito-cualquiera

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.

Por qué nada explícito no es suficiente

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.

Etapa 2: Mejora de las capacidades de verificación de tipos

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.

ningún argumento inseguro

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);


no-asignación-insegura

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


sin acceso-miembro-inseguro y sin-llamada-insegura

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:

  • Llamar a la función authenticate puede ser inseguro, ya que podemos olvidarnos de pasar argumentos importantes a la función.
  • Leer la propiedad 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); }


sin retorno inseguro

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)); }


Una nota sobre el rendimiento

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.

Conclusión

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!

Enlaces útiles