paint-brush
Liberar el poder de TypeScript: consideraciones clave en tsconfigpor@nodge
2,700 lecturas
2,700 lecturas

Liberar el poder de TypeScript: consideraciones clave en tsconfig

por Maksim Zemskov13m2023/07/12
Read on Terminal Reader

Demasiado Largo; Para Leer

TypeScript es un lenguaje popular para crear aplicaciones complejas, gracias a su fuerte sistema de tipos y capacidades de análisis estático. Sin embargo, para lograr la máxima seguridad de tipos, es importante configurar tsconfig correctamente. En este artículo, analizaremos las consideraciones clave para configurar tsconfig para lograr una seguridad de tipos óptima.
featured image - Liberar el poder de TypeScript: consideraciones clave en tsconfig
Maksim Zemskov HackerNoon profile picture
0-item
1-item

Si está creando aplicaciones web complejas, es probable que TypeScript sea su lenguaje de programación preferido. TypeScript es apreciado por su sólido sistema de tipos y sus capacidades de análisis estático, lo que lo convierte en una poderosa herramienta para garantizar que su código sea sólido y sin errores.


También acelera el proceso de desarrollo a través de la integración con editores de código, lo que permite a los desarrolladores navegar por el código de manera más eficiente y obtener sugerencias más precisas y finalización automática, además de permitir la refactorización segura de grandes cantidades de código.


El Compilador es el corazón de TypeScript, responsable de verificar la corrección de tipos y transformar el código TypeScript en JavaScript. Sin embargo, para utilizar completamente el poder de TypeScript, es importante configurar el Compilador correctamente.


Cada proyecto de TypeScript tiene uno o más archivos tsconfig.json que contienen todas las opciones de configuración del compilador.


La configuración de tsconfig es un paso crucial para lograr una seguridad de tipos y una experiencia de desarrollador óptimas en sus proyectos de TypeScript. Al tomarse el tiempo para considerar cuidadosamente todos los factores clave involucrados, puede acelerar el proceso de desarrollo y garantizar que su código sea sólido y sin errores.

Desventajas de la configuración estándar

La configuración predeterminada en tsconfig puede hacer que los desarrolladores pierdan la mayoría de los beneficios de TypeScript. Esto se debe a que no habilita muchas capacidades potentes de verificación de tipos. Por configuración "predeterminada", me refiero a una configuración en la que no se establecen opciones del compilador de verificación de tipo.


Por ejemplo:


 { "compilerOptions": { "target": "esnext", "module": "esnext", "esModuleInterop": true, "forceConsistentCasingInFileNames": true, "skipLibCheck": true, }, "include": ["src"] }


La ausencia de varias opciones de configuración clave puede resultar en una menor calidad del código por dos razones principales. En primer lugar, el compilador de TypeScript puede manejar incorrectamente tipos null e undefined en varios casos.


En segundo lugar, any tipo puede aparecer de forma incontrolable en su base de código, lo que lleva a la verificación de tipo deshabilitada en torno a este tipo.


Afortunadamente, estos problemas son fáciles de solucionar ajustando algunas opciones en la configuración.

El modo estricto

 { "compilerOptions": { "strict": true } }


El modo estricto es una opción de configuración esencial que proporciona garantías más sólidas de la corrección del programa al permitir una amplia gama de comportamientos de verificación de tipos.


Habilitar el modo estricto en el archivo tsconfig es un paso crucial para lograr la máxima seguridad de tipos y una mejor experiencia para los desarrolladores.


Requiere un poco de esfuerzo adicional para configurar tsconfig, pero puede ayudar mucho a mejorar la calidad de su proyecto.


La opción de compilador strict habilita todas las opciones de la familia de modo estricto, que incluyen noImplicitAny , strictNullChecks , strictFunctionTypes , entre otros.


Estas opciones también se pueden configurar por separado, pero no se recomienda desactivar ninguna de ellas. Veamos ejemplos para ver por qué.

Cualquier inferencia implícita

 { "compilerOptions": { "noImplicitAny": true } }


El tipo any es una escapatoria peligrosa en el sistema de tipos estáticos, y su uso desactiva todas las reglas de verificación de tipos. Como resultado, se pierden todos los beneficios de TypeScript: se pierden errores, las sugerencias del editor de código dejan de funcionar correctamente, etc.


Usar any está bien solo en casos extremos o para necesidades de creación de prototipos. A pesar de nuestros mejores esfuerzos, any tipo a veces puede infiltrarse implícitamente en una base de código.


Por defecto, el compilador nos perdona muchos errores a cambio de la aparición de any en un código base. Específicamente, TypeScript nos permite no especificar el tipo de variables, incluso cuando el tipo no se puede inferir automáticamente.


El problema es que podemos olvidarnos accidentalmente de especificar el tipo de una variable, por ejemplo, a un argumento de función. En lugar de mostrar un error, TypeScript deducirá automáticamente el tipo de la variable como any .


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


Habilitar la opción del compilador noImplicitAny hará que el compilador resalte todos los lugares donde el tipo de una variable se deduce automáticamente como any . En nuestro ejemplo, TypeScript nos pedirá que especifiquemos el tipo para el argumento de la función.


 function parse(str) { // ^ Error: Parameter 'str' implicitly has an 'any' type. return str.split(''); }


Cuando especificamos el tipo, TypeScript detectará rápidamente el error de pasar un número a un parámetro de cadena. El valor de retorno de la función, almacenado en la variable res2 , también tendrá el tipo correcto.


 function parse(str: string) { return str.split(''); } const res1 = parse(42); // ^ Error: Argument of type 'number' is not // assignable to parameter of type 'string' const res2 = parse('hello'); // ^? string[]


Tipo desconocido en variables de captura

 { "compilerOptions": { "useUnknownInCatchVariables": true } }


La configuración de useUnknownInCatchVariables permite el manejo seguro de excepciones en bloques try-catch. De forma predeterminada, TypeScript asume que el tipo de error en un bloque catch es any , lo que nos permite hacer cualquier cosa con el error.


Por ejemplo, podríamos pasar el error detectado tal cual a una función de registro que acepta una instancia de Error .


 function logError(err: Error) { // ... } try { return JSON.parse(userInput); } catch (err) { // ^? any logError(err); }


Sin embargo, en realidad, no hay garantías sobre el tipo de error y solo podemos determinar su verdadero tipo en tiempo de ejecución cuando ocurre el error. Si la función de registro recibe algo que no es un Error , se producirá un error de tiempo de ejecución.


Por lo tanto, la opción useUnknownInCatchVariables cambia el tipo de error de any a unknown para recordarnos que verifiquemos el tipo de error antes de hacer algo con él.


 try { return JSON.parse(userInput); } catch (err) { // ^? unknown // Now we need to check the type of the value if (err instanceof Error) { logError(err); } else { logError(new Error('Unknown Error')); } }


Ahora, TypeScript nos pedirá que verifiquemos el tipo de la variable err antes de pasarla a la función logError , lo que dará como resultado un código más correcto y seguro. Desafortunadamente, esta opción no ayuda con los errores de tipeo en las funciones promise.catch() o las funciones de devolución de llamada.


Pero discutiremos formas de lidiar con any en tales casos en el próximo artículo.

Comprobación de tipos para los métodos Call y Apply

 { "compilerOptions": { "strictBindCallApply": true } }


Otra opción corrige la apariencia de any llamada en función a través de call and apply . Este es un caso menos común que los dos primeros, pero aún así es importante tenerlo en cuenta. De forma predeterminada, TypeScript no verifica los tipos en tales construcciones.


Por ejemplo, podemos pasar cualquier cosa como argumento a una función y, al final, siempre recibiremos el tipo any .


 function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? any const n2 = parse.call(undefined, false); // ^? any


Habilitar la opción strictBindCallApply hace que TypeScript sea más inteligente, por lo que el tipo de devolución se deducirá correctamente como number . Y cuando intente pasar un argumento del tipo incorrecto, TypeScript señalará el error.


 function parse(value: string) { return parseInt(value, 10); } const n1 = parse.call(undefined, '10'); // ^? number const n2 = parse.call(undefined, false); // ^ Argument of type 'boolean' is not // assignable to parameter of type 'string'.


Tipos estrictos para el contexto de ejecución

 { "compilerOptions": { "noImplicitThis": true } }


La siguiente opción que puede ayudar a prevenir la aparición de any en su proyecto corrige el manejo del contexto de ejecución en las llamadas a funciones. La naturaleza dinámica de JavaScript dificulta la determinación estática del tipo de contexto dentro de una función.


De forma predeterminada, TypeScript usa el tipo any para el contexto en tales casos y no proporciona ninguna advertencia.


 class Person { private name: string; constructor(name: string) { this.name = name; } getName() { return function () { return this.name; // ^ 'this' implicitly has type 'any' because // it does not have a type annotation. }; } }


Habilitar la opción del compilador noImplicitThis nos pedirá que especifiquemos explícitamente el tipo de contexto para una función. De esta forma, en el ejemplo anterior, podemos detectar el error de acceder al contexto de la función en lugar del campo name de la clase Person .


Soporte nulo e indefinido en TypeScript

 { "compilerOptions": { "strictNullChecks": true } }


A continuación, varias opciones que se incluyen en el modo strict no dan como resultado any tipo que aparezca en la base de código. Sin embargo, hacen que el comportamiento del compilador de TS sea más estricto y permiten que se encuentren más errores durante el desarrollo.


La primera de estas opciones corrige el manejo de null e undefined en TypeScript. De forma predeterminada, TypeScript asume que null e undefined son valores válidos para cualquier tipo, lo que puede generar errores de tiempo de ejecución inesperados.


Habilitar la opción del compilador strictNullChecks obliga al desarrollador a manejar explícitamente los casos en los que pueden ocurrir null e undefined .


Por ejemplo, considere el siguiente código:


 const users = [ { name: 'Oby', age: 12 }, { name: 'Heera', age: 32 }, ]; const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } console.log(loggedInUser.age); // ^ TypeError: Cannot read properties of undefined


Este código se compilará sin errores, pero puede arrojar un error de tiempo de ejecución si el usuario con el nombre "Max" no existe en el sistema y users.find() devuelve undefined . Para evitar esto, podemos habilitar la opción del compilador strictNullChecks .


Ahora, TypeScript nos obligará a manejar explícitamente la posibilidad de que users.find() devuelva null o undefined .


 const loggedInUser = users.find(u => u.name === 'Max'); // ^? { name: string; age: number; } | undefined if (loggedInUser) { console.log(loggedInUser.age); }


Al manejar explícitamente la posibilidad de null y undefiined , podemos evitar errores de tiempo de ejecución y asegurarnos de que nuestro código sea más robusto y libre de errores.

Tipos de funciones estrictas

 { "compilerOptions": { "strictFunctionTypes": true } }


Habilitar strictFunctionTypes hace que el compilador de TypeScript sea más inteligente. Antes de la versión 2.6, TypeScript no verificaba la contravarianza de los argumentos de la función. Esto dará lugar a errores de tiempo de ejecución si se llama a la función con un argumento del tipo incorrecto.


Por ejemplo, incluso si un tipo de función es capaz de manejar cadenas y números, podemos asignar una función a ese tipo que solo puede manejar cadenas. Todavía podemos pasar un número a esa función, pero recibiremos un error de tiempo de ejecución.


 function greet(x: string) { console.log("Hello, " + x.toLowerCase()); } type StringOrNumberFn = (y: string | number) => void; // Incorrect Assignment const func: StringOrNumberFn = greet; // TypeError: x.toLowerCase is not a function func(10);


Afortunadamente, habilitar la opción strictFunctionTypes corrige este comportamiento y el compilador puede detectar estos errores en tiempo de compilación, mostrándonos un mensaje detallado de la incompatibilidad de tipos en las funciones.


 const func: StringOrNumberFn = greet; // ^ Type '(x: string) => void' is not assignable to type 'StringOrNumberFn'. // Types of parameters 'x' and 'y' are incompatible. // Type 'string | number' is not assignable to type 'string'. // Type 'number' is not assignable to type 'string'.


Inicialización de propiedad de clase

 { "compilerOptions": { "strictPropertyInitialization": true } }


Por último, pero no menos importante, la opción strictPropertyInitialization permite verificar la inicialización de propiedad de clase obligatoria para tipos que no incluyen undefined como valor.


Por ejemplo, en el siguiente código, el desarrollador olvidó inicializar la propiedad email . De forma predeterminada, TypeScript no detectaría este error y podría producirse un problema en tiempo de ejecución.


 class UserAccount { name: string; email: string; constructor(name: string) { this.name = name; // Forgot to assign a value to this.email } }


Sin embargo, cuando la opción strictPropertyInitialization está habilitada, TypeScript resaltará este problema para nosotros.


 email: string; // ^ Error: Property 'email' has no initializer and // is not definitely assigned in the constructor.

Firmas de índice seguras

 { "compilerOptions": { "noUncheckedIndexedAccess": true } }


La opción noUncheckedIndexedAccess no forma parte del modo strict , pero es otra opción que puede ayudar a mejorar la calidad del código en su proyecto. Permite que la comprobación de las expresiones de acceso al índice tenga un tipo de retorno null o undefined , lo que puede evitar errores de tiempo de ejecución.


Considere el siguiente ejemplo, donde tenemos un objeto para almacenar valores en caché. Luego obtenemos el valor de una de las claves. Por supuesto, no tenemos ninguna garantía de que el valor de la clave deseada realmente exista en el caché.


De forma predeterminada, TypeScript asumiría que el valor existe y tiene el tipo string . Esto puede conducir a un error de tiempo de ejecución.


 const cache: Record<string, string> = {}; const value = cache['key']; // ^? string console.log(value.toUpperCase()); // ^ TypeError: Cannot read properties of undefined


Habilitar la opción noUncheckedIndexedAccess en TypeScript requiere verificar las expresiones de acceso de índice para un tipo de retorno undefined , lo que puede ayudarnos a evitar errores de tiempo de ejecución. Esto también se aplica al acceso a elementos en una matriz.


 const cache: Record<string, string> = {}; const value = cache['key']; // ^? string | undefined if (value) { console.log(value.toUpperCase()); }

Configuración recomendada

En función de las opciones discutidas, se recomienda enfáticamente habilitar las opciones de acceso strict y noUncheckedIndexedAccess en el archivo tsconfig.json de su proyecto para una seguridad de tipos óptima.


 { "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, } }


Si ya ha habilitado la opción strict , puede considerar eliminar las siguientes opciones para evitar duplicar la opción strict: true :


  • noImplicitAny
  • useUnknownInCatchVariables
  • strictBindCallApply
  • noImplicitThis
  • strictFunctionTypes
  • strictNullChecks
  • strictPropertyInitialization


También se recomienda eliminar las siguientes opciones que pueden debilitar el sistema de tipos o causar errores de tiempo de ejecución:


  • keyofStringsOnly
  • noStrictGenericChecks
  • suppressImplicitAnyIndexErrors
  • suppressExcessPropertyErrors


Al considerar y configurar cuidadosamente estas opciones, puede lograr una seguridad de tipo óptima y una mejor experiencia de desarrollador en sus proyectos de TypeScript.

Conclusión

TypeScript ha recorrido un largo camino en su evolución, mejorando constantemente su compilador y sistema de tipos. Sin embargo, para mantener la compatibilidad con versiones anteriores, la configuración de TypeScript se ha vuelto más compleja, con muchas opciones que pueden afectar significativamente la calidad de la verificación de tipos.


Al considerar y configurar cuidadosamente estas opciones, puede lograr una seguridad de tipo óptima y una mejor experiencia de desarrollador en sus proyectos de TypeScript. Es importante saber qué opciones habilitar y eliminar de la configuración de un proyecto.


Comprender las consecuencias de deshabilitar ciertas opciones le permitirá tomar decisiones informadas para cada una.


Es importante tener en cuenta que la tipificación estricta puede tener consecuencias. Para lidiar de manera efectiva con la naturaleza dinámica de JavaScript, deberá tener una buena comprensión de TypeScript más allá de simplemente especificar "número" o "cadena" después de una variable.


Necesitará estar familiarizado con construcciones más complejas y el primer ecosistema de bibliotecas y herramientas de TypeScript para resolver de manera más efectiva los problemas relacionados con el tipo que surgirán.


Como resultado, escribir código puede requerir un poco más de esfuerzo, pero según mi experiencia, este esfuerzo vale la pena para proyectos a largo plazo.


Espero que hayas aprendido algo nuevo de este artículo. Esta es la primera parte de una serie. En el próximo artículo, discutiremos cómo lograr una mejor seguridad de tipo y calidad de código mejorando los tipos en la biblioteca estándar de TypeScript. ¡Estén atentos, y gracias por leer!

Enlaces útiles