TypeScript: Más allá del any
Tabla de Contenidos
Abrir tabla de contenidos
- Dominando el tipado avanzado para proyectos de alto impacto
- 1. El poder de
unknowny el estrechamiento de tipos (Narrowing) - 2. Type Guards y Predicados de Tipo: El filtro de seguridad
- 3. Utility Types: DRY (Don’t Repeat Yourself) en el tipado
- 4. Discriminated Unions: El patrón de oro
- 5. Generics: Flexibilidad sin perder el control
- 6. El operador
satisfies(TS 4.9+) - Conclusión
- 1. El poder de
Dominando el tipado avanzado para proyectos de alto impacto
En el ecosistema de desarrollo actual, usar TypeScript como si fuera JavaScript con esteroides es un desperdicio. Muchos nos quedamos en la superficie, pero la verdadera potencia aparece cuando dejamos de pelear con el compilador y empezamos a usarlo como nuestro arquitecto de cabecera.
Aquí te presento una inmersión profunda en las técnicas que separan a los desarrolladores de “copy-paste” de los ingenieros de software senior.
1. El poder de unknown y el estrechamiento de tipos (Narrowing)
El tipo any es el “botón de pánico”: apaga todas las alarmas del compilador. Por el contrario, unknown es la forma honesta de decir “no sé qué es esto todavía”.
¿Por qué usarlo? Porque te obliga a realizar comprobaciones de seguridad. Es ideal para respuestas de APIs, entradas de formularios dinámicos o integraciones con librerías externas de dudosa procedencia.
async function fetchConfig(id: string): Promise<unknown> {
const response = await fetch(`/api/config/${id}`);
return response.json();
}
const config = await fetchConfig("user-123");
// config.theme; // Error: Object is of type 'unknown'
if (config && typeof config === "object" && "theme" in config) {
// Aquí TS ya sabe que es un objeto con la propiedad theme
console.log("Configurando tema:", config.theme);
}
2. Type Guards y Predicados de Tipo: El filtro de seguridad
A veces las interfaces se parecen mucho o manejamos uniones de tipos complejas. Un Type Predicate es una función que retorna un booleano, pero con un superpoder: si retorna true, el compilador “aprende” el tipo de la variable en el resto del bloque.
Escenario práctico: Diferenciar entre un error esperado de la API y un error crítico de sistema.
interface ApiError { message: string; code: number }
interface SystemError { stack: string; fatal: boolean }
function isApiError(error: any): error is ApiError {
return (error as ApiError).code !== undefined;
}
try {
// ... lógica de negocio
} catch (err) {
if (isApiError(err)) {
alert(`Error de API (${err.code}): ${err.message}`);
} else {
console.error("Error crítico de sistema", err);
}
}
3. Utility Types: DRY (Don’t Repeat Yourself) en el tipado
Si tienes una interfaz User y creas otra llamada UserUpdate manualmente, estás trabajando doble. TS ya tiene herramientas para transformar tipos existentes.
Partial<T>: Útil para funciones depatcho configuración inicial.Omit<T, K>: Perfecto para proteger datos sensibles (ej. quitar elpasswordantes de mandar el objeto al frontend).Readonly<T>: Inmutabilidad total a nivel de tipo.
interface DBUser {
id: string;
name: string;
email: string;
role: 'admin' | 'user';
createdAt: Date;
}
// Creamos un tipo para el registro (sin ID ni fecha de creación)
type UserRegistration = Omit<DBUser, 'id' | 'createdAt'>;
// Creamos un tipo para edición (todo opcional menos el ID)
type UserUpdate = Partial<UserRegistration> & Pick<DBUser, 'id'>;
4. Discriminated Unions: El patrón de oro
Este es, quizás, el patrón más importante en Redux, estados de React o lógica de navegación. Consiste en añadir una propiedad común (usualmente llamada type o kind) a varios tipos para que TS los pueda diferenciar fácilmente.
type LoadingState = { status: 'loading' };
type SuccessState = { status: 'success', data: string[] };
type ErrorState = { status: 'error', error: Error };
type State = LoadingState | SuccessState | ErrorState;
function renderUI(state: State) {
switch (state.status) {
case 'loading': return "Cargando...";
case 'success': return `Items: ${state.data.length}`;
case 'error': return `Error: ${state.error.message}`;
}
}
5. Generics: Flexibilidad sin perder el control
Los genéricos te permiten escribir funciones o componentes que funcionen con múltiples tipos, pero manteniendo la integridad. Son los “parámetros de los tipos”.
interface ApiResponse<T> {
data: T;
timestamp: number;
}
async function getResource<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
const data = await res.json();
return { data, timestamp: Date.now() };
}
// Al llamarlo, definimos qué esperamos recibir
const products = await getResource<Product[]>("/products");
6. El operador satisfies (TS 4.9+)
Este operador es una joya reciente. Te permite validar que un objeto coincide con un tipo, pero retiene la forma exacta de los valores originales para un autocompletado más fino.
type Color = string | { r: number, g: number, b: number };
const palette = {
primary: "#4f46e5",
secondary: { r: 100, g: 100, b: 255 }
} satisfies Record<string, Color>;
// Gracias a satisfies, TS sabe que primary es un string:
palette.primary.toUpperCase();
// Y sabe que secondary es un objeto:
console.log(palette.secondary.r);
Conclusión
Dominar estos conceptos no es solo por “presumir” un código más limpio. Se trata de seguridad. En proyectos grandes, un buen sistema de tipos actúa como una unidad de pruebas constante que corre mientras escribes cada línea de código.
No le tengas miedo a la complejidad del tipado; tenle miedo a un runtime error que pudiste evitar con un poco de ingeniería previa.
¿Cuál de estos vas a aplicar en tu próximo PR?