El estrechamiento de tipos (type narrowing) es TypeScript deduciendo un tipo más concreto para un valor en un punto determinado del código, a partir de las comprobaciones que el código ya ha hecho. Un parámetro string | number pasa a ser string dentro de if (typeof x === "string") y number en el else.
Llamar a value.toFixed(2) antes de la comprobación sería un error de compilación, porque toFixed no existe en string. La comprobación es JavaScript normal y se ejecuta en tiempo de ejecución; el estrechamiento es el compilador leyéndola y ajustando el tipo. No se genera nada extra.
Análisis de flujo de control
TypeScript sigue cada camino de una función: if/else, los return y throw tempranos, switch, los bucles y los operadores de cortocircuito &&, ||, ?? y ?:. En cada punto, el tipo de una variable es lo que todavía es posible ahí.
El estilo del return temprano («cláusulas de guarda») es la forma más legible de estrechar: maneja primero los casos raros y el resto de la función trabaja con el tipo limpio.
Todas las formas de estrechar
| Forma | Ejemplo | Estrecha |
|---|---|---|
| typeof | typeof x === "string" | primitivos y funciones |
| Veracidad | if (x) | quita null, undefined y los literales falsy |
| Igualdad | x === "a", x == null, x !== undefined | literales, null, undefined |
in | "swim" in pet | uniones de objetos, por propiedad |
| instanceof | err instanceof TypeError | instancias de clases |
Array.isArray | Array.isArray(x) | arrays frente a todo lo demás |
| Asignación | x = 5 | al tipo asignado |
| Predicado de tipo | function isUser(x: unknown): x is User | cualquier cosa que puedas comprobar |
| Función de aserción | function assertUser(x: unknown): asserts x is User | todo lo que va después de la llamada |
| Propiedad discriminante | switch (shape.kind) | uniones etiquetadas |
Las tres últimas se explican en las páginas de type guards y uniones discriminadas. El resto está a continuación.
Estrechamiento por veracidad
if (x) quita null y undefined (y los tipos literales false, 0 y ""). Es corto, y tiene una trampa clásica: 0 y "" son falsy, así que valores válidos se tratan como si faltaran.
Con números y strings, compara con undefined o null de forma explícita (o usa ??). La veracidad está bien para objetos, arrays y funciones, que nunca son falsy.
Estrechamiento por igualdad
===, !==, == y != estrechan los dos lados. Comparar con un literal estrecha a ese literal; == null (igualdad laxa) cubre null y undefined en una sola comprobación, y es el único sitio donde la igualdad laxa es idiomática.
Comparar dos variables estrecha ambas a lo que podrían tener en común: si a: string | number y b: string | boolean pasan a === b, las dos son string dentro del if.
El operador in
"key" in obj estrecha una unión de tipos objeto a los miembros que tienen (o pueden tener) esa propiedad.
in también funciona sobre unknown una vez que sabes que es un objeto: tras typeof v === "object" && v !== null && "id" in v, TypeScript sabe que v tiene una propiedad id de tipo unknown. Para las uniones que diseñas tú, una propiedad etiqueta compartida (kind: "fish") es más clara que buscar métodos: ese patrón se llama unión discriminada.
Estrechamiento por asignación
Una variable tiene un tipo declarado y un tipo estrechado que sigue a sus asignaciones. Asignar un valor la estrecha al tipo de ese valor, dentro de los límites del tipo declarado.
Cuándo se pierde el estrechamiento
El estrechamiento es local y conservador. Hay algunas situaciones que lo anulan:
- Una expresión distinta. Comprobar
obj.nameestrechaobj.name(yobj["name"]), pero noobj[key]cuandokeyes una variablestringen lugar de un literal, ni una copia hecha antes de la comprobación. - Callbacks y reasignación. Dentro de un callback, un
letestrechado solo mantiene el estrechamiento si no se vuelve a asignar después de crear el callback. Unaconsto un parámetro que nunca se reasigna sigue estrechado. - Comprobaciones escondidas en funciones auxiliares. Una función
isString(x: unknown): booleanno le dice nada al compilador. Dale un predicado de tipo como tipo de retorno,x is string, y las llamadas a ella estrecharán igual quetypeof.
El compilador informa index.ts(5,38): error TS18048: 'x' is possibly 'undefined'. El callback podría ejecutarse más tarde, después de x = undefined. Borra esa última asignación (o copia el valor en una const dentro del if) y compila e imprime 5 dos veces.
Una función auxiliar que devuelve boolean se arregla declarando lo que demuestra. Eso es un type guard:
function isString(value: unknown): value is string {
return typeof value === "string";
}
Desde TypeScript 5.5 el compilador infiere esos predicados para funciones flecha sencillas, y por eso list.filter((x) => x !== undefined) devuelve ahora un array sin undefined.
Preguntas frecuentes
¿Qué es el estrechamiento de tipos en TypeScript?
El estrechamiento es TypeScript refinando el tipo de una variable dentro de un bloque según una comprobación que hace el código. Tras if (typeof x === "string"), un string | number es solo string dentro del if y solo number en el else. El compilador sigue if, else, return, switch, &&, || y ?: para deducir el tipo en cada punto, lo que se llama análisis de flujo de control.
¿Por qué TypeScript no estrecha mi tipo?
Causas habituales: la comprobación se hace sobre una expresión distinta de la que usas (compruebas obj.a y usas obj[key] con un key de tipo string); el valor es un let que se reasigna después de crear un callback, así que el callback pierde el estrechamiento; o la comprobación está escondida en una función auxiliar que devuelve un simple boolean en lugar de un predicado de tipo x is T.
¿El estrechamiento de tipos funciona en tiempo de ejecución?
Las comprobaciones sí: typeof, instanceof, in y === son JavaScript normal que se ejecuta. El estrechamiento en sí es solo en tiempo de compilación. TypeScript lee tus comprobaciones y ajusta los tipos estáticos para que coincidan con ellas, y no se añade nada al JavaScript generado.
¿Cómo estrecho un tipo unknown en TypeScript?
Con las mismas comprobaciones: typeof value === "string", Array.isArray(value), value instanceof Date, o para objetos typeof value === "object" && value !== null && "id" in value. Para comprobaciones reutilizables escribe una función type guard con tipo de retorno value is T.
¿Cómo filtro undefined de un array en TypeScript?
items.filter((x) => x !== undefined) devuelve T[] sin undefined desde TypeScript 5.5, que infiere el callback como un predicado de tipo. En versiones anteriores escribe tú el predicado: items.filter((x): x is T => x !== undefined).