learning-front

Extra · Type-level TypeScript (opcional)

Conditional types

El if/else de los tipos: T extends U ? X : Y, el tercer uso de extends, y la distributividad sobre uniones que hace desaparecer lo que no encaja.

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:

typescript
// 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.

typescript
// 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.

typescript
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:

typescript
// 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:

typescript
// 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.
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.