Bienvenido a la parte avanzada de TypeScript. En el Nivel 5 usaste los tipos para describir datos; aquí vas a programar con ellos. En el capítulo de typeof, keyof y acceso indexado viste que existe un “mundo de los tipos” paralelo al de los valores. Resulta que ese mundo tiene sus propias herramientas de control de flujo, y la primera es el conditional type: el if/else de los tipos.
Este nivel es opcional: no hace falta para construir apps con React ni para entrar a trabajar. Es para dominar TypeScript de verdad, al estilo de Total TypeScript. Si vienes del Nivel 5, tienes todo lo necesario.
El conditional type: T extends U ? X : Y#
Hasta ahora extends te apareció en dos sitios: heredar una interface (interface A extends B) y acotar un genérico (<T extends ...>). Hay un tercer uso: dentro de un tipo, como condición. La sintaxis es idéntica al operador ternario de JavaScript, pero a nivel de tipos:
// Si T es asignable a Heroe, el resultado es un tipo; si no, otro.
type EtiquetaDe<T> = T extends Heroe ? "es un héroe" : "no lo es";EtiquetaDe<Heroe> se resuelve a "es un héroe", y EtiquetaDe<number> a "no lo es". Todo en compilación: es el compilador eligiendo una rama u otra según el tipo que le pases.
// Pasa el ratón por encima: TypeScript te muestra el tipo ya resuelto.
type A = EtiquetaDe<Heroe>; // "es un héroe"
type B = EtiquetaDe<number>; // "no lo es"Distributividad: el conditional se reparte sobre la unión#
Aquí está la propiedad que hace los conditional types tan potentes. Cuando el tipo que pasas es una unión y el parámetro aparece “desnudo” (solo T, no envuelto en otra cosa), el conditional no se evalúa de golpe: se reparte sobre cada miembro de la unión y luego vuelve a unir los resultados.
type Rol = "Daño" | "Tanque" | "Apoyo";
// Con T desnudo, esto se evalúa miembro a miembro:
// "Daño" -> "Daño" (encaja)
// "Tanque" -> "Tanque" (encaja)
// "Apoyo" -> never (no encaja)
type SoloAtaque<T> = T extends "Daño" | "Tanque" ? T : never;
// Resultado: "Daño" | "Tanque" | never == "Daño" | "Tanque"
type Ataque = SoloAtaque<Rol>;¿Y por qué desaparece "Apoyo"? Porque cae en la rama : never, y aquí reaparece el never que viste en el Nivel 5: es el elemento neutro de la unión, así que "Daño" | "Tanque" | never es exactamente "Daño" | "Tanque". El miembro que no encaja se vuelve never y se evapora del resultado.
Esa combinación —conditional distributivo + never— es el mecanismo con el que se filtran uniones. De hecho, es justo así como están hechos los utility types Exclude y Extract que usaste como cajas negras: lo reconstruyes en el ejercicio.
Comprobar tipos: Equal y Expect#
Pasar el ratón por encima de un tipo funciona para mirar, pero cuando quieres dejar fijado que un tipo es exactamente el que esperas —y que el compilador te avise si algún día deja de serlo— se usa un test de tipos:
// Equal<A, B> es true si A y B son el mismo tipo, y false si no.
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
// Expect<T> solo compila si T es true; si le llega false, marca error.
type Expect<T extends true> = T;No te preocupes ahora por el interior de Equal (ese patrón con funciones genéricas lo verás a fondo en el capítulo “Testear a nivel de tipos”). Lo que importa es cómo se usa:
// Compila en silencio: SoloAtaque<Rol> ES "Daño" | "Tanque".
type _Test = Expect<Equal<SoloAtaque<Rol>, "Daño" | "Tanque">>;Si el tipo de la izquierda dejara de coincidir con el de la derecha, Equal daría false, Expect marcaría rojo y lo verías al instante. Es la red de seguridad para tu propio código de tipos, y es la herramienta con la que vas a comprobar los ejercicios de aquí en adelante.
Comprueba lo que sabes#
Pregunta 1 de 4
¿Qué hace un conditional type `T extends U ? X : Y`?
Tu turno#
Completa los conditional types del Team Builder hasta que los tests Expect<Equal<...>> queden sin subrayado rojo. Empiezas con un conditional básico y acabas reconstruyendo Exclude y Extract a mano. Cuando lo tengas (o si te atascas), despliega las soluciones y fíjate en el salto de un nivel al siguiente.
Ejercicio · en esta página
Construye conditional types para el Team Builder
Trabajas a nivel de tipos: completa los conditional types hasta que los tests Expect<Equal<...>> queden sin subrayado rojo. Empiezas con un conditional básico y acabas reconstruyendo Exclude y Extract a mano.
Paso 1: Que funcione
- EsRol<T> es un conditional básico: T extends Rol ? true : false.
- SoloAtaque<T> filtra una unión con distributividad (los que no encajan van a never).
- Los tests de EsRol y SoloAtaque quedan en verde.
Paso 2: Que generalice
- Añades Excluir<T, U> = T extends U ? never : T: el filtro genérico.
- Reconoces que es, literalmente, el utility Exclude reconstruido a mano.
- Su test queda en verde.
Paso 3: Que cierre el círculo
- Añades Incluir<T, U> = T extends U ? T : never: el contrario (el utility Extract).
- Entiendes que Exclude y Extract no son magia: un conditional distributivo sobre una unión.
- Los cinco tests en verde.
Ver soluciones
// Helper de testing de tipos (lo verás a fondo en "Testear a nivel de tipos").
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type Rol = "Daño" | "Tanque" | "Apoyo";
// EsRol<T>: un conditional type básico. Si T extiende Rol, true; si no, false.
type EsRol<T> = T extends Rol ? true : false;
// SoloAtaque<T>: aprovecha la DISTRIBUTIVIDAD. Cuando T es una unión, el conditional
// se evalúa para cada miembro por separado; los que no encajan se vuelven never y
// desaparecen de la unión (never es el elemento neutro: "Daño" | never es "Daño").
type SoloAtaque<T> = T extends "Daño" | "Tanque" ? T : never;
// Tests en verde:
type _T1 = Expect<Equal<EsRol<"Daño">, true>>;
type _T2 = Expect<Equal<EsRol<"Healer">, false>>;
type _T3 = Expect<Equal<SoloAtaque<Rol>, "Daño" | "Tanque">>;
console.log("EsRol y SoloAtaque correctos: sin subrayado rojo en los tests."); Por qué este nivel
- EsRol<T> es el conditional en su forma más pura: una condición (T extends Rol) y dos ramas (true / false).
- SoloAtaque<T> introduce la distributividad: al ser T una unión, el conditional se evalúa miembro a miembro y los que no encajan se vuelven never y desaparecen.
- Funciona, pero está atado a "Daño" | "Tanque" en concreto: no es reutilizable para otros filtros. Eso lo arregla el nivel siguiente.
// Helper de testing de tipos (lo verás a fondo en "Testear a nivel de tipos").
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type Rol = "Daño" | "Tanque" | "Apoyo";
// EsRol<T>: un conditional type básico.
type EsRol<T> = T extends Rol ? true : false;
// SoloAtaque<T>: filtro distributivo sobre la unión.
type SoloAtaque<T> = T extends "Daño" | "Tanque" ? T : never;
// Excluir<T, U>: generaliza el filtro. Quita de T los miembros que extienden U,
// volviéndolos never (y por tanto desaparecen). Es exactamente el utility Exclude
// de la librería estándar, reconstruido a mano.
type Excluir<T, U> = T extends U ? never : T;
// Tests en verde:
type _T1 = Expect<Equal<EsRol<"Daño">, true>>;
type _T2 = Expect<Equal<EsRol<"Healer">, false>>;
type _T3 = Expect<Equal<SoloAtaque<Rol>, "Daño" | "Tanque">>;
type _T4 = Expect<Equal<Excluir<Rol, "Apoyo">, "Daño" | "Tanque">>;
console.log("EsRol, SoloAtaque y Excluir correctos: sin subrayado rojo."); Por qué es mejor que el anterior
- Excluir<T, U> generaliza el filtro: en vez de fijar los valores a conservar, recibe el criterio U por parámetro.
- Es exactamente el utility Exclude de la librería estándar. Acabas de reconstruir una pieza que usabas como caja negra en el Nivel 5.
- El salto a excelente es ver que el mismo patrón, con las ramas cambiadas, da el utility contrario.
// Helper de testing de tipos (lo verás a fondo en "Testear a nivel de tipos").
type Equal<A, B> =
(<T>() => T extends A ? 1 : 2) extends (<T>() => T extends B ? 1 : 2) ? true : false;
type Expect<T extends true> = T;
type Rol = "Daño" | "Tanque" | "Apoyo";
// EsRol<T>: un conditional type básico.
type EsRol<T> = T extends Rol ? true : false;
// SoloAtaque<T>: filtro distributivo sobre la unión.
type SoloAtaque<T> = T extends "Daño" | "Tanque" ? T : never;
// Excluir<T, U>: quita de T los miembros que extienden U. Es el utility Exclude.
type Excluir<T, U> = T extends U ? never : T;
// Incluir<T, U>: el contrario de Excluir. Se queda solo con los miembros de T que
// extienden U (los demás se vuelven never y desaparecen). Es el utility Extract.
type Incluir<T, U> = T extends U ? T : never;
// El cierre del círculo: Excluir e Incluir son, literalmente, Exclude y Extract de
// la librería estándar. En un proyecto real usarías los de TypeScript (ya existen),
// pero ahora sabes que por dentro no son magia: un conditional distributivo sobre
// una unión, donde lo que no encaja se vuelve never y desaparece.
// Tests en verde:
type _T1 = Expect<Equal<EsRol<"Daño">, true>>;
type _T2 = Expect<Equal<EsRol<"Healer">, false>>;
type _T3 = Expect<Equal<SoloAtaque<Rol>, "Daño" | "Tanque">>;
type _T4 = Expect<Equal<Excluir<Rol, "Apoyo">, "Daño" | "Tanque">>;
type _T5 = Expect<Equal<Incluir<Rol, "Apoyo">, "Apoyo">>;
console.log("Los cinco tests en verde: conditional types dominados."); Por qué es mejor que el anterior
- Incluir<T, U> es Excluir con las ramas intercambiadas: se queda con lo que encaja en vez de descartarlo. Es el utility Extract.
- La lección de fondo: Exclude y Extract no son magia del compilador, son un conditional distributivo sobre una unión donde lo que sobra se vuelve never.
- En un proyecto real usarías los de TypeScript, pero ahora entiendes qué hacen por dentro: eso es lo que separa usar TypeScript de comprenderlo.