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.
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.
{ "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é.
{ "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[]
{ "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.
{ "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'.
{ "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
.
{ "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.
{ "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'.
{ "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.
{ "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()); }
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.
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!