<!-- Canonical: https://lucioduran.com/es/blog/typescript-5-5-type-predicates-inference
Language: es
Content-SHA256: bee4053d4cae3fb220e10ba117d9998fe1727729f7ba2545cd9fc160f9250fb6
-->

TypeScript

# TypeScript 5.5: Type Predicates Inferidos, Isolated Declarations y Gotchas del Mundo Real

2025-12-249 min lectura

Lucio Durán
Engineering Manager & AI Solutions Architect

También disponible en: [English](<https://lucioduran.com/blog/typescript-5-5-type-predicates-inference>)

## Type predicates inferidos: la mejora de calidad de vida más grande en años

Esta es probablemente la mejora de calidad de vida más anticipada para desarrolladores TypeScript. Considerar el patrón más común en cualquier codebase:

```typescript
// Antes de TS 5.5
const values: (string | null)[] = ["hola", null, "mundo", null, "!"];

// Esto DEBERÍA darnos string[], pero no
const filtered = values.filter((v) => v !== null);
// Tipo: (string | null)[], MAL! Acabamos de filtrar los nulls!

// El workaround era este código adicional
const filteredCorrect = values.filter((v): v is string => v !== null);
// Tipo: string[], Correcto, pero boilerplate molesto
```

En TypeScript 5.5, el compilador infiere automáticamente que ` (v) => v !== null ` es un type predicate. Sin anotación:

```typescript
// TypeScript 5.5
const values: (string | null)[] = ["hola", null, "mundo", null, "!"];

const filtered = values.filter((v) => v !== null);
// Tipo: string[], POR FIN!

// Funciona con patrones más complejos también
interface User {
 id: string;
 name: string;
 email: string | null;
 deletedAt: Date | null;
}

const users: User[] = getUsers();

const activeWithEmail = users.filter((u) => {
 return u.deletedAt === null && u.email !== null;
});
```

### Dónde funciona la inferencia (y dónde no)

La inferencia tiene reglas específicas que se necesita conocer:

```typescript
// FUNCIONA: sentencia return explícita
const isString = (x: unknown) => {
 return typeof x === "string";
};
// Inferido: (x: unknown) => x is string

// NO FUNCIONA: cuerpo de expresión (sin llaves)
const isStringExpr = (x: unknown) => typeof x === "string";
// Inferido: (x: unknown) => boolean, SIN predicado!
// Esto es porque los cuerpos de expresión podrían tener side effects
// que hacen la inferencia de predicados unsound

// FUNCIONA: chequeos de null/undefined
const isDefined = <T>(x: T | null | undefined) => {
 return x !== null && x !== undefined;
};
// Inferido: (x: T | null | undefined) => x is T

// FUNCIONA: chequeos instanceof
const isDate = (x: unknown) => {
 return x instanceof Date;
};
// Inferido: (x: unknown) => x is Date

// FUNCIONA: chequeos de propiedad
const hasName = (x: object) => {
 return "name" in x;
};
// Inferido: (x: object) => x is object & Record<"name", unknown>

// NO FUNCIONA: condiciones complejas con side effects
let sideEffect = false;
const isBad = (x: unknown) => {
 sideEffect = true; // Side effect previene inferencia
 return typeof x === "string";
};
// Inferido: (x: unknown) => boolean
```

### El gotcha de la migración

Este es un gotcha común de migración. Consideremos código así:

```typescript
interface ApiResponse {
 data: User | null;
 error: string | null;
}

const responses: ApiResponse[] = await Promise.all(requests);

const successful = responses.filter((r) => r.data !== null);

// Antes: successful[0].data era User | null (sin narrowing)
// Después: TypeScript ahora hace narrowing mejor a través de filter

// El PROBLEMA: código downstream que hacía null checks redundantes
// ahora se marca porque TS sabe que el valor no puede ser null
successful.forEach((r) => {
 if (r.data === null) {
 // TS 5.5: Error! Esta condición siempre será false
 // Porque .filter() ya hizo narrowing
 return;
 }
 processUser(r.data);
});
```

El fix suele ser remover los null checks ahora redundantes. Pero si teners un codebase grande, eso significa docenas de cambios. Un approach práctico: corregirlos gradualmente con comentarios ` @ts-expect-error ` y una regla de lint para limpiarlos con el tiempo.

## Isolated declarations: revolución de velocidad de build en monorepos

Esta es la feature que más importa para equipos grandes. ` isolatedDeclarations ` es una nueva opción del compilador que requiere que las funciones y valores exportados tengan anotaciones de tipo de retorno explícitas:

```typescript
// tsconfig.json
{
 "compilerOptions": {
 "isolatedDeclarations": true,
 "declaration": true
 }
}

// SIN isolatedDeclarations (return types implícitos están bien)
export function add(a: number, b: number) {
 return a + b; // Return type inferido como: number
}

// CON isolatedDeclarations (return type explícito requerido)
export function add(a: number, b: number): number {
 return a + b;
}

// Error sin return type explícito:
export function processData(input: string) {
 // ^^^ Error!
 // La función exportada debe tener una anotación de tipo de retorno
 // explícita con --isolatedDeclarations
 const parsed = JSON.parse(input);
 return { ...parsed, processed: true };
}

// Fix: agregar return type explícito
interface ProcessedData {
 processed: true;
 [key: string]: unknown;
}

export function processData(input: string): ProcessedData {
 const parsed = JSON.parse(input);
 return { ...parsed, processed: true };
}
```

¿Por qué importa? Porque con tipos explícitos en todas las exportaciones, una herramienta puede generar archivos ` .d.ts ` para un paquete **sin hacer type-checking del paquete entero**. Esto significa:

1. **Generación paralela de .d.ts**: Cada paquete en tu monorepo puede generar declarations independientemente
2. **Builds incrementales**: Solo regenerar declarations para paquetes que cambiaron
3. **Herramientas de terceros**: Tools como ` oxc `, ` swc ` y ` esbuild ` pueden generar archivos ` .d.ts ` sin correr el compilador TypeScript completo

Para un monorepo con 23 paquetes, esto baja el tiempo de ` tsc --build ` de 94 segundos a 31 segundos.

### Estrategia de adopción gradual

No se necesita habilitarlo en todos lados de una:

```json
// packages/shared-utils/tsconfig.json (paquete hoja, comenzar aquí)
{
 "extends": "../../tsconfig.base.json",
 "compilerOptions": {
 "isolatedDeclarations": true
 }
}

// packages/api-client/tsconfig.json (depende de shared-utils)
// Habilitar DESPUÉS de migrar shared-utils
{
 "extends": "../../tsconfig.base.json",
 "compilerOptions": {
 "isolatedDeclarations": true
 }
}

// apps/web/tsconfig.json (app, no necesita habilitarse)
{
 "extends": "../../tsconfig.base.json"
}
```

Comenzar con tus paquetes hoja (sin dependencias internas), después avanzar hacia adentro. Los paquetes de app no lo necesitan porque no producen declarations.

## Config extends: la variable template ${configDir}

TypeScript 5.5 introduce ` ${configDir} ` como variable template en paths de tsconfig. Esto resuelve un dolor de larga data en monorepos:

```json
// ANTES: los paths son relativos a la ubicación del config que extiende
// tsconfig.base.json
{
 "compilerOptions": {
 "outDir": "./dist", // Relativo a... ¿qué archivo?
 "rootDir": "./src", // Cambia de significado cuando se extiende
 "paths": {
 "@/*": ["./src/*"] // Se rompe en configs anidados
 }
 }
}

// DESPUÉS: ${configDir} siempre se refiere al directorio del archivo
// tsconfig.base.json
{
 "compilerOptions": {
 "outDir": "${configDir}/dist",
 "rootDir": "${configDir}/src",
 "paths": {
 "@/*": ["${configDir}/src/*"]
 }
 }
}
```

Ahora cuando ` packages/api/tsconfig.json ` extiende ` ../../tsconfig.base.json `, ` ${configDir} ` resuelve a ` packages/api/ `, no al root. No más paths rotos.

## Regex type narrowing

TypeScript 5.5 agrega type narrowing a través de llamadas ` .test() ` y ` .match() ` de expresiones regulares:

```typescript
function processInput(value: string | number) {
 if (typeof value === "string") {
 if (/^\d+$/.test(value)) {
 // value sigue siendo string, pero TS ahora sabe que matcheó la regex
 const num = parseInt(value, 10); // TS sabe que esto es seguro
 }
 }
}

// Donde NO funciona
const result = "hola".search(/patron/);
// search devuelve un number, no un boolean, sin narrowing

const replaced = "hola".replace(/h/, "H");
// replace devuelve un string, sin narrowing
```

## Patrones del mundo real que mejoraron

Algunos patrones de un codebase real que mejoraron con la 5.5:

### Filtrado de uniones discriminadas

```typescript
type Event =
 | { type: "click"; x: number; y: number }
 | { type: "keypress"; key: string }
 | { type: "scroll"; delta: number };

const events: Event[] = getEvents();

// Antes de TS 5.5: necesitabas predicado explícito
const clicks = events.filter(
 (e): e is Extract<Event, { type: "click" }> => e.type === "click"
);

// Después de TS 5.5: el predicado se infiere
const clicks = events.filter((e) => {
 return e.type === "click";
});
// Tipo: { type: "click"; x: number; y: number }[]
```

### Array.from con map y filter

```typescript
const validEntries = Array.from(formData.entries())
 .map(([key, value]) => {
 if (typeof value !== "string") return null;
 if (value.trim() === "") return null;
 return { key, value: value.trim() };
 })
 .filter((entry) => {
 return entry !== null;
 });
// TS 5.5: { key: string; value: string }[]
// TS 5.4: ({ key: string; value: string } | null)[]
```

## Los números de rendimiento

Números reales de una migración de monorepo (23 paquetes, ~180K líneas de TypeScript):

| Métrica | TS 5.4 | TS 5.5 | TS 5.5 + isolatedDecl |
| --- | --- | --- | --- |
| Build completo | 94s | 89s | 31s |
| Incremental (1 archivo) | 12s | 11s | 3.2s |
| Generación declarations | 28s | 26s | 8.1s |
| Responsividad IDE | ~800ms | ~650ms | ~650ms |

La mejora con ` isolatedDeclarations ` es el número titular. Pasar de 94s a 31s para un build completo es la diferencia entre "andá a buscar un café" y "ya terminó."

## Consideraciones conocidas de actualización

Algunas cosas que vale la pena saber antes de upgradear:

1. **Los predicados inferidos pueden ensanchar tus tipos**: Si una función antes devolvía ` boolean ` y ahora devuelve ` x is Foo `, código que dependía del return value booleano puede comportarse distinto con el análisis de control de flujo.
2. **isolatedDeclarations + declaration maps**: Si usa ` declarationMap `, los maps ahora apuntan al source original incluso sin type checking completo. Asegurarse que tu configuración de source maps sea consistente.
3. **La interacción satisfies + predicados inferidos**: ` satisfies ` no preserva el predicado inferido. Si teners ` const isString = ((x: unknown) => typeof x === 'string') satisfies (x: unknown) => boolean `, el predicado se pierde.
4. **Regresión de rendimiento con conditional types profundamente anidados**: Existe un caso conocido donde la nueva inferencia de tipos de TS 5.5 causó un slowdown de 10x en un archivo específico con cadenas de ` Omit<Pick<Partial<...>>> ` muy anidadas. El fix fue simplificar los tipos.

TypeScript 5.5 es una release genuinamente genial. Los type predicates inferidos arreglan uno de los papercuts más comunes del lenguaje. Isolated declarations desbloquean mejoras reales de velocidad de build para monorepos. Y las mejoras de config hacen que las configuraciones compartidas funcionen de verdad. Actualizar con cuidado, esperar algunos errores de tipo que en realidad son bug fixes, y disfrutar el viaje.

## Preguntas frecuentes

### ¿Qué son los type predicates inferidos en TypeScript 5.5?

TypeScript 5.5 puede inferir automáticamente que una función como (x) =\> x !== null actúa como type predicate (x is NonNullable\<T\>), sin que tengas que escribir la anotación 'is' manualmente. Esto hace que .filter(Boolean) y patrones similares funcionen correctamente con type narrowing.

### ¿Qué es isolatedDeclarations y por qué me debería importar?

isolatedDeclarations requiere que todas las funciones y valores exportados tengan anotaciones de tipo explícitas, permitiendo la generación de archivos de declaración sin type-checking completo. Esto habilita generación de .d.ts paralela e incremental, acelerando dramáticamente los builds de monorepos.

### ¿TypeScript 5.5 rompe código existente?

Potencialmente sí. Los type predicates inferidos pueden cambiar los tipos inferidos en llamadas .filter() existentes, lo que podría revelar errores de tipo previamente ocultos. El fix suele ser correcto, el comportamiento anterior era técnicamente incorrecto, pero puede sorprender.

## Tips

1. **Tip:** Los type predicates inferidos solo funcionan con sentencias ` return `, arrow functions con cuerpo de expresión como ` (x) => x !== null ` no infieren predicados. Envolverlos en llaves.
2. **Tip:** Habilitar ` isolatedDeclarations ` gradualmente, comenzar con tus paquetes hoja en un monorepo y avanzar hacia adentro. Requiere return types explícitos en funciones exportadas.
3. **Tip:** La variable template ` configDir ` en tsconfig extends mejora significativamente el flujo de trabajo en monorepos, utilizar ` ${configDir}/src ` en vez de paths relativos que se rompen con configs anidados.
4. **Tip:** El regex type narrowing solo funciona con ` .test() ` y ` .match() `, ` .search() ` y ` .replace() ` no hacen narrowing de tipos. Esto agarra desprevenida a la gente constantemente.

- typescript
- type-predicates
- isolated-declarations
- type-narrowing
- javascript
- dx
- compiler
- type-system

[Volver al Blog](<https://lucioduran.com/es/blog>)
