Las reglas de abajo son las que evitan más bugs en código TypeScript real. Cada una muestra primero la versión habitual y después la mejor, en código que puedes ejecutar. La primera regla es la más importante: deja de usar any.
Usa unknown en lugar de any
any desactiva la comprobación de tipos de un valor y de todo lo que se calcula a partir de él. unknown también acepta cualquier valor, pero tienes que comprobarlo antes de usarlo, lo que sitúa la comprobación donde los datos entran en tu programa:
Las fronteras son los lugares donde los tipos dejan de estar garantizados: JSON.parse, las respuestas de fetch, localStorage, los datos de formularios, las variables de entorno y los mensajes de otros procesos. Valida ahí con un type guard o una librería de esquemas, y el resto del código podrá fiarse de sus tipos.
Mantén strict activado
strict es el valor por defecto en TypeScript 7; no lo desactives. Añade las comprobaciones que deja fuera y que detectan más bugs:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"noImplicitOverride": true,
"noFallthroughCasesInSwitch": true
}
}
noUncheckedIndexedAccess hace que arr[i] y record[key] incluyan undefined, que es lo que devuelven para un índice que no existe. noImplicitOverride obliga a que un método de una subclase indique override, y noFallthroughCasesInSwitch rechaza un case que continúa en el siguiente.
Deja trabajar a la inferencia
Anota lo que TypeScript no puede saber: los parámetros de las funciones y los tipos de retorno de las funciones que usan otros módulos. Deja las variables locales y los parámetros de los callbacks a la inferencia. Una anotación innecesaria no es solo ruido; puede hacer que un tipo sea más amplio que el valor:
Sin el comentario @ts-expect-error, setStatus(annotated) es el error TS2345. La const inferida conserva el tipo literal "active", así que se acepta. Pasa el cursor por una variable en tu editor para ver qué se infirió antes de añadir un tipo.
Prefiere los tipos unión a los enums
Una unión de literales de string da autocompletado y comprobaciones exhaustivas sin código generado. Cuando también necesitas la lista de valores en tiempo de ejecución, deriva el tipo de un array as const:
const ROLES = ["admin", "editor", "viewer"] as const;
type Role = (typeof ROLES)[number]; // "admin" | "editor" | "viewer"
function canEdit(role: Role): boolean {
return role !== "viewer";
}
console.log(canEdit("editor")); // true
console.log(ROLES.filter(canEdit)); // [ 'admin', 'editor' ]
canEdit("owner"); // error TS2345: Argument of type '"owner"' is not assignable to parameter of type '"admin" | "editor" | "viewer"'.
Un enum Role { Admin, Viewer } compila a un objeto con mapeo inverso, y un parámetro de enum numérico acepta cualquier variable number, incluso una con un valor que el enum no incluye. Los enums tampoco funcionan con el type stripping de Node (TypeScript enum is not supported in strip-only mode). Las ventajas e inconvenientes se comparan en la página de enums.
Comprueba los objetos de configuración con satisfies
Anotar un objeto con un tipo amplio como Record<string, Route> comprueba sus valores pero olvida sus claves. satisfies comprueba lo mismo y conserva el tipo exacto:
Usa una anotación cuando la variable deba tener exactamente el tipo declarado (un parámetro de función, un valor que vas a reasignar). Usa satisfies para tablas de búsqueda, mapas de rutas, tokens de tema y otros objetos constantes.
Modela el estado con uniones discriminadas
Un único objeto con campos opcionales permite estados que no pueden darse: loading: true junto con un error, o que falte data tras un éxito. Una unión de objetos con una etiqueta compartida solo permite los estados reales, y cada rama ve únicamente sus propios campos:
La línea con never es la comprobación exhaustiva. Cuando alguien añade un estado y se olvida de manejarlo, la compilación falla en esa línea:
El error es index.ts(14,13): error TS2322: Type '{ status: "cancelled"; }' is not assignable to type 'never'. Nombra el caso sin manejar. Añade case "cancelled": y compila.
Evita ! y as
La aserción non-null x! y la aserción de tipo x as T le dicen al compilador que deje de comprobar. Ninguna cambia el valor en tiempo de ejecución, así que una aserción equivocada se convierte más tarde en un fallo, lejos de su causa:
Sustituye ! por ?., ??, un return temprano o un error lanzado con un mensaje útil. Sustituye as por un type guard que compruebe de verdad el valor. La única aserción siempre segura es as const, porque solo hace un tipo más estrecho y de solo lectura. Una buena configuración de lint (las reglas no-non-null-assertion y no-explicit-any de typescript-eslint) marca el resto.
Haz los datos readonly
Marca como readonly las propiedades y los arrays que el código no debe cambiar. El compilador rechaza entonces push, sort y las asignaciones, y las funciones devuelven valores nuevos en lugar de modificar sus entradas:
readonly solo se comprueba en tiempo de compilación y solo a un nivel de profundidad: no congela el objeto en tiempo de ejecución. Aun así basta para detectar la modificación accidental de estado compartido, que es lo que causa la mayoría de estos bugs.
Preguntas frecuentes
¿Debo usar any en TypeScript?
Casi nunca en código de aplicación. any desactiva la comprobación del valor y de todo lo que se deriva de él. Usa unknown para valores cuyo tipo todavía no conoces y estréchalos con comprobaciones; reserva any para escapatorias poco frecuentes, con un comentario que explique por qué.
¿Debo anotar todas las variables en TypeScript?
No. Deja que TypeScript infiera las variables locales y los parámetros de los callbacks. Anota los parámetros de las funciones (no se pueden inferir) y los tipos de retorno de las funciones exportadas, para que un cambio dentro de la función no pueda cambiar su tipo público sin avisar.
¿Los enums de TypeScript son una mala práctica?
No son un error, pero muchos equipos los evitan. Un enum genera código en tiempo de ejecución, no funciona con el type stripping de Node, y un parámetro de enum numérico acepta cualquier variable number, tenga el valor que tenga. Una unión de literales de string, o un array as const con un tipo derivado, da el mismo autocompletado y las mismas comprobaciones sin código generado.
¿Cuándo debo usar aserciones de tipo con as?
Solo cuando sabes algo que el compilador no puede saber, y preferiblemente justo después de una comprobación que lo demuestre. as no cambia ningún valor en tiempo de ejecución, así que {} as User compila y después no tiene name. En la mayoría de los casos, un type guard que compruebe el valor es la herramienta más segura.
¿Qué configuración de tsconfig es la mejor para un proyecto nuevo de TypeScript?
Mantén strict activado (el valor por defecto en TypeScript 7) y añade noUncheckedIndexedAccess. Muchos proyectos activan también noImplicitOverride, noFallthroughCasesInSwitch y verbatimModuleSyntax. La configuración que escribe tsc --init activa strict, noUncheckedIndexedAccess, exactOptionalPropertyTypes y verbatimModuleSyntax, entre otras.