Buenas prácticas de TypeScript: código más seguro y fácil de mantener
Aprende los patrones de TypeScript que más importan: modo estricto, tipos utilitarios, genéricos, uniones discriminadas y cómo evitar las trampas más comunes del sistema de tipos.
TypeScript ha pasado de ser opcional a imprescindible en la mayoría de los proyectos JavaScript profesionales. Pero hay una gran diferencia entre "técnicamente TypeScript" y "TypeScript que realmente detecta errores". Esa diferencia se reduce a un puñado de patrones que desbloquean el verdadero poder del sistema de tipos.
Activa el modo estricto — siempre
El cambio de configuración de TypeScript con mayor impacto:
// tsconfig.json
{
"compilerOptions": {
"strict": true
}
}
strict activa un conjunto de verificaciones:
strictNullChecks—nullyundefinedno son asignables a otros tiposnoImplicitAny— las variables no pueden tener implícitamente el tipoanystrictFunctionTypes— verificación más estricta de parámetros de funciónstrictPropertyInitialization— las propiedades de clase deben inicializarse
Sin strictNullChecks, TypeScript pasa por alto la clase de errores en tiempo de ejecución más frecuente. Actívalo desde el primer día: añadirlo a una base de código grande es un proceso doloroso.
Evita any — usa unknown en su lugar
any es una vía de escape del sistema de tipos que silencia todos los errores y anula el propósito de TypeScript:
// ❌ any desactiva toda la verificación de tipos
function processData(data: any) {
data.nonExistentMethod(); // ¡Sin error! Falla en tiempo de ejecución.
}
// ✅ unknown te obliga a reducir el tipo antes de usarlo
function processData(data: unknown) {
if (typeof data === "string") {
console.log(data.toUpperCase()); // Seguro — reducido a string
}
if (data instanceof Error) {
console.log(data.message); // Seguro — reducido a Error
}
}
unknown es la versión segura de any. Te obliga a verificar qué tienes antes de usarlo. Úsalo para respuestas de API, parseo de JSON y cualquier dato proveniente de fuentes externas.
Uniones discriminadas para manejo exhaustivo
Uno de los patrones más poderosos de TypeScript:
type LoadingState = { status: "loading" };
type SuccessState = { status: "success"; data: User[] };
type ErrorState = { status: "error"; message: string };
type ApiState = LoadingState | SuccessState | ErrorState;
function render(state: ApiState) {
switch (state.status) {
case "loading":
return <Spinner />;
case "success":
return <UserList users={state.data} />; // state.data disponible aquí
case "error":
return <ErrorMessage msg={state.message} />; // state.message disponible aquí
default:
// TypeScript dará un error aquí si añades un nuevo estado sin manejarlo
const _exhaustive: never = state;
throw new Error(`Unhandled state: ${_exhaustive}`);
}
}
La asignación a never al final es la verificación de exhaustividad: si añades una nueva variante a ApiState sin manejarla en el switch, TypeScript genera un error en tiempo de compilación. Esta es una de las redes de seguridad más valiosas de todo el lenguaje.
Tipos utilitarios
TypeScript incluye transformadores de tipos integrados:
interface User {
id: number;
name: string;
email: string;
role: "admin" | "editor" | "viewer";
createdAt: Date;
}
// Hace todas las propiedades opcionales
type PartialUser = Partial<User>;
// Hace todas las propiedades obligatorias
type RequiredUser = Required<User>;
// Hace todas las propiedades de solo lectura
type ReadonlyUser = Readonly<User>;
// Selecciona propiedades específicas
type UserPreview = Pick<User, "id" | "name">;
// Excluye propiedades específicas
type UserWithoutDates = Omit<User, "createdAt">;
// Crea un tipo a partir de los valores de un objeto
type UserRole = User["role"]; // "admin" | "editor" | "viewer"
// Hace las propiedades anulables
type NullableUser = { [K in keyof User]: User[K] | null };
// Tipo Record: objeto con tipos de clave y valor específicos
type RolePermissions = Record<UserRole, string[]>;
// Extract/Exclude de tipos unión
type AdminOrEditor = Extract<UserRole, "admin" | "editor">; // "admin" | "editor"
type NonAdmin = Exclude<UserRole, "admin">; // "editor" | "viewer"
Genéricos para código reutilizable y con tipos seguros
Los genéricos te permiten escribir funciones y clases que funcionan con cualquier tipo sin perder la seguridad de tipos:
// Sin genéricos — pierde información de tipo
function first(arr: any[]): any {
return arr[0];
}
// Con genéricos — preserva el tipo
function first<T>(arr: T[]): T | undefined {
return arr[0];
}
const num = first([1, 2, 3]); // tipo: number | undefined
const str = first(["a", "b"]); // tipo: string | undefined
Restringir genéricos
// T debe tener una propiedad 'id'
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
}
// K debe ser una clave de T
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { id: 1, name: "Alice", role: "admin" };
const name = getProperty(user, "name"); // tipo: string
const role = getProperty(user, "role"); // tipo: string
// getProperty(user, "missing"); // ❌ Error de TypeScript — "missing" no es una clave
Type guards y narrowing
// Predicado de tipo — type guard personalizado
function isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
"name" in value &&
"email" in value
);
}
// Uso
const data: unknown = await fetchUser();
if (isUser(data)) {
console.log(data.name); // TypeScript sabe que data es User aquí
}
Para validar respuestas de API en tiempo de ejecución, combina las interfaces de TypeScript con validación de esquemas en tiempo de ejecución usando Zod. Genera esquemas Zod directamente desde tu JSON con nuestro conversor JSON to Zod: pega un ejemplo de respuesta JSON y obtén un esquema con tipos seguros al instante.
Tipos literales de plantilla
TypeScript 4.1+ admite manipulación de cadenas a nivel de tipos:
type EventName = "click" | "focus" | "blur";
type Handler = `on${Capitalize<EventName>}`; // "onClick" | "onFocus" | "onBlur"
type CSSProperty = "margin" | "padding";
type CSSDirection = "top" | "right" | "bottom" | "left";
type CSSKey = `${CSSProperty}-${CSSDirection}`;
// "margin-top" | "margin-right" | ... | "padding-left"
Operador satisfies (TypeScript 4.9+)
Valida que un valor coincide con un tipo sin ampliarlo:
type ColorMap = Record<string, string>;
// ❌ Sin satisfies — el tipo se amplía a Record<string, string>
const colors: ColorMap = {
primary: "#3b82f6",
secondary: "#8b5cf6",
};
colors.primary.toUpperCase(); // Funciona pero pierde el tipo string
// ✅ Con satisfies — valida Y preserva los tipos literales
const colors = {
primary: "#3b82f6",
secondary: "#8b5cf6",
} satisfies ColorMap;
colors.primary.toUpperCase(); // ✅ TypeScript sabe que es un string
colors.nonexistent; // ❌ Error — la clave no existe
Generar tipos a partir de datos
En lugar de escribir interfaces de TypeScript a mano desde respuestas de API:
- Pega una respuesta JSON de ejemplo en nuestro conversor JSON to TypeScript para obtener interfaces al instante
- O genera un esquema Zod con JSON to Zod — Zod infiere los tipos de TypeScript automáticamente mediante
z.infer<typeof schema>
import { z } from "zod";
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email(),
role: z.enum(["admin", "editor", "viewer"]),
});
// Tipo inferido automáticamente — sin duplicación
type User = z.infer<typeof UserSchema>;
// Valida en tiempo de ejecución Y proporciona tipos de TypeScript
const user = UserSchema.parse(await response.json());
Antipatrones comunes que debes evitar
| Antipatrón | Problema | Solución |
|---|---|---|
as any |
Desactiva la verificación de tipos | Usa unknown + narrowing |
// @ts-ignore |
Silencia errores sin corregirlos | Corrige el error de tipo |
as SomeType (cast inseguro) |
Omite la inferencia | Usa type guards |
interface para uniones |
No funciona con uniones discriminadas | Usa type |
object demasiado genérico |
Sin estructura | Define una interfaz adecuada |
Function como tipo |
Sin info de parámetros/retorno | Usa una firma específica |
Configuración de TypeScript que vale la pena conocer
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true, // array[n] devuelve T | undefined
"exactOptionalPropertyTypes": true, // distingue entre ausente e undefined
"noImplicitReturns": true, // todos los caminos de código deben retornar
"noFallthroughCasesInSwitch": true, // sin caída accidental en switch
"verbatimModuleSyntax": true // import type explícito para imports solo de tipo
}
}
El valor de TypeScript es proporcional a la rigurosidad con que lo uses. Los patrones aquí descritos — modo estricto, uniones discriminadas, genéricos, verificaciones de exhaustividad — son lo que separa a un TypeScript que detecta errores de uno que solo añade sintaxis.