Estrechamiento de tipos
Receta
Estrecha tipos de TypeScript en tiempo de ejecución usando guardas de tipo, funciones de aserción, el operador in y la palabra clave satisfies. Escribe código que le dé a TypeScript suficiente información para inferir tipos precisos en cada rama.
Ejemplo funcional
// Estrechamiento con typeof
function formatValue(value: string | number | boolean) {
if (typeof value === "string") {
return value.toUpperCase(); // TypeScript sabe: string
}
if (typeof value === "number") {
return value.toFixed(2); // TypeScript sabe: number
}
return value ? "Yes" : "No"; // TypeScript sabe: boolean
}
// Guarda de tipo personalizada con predicado "is"
type User = { kind: "user"; name: string; email: string };
type Admin = { kind: "admin"; name: string; permissions: string[] };
type Account = User | Admin;
function isAdmin(account: Account): account is Admin {
return account.kind === "admin";
}
function AccountBadge({ account }: { account: Account }) {
if (isAdmin(account)) {
return <span>Admin: {account.permissions.length} permissions</span>;
}
return <span>User: {account.email}</span>;
}// Estrechamiento con operador "in"
function renderAccount(account: Account) {
if ("permissions" in account) {
// TypeScript estrecha a Admin
return <div>{account.permissions.join(", ")}</div>;
}
// TypeScript estrecha a User
return <div>{account.email}</div>;
}// Palabra clave satisfies - validar sin ampliar
const ROUTES = {
home: "/",
about: "/about",
contact: "/contact",
} satisfies Record<string, string>;
// El tipo se preserva como { home: "/"; about: "/about"; contact: "/contact" }
// No se amplía a Record<string, string>
type RouteKey = keyof typeof ROUTES; // "home" | "about" | "contact"Análisis profundo
Cómo funciona
- Estrechamiento con typeof funciona para tipos primitivos:
string,number,boolean,symbol,bigint,undefined,functionyobject. - Guardas de tipo personalizadas usan la anotación de tipo de retorno
param is Type. Cuando la función devuelvetrue, TypeScript estrecha el parámetro aTypeen el ámbito de llamada. - Operador
inestrecha basándose en la existencia de propiedad."email" in accountestrecha a los miembros de la unión que tienen una propiedademail. instanceofestrecha instancias de clase:if (error instanceof TypeError)estrecha aTypeError.satisfiesvalida que una expresión se ajusta a un tipo sin cambiar su tipo inferido. Esto preserva tipos literales y formas específicas mientras asegura la corrección.- Funciones de aserción usan
asserts param is Type. Lanzan si la condición es falsa y estrechan el tipo para todo el código siguiente (no solo el bloqueif).
Variaciones
Función de aserción:
function assertIsString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
function processInput(input: unknown) {
assertIsString(input);
// TypeScript sabe que input es string a partir de aquí
console.log(input.toUpperCase());
}Estrechamiento con Array.isArray:
function renderItems(data: string | string[]) {
if (Array.isArray(data)) {
return <ul>{data.map((item) => <li key={item}>{item}</li>)}</ul>;
}
return <p>{data}</p>;
}Estrechamiento de unión discriminada (switch):
type AsyncState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: Error };
function renderState<T>(state: AsyncState<T>, renderData: (data: T) => React.ReactNode) {
switch (state.status) {
case "idle":
return null;
case "loading":
return <Spinner />;
case "success":
return renderData(state.data); // Estrechado: data existe
case "error":
return <ErrorMessage error={state.error} />; // Estrechado: error existe
}
}satisfies con objetos de configuración:
type ColorConfig = Record<string, { bg: string; text: string }>;
const THEME = {
primary: { bg: "#3b82f6", text: "#ffffff" },
danger: { bg: "#ef4444", text: "#ffffff" },
success: { bg: "#22c55e", text: "#ffffff" },
} satisfies ColorConfig;
// THEME.primary está completamente tipado con valores literales
// THEME.nonExistent produciría un errorNotas de TypeScript
- Las guardas de tipo estrechan en las ramas
ifyelse. En elelsedeisAdmin(account), TypeScript sabe queaccountesUser. satisfiesse agregó en TypeScript 4.9. Es especialmente valioso para objetos de configuración, mapas de rutas y definiciones de constantes.- Las funciones de aserción estrechan para todo el código después de la aserción, no solo dentro de un bloque
if. Esto las hace poderosas para validación temprana al inicio de una función. - Las comprobaciones nulas (
if (x),if (x != null)) estrechannullyundefinedpero también estrechan valores falsy como0y"". Usa!= nullpara estrechamiento preciso de null/undefined.
Trampas
- Las guardas de tipo personalizadas ponen la corrección en ti. TypeScript confía en tu predicado
is. Si escribes una guarda defectuosa, TypeScript estará equivocado sobre el tipo estrechado. typeof null === "object"es una peculiaridad de JavaScript. Usavalue !== null && typeof value === "object"para comprobaciones de objeto.- Las funciones de aserción deben lanzar (no devolver false) cuando la condición falla. TypeScript se basa en que la función nunca devuelva normalmente en el caso negativo.
- Desestructurar antes de estrechar rompe la conexión.
const { status } = state; if (status === "success") { state.data }no estrechastatea la variante success. Usastate.statusdirectamente.
Alternativas
| Enfoque | Ventajas | Desventajas |
|---|---|---|
Guarda de tipo personalizada (is) | Reutilizable, legible, explícita | La corrección de la guarda es tu responsabilidad |
Operador in | No necesita función auxiliar | Solo comprueba la existencia de propiedad, no el tipo de valor |
instanceof | Integrado en JavaScript | Solo funciona con clases, no con interfaces |
satisfies | Valida sin ampliar | No crea un tipo reutilizable |
Función de aserción (asserts) | Estrecha todo el código siguiente | Debe lanzar, no puede devolver false |
Zod .parse() | Seguridad en tiempo de ejecución y compilación | Dependencia externa |
Preguntas frecuentes
¿Qué es el estrechamiento de tipos en TypeScript?
- El proceso de refinar un tipo amplio a uno más específico dentro de un bloque de código.
- TypeScript rastrear el estrechamiento a través del análisis de flujo de control (
if,switch,typeof,in, etc.). - Después del estrechamiento, puedes acceder a propiedades específicas del tipo estrechado sin errores.
¿Cómo funciona el estrechamiento con typeof y qué tipos soporta?
typeofestrecha a:string,number,boolean,symbol,bigint,undefined,functionyobject.- Dentro del bloque
if (typeof value === "string"), TypeScript sabe quevalueesstring. - No distingue entre arrays, dates u otros subtipos de objeto.
¿Qué es una guarda de tipo personalizada y cómo se escribe?
function isAdmin(account: Account): account is Admin {
return account.kind === "admin";
}- Usa la anotación de tipo de retorno
param is Type. - Cuando la función devuelve
true, TypeScript estrecha el parámetro al tipo especificado. - La corrección de la guarda es tu responsabilidad.
¿Cuál es la diferencia entre una guarda de tipo (is) y una función de aserción (asserts)?
- Una guarda de tipo estrecha dentro del bloque
ifdonde se comprueba. - Una función de aserción estrecha para todo el código después de la llamada de aserción (no solo dentro de un
if). - Las funciones de aserción deben lanzar cuando la condición falla -- no pueden devolver
false.
¿Cómo difiere la palabra clave satisfies de una anotación de tipo?
satisfiesvalida que una expresión se ajusta a un tipo sin cambiar su tipo inferido.- Una anotación de tipo (
const x: T = ...) amplía el tipo aT. satisfiespreserva tipos literales y formas específicas mientras asegura la corrección.
Trampa: ¿Por qué typeof null === "object" causa problemas?
- Esta es una peculiaridad bien conocida de JavaScript.
- Usar
typeof value === "object"solo no excluyenull. - Siempre escribe
value !== null && typeof value === "object"para comprobaciones de objeto seguras.
¿Cómo estrecha el operador in los tipos?
if ("permissions" in account) {
// TypeScript estrecha al miembro de la unión que tiene "permissions"
account.permissions; // OK
}- Estrecha basándose en la existencia de propiedad, no en el valor de la propiedad.
- No necesita función auxiliar, pero es menos preciso que una guarda de tipo personalizada.
Trampa: ¿Por qué desestructurar antes de estrechar rompe la inferencia de tipos?
const { status } = state; if (status === "success") { state.data }falla porque TypeScript pierde la conexión entrestatusystate.- Usa
state.statusdirectamente en la comprobación para mantener el estrechamiento. - Este es un error común con uniones discriminadas.
¿Cuándo deberías usar satisfies vs una anotación de tipo para objetos de configuración?
// Con satisfies: preserva tipos literales
const ROUTES = {
home: "/",
about: "/about",
} satisfies Record<string, string>;
// Tipo: { home: "/"; about: "/about" }
// Con anotación: amplía a Record<string, string>
const ROUTES2: Record<string, string> = { home: "/", about: "/about" };- Usa
satisfiescuando quieras validación Y tipos literales preservados.
¿Pueden las guardas de tipo estrechar en las ramas if y else?
- Sí. En la rama
elsedeif (isAdmin(account)), TypeScript sabe queaccountesUser(el otro miembro de la unión). - Esto funciona para todas las técnicas de estrechamiento:
typeof,in,instanceofy guardas personalizadas.
¿A qué estrecha Array.isArray()?
function renderItems(data: string | string[]) {
if (Array.isArray(data)) {
return data.map((item) => item); // data es string[]
}
return data; // data es string
}- Estrecha el tipo a la variante array de la unión.
- Funciona como una guarda de tipo integrada reconocida por TypeScript.