Tipos Condicionales

En el corazón de la mayoría de los programas útiles, tenemos que tomar decisiones basadas en las entradas. Los programas JavaScript no son diferentes, pero dado que los valores pueden ser fácilmente introspeccionados, esas decisiones también se basan en los tipos de entradas. Los tipos condicionales ayudan a describir la relación entre los tipos de entradas y salidas.

Prueba este código ↗

interface Animal {
  live(): void;
}
interface Dog extends Animal {
  woof(): void;
}
 
type Example1 = Dog extends Animal ? number : string;
        
type Example1 = number
 
type Example2 = RegExp extends Animal ? number : string;
        
type Example2 = string

Los tipos condicionales toman una forma que se parece un poco a las expresiones condicionales (condition ? trueExpression : falseExpression) en JavaScript:

Prueba este código ↗

  SomeType extends OtherType ? TrueType : FalseType;

Cuando el tipo de la izquierda de extends se puede asignar al de la derecha, obtendrás el tipo en la primera rama (la rama “verdadera”); de lo contrario, obtendrás el tipo en la última rama (la rama “falsa”).

De los ejemplos anteriores, los tipos condicionales pueden no parecer útiles de inmediato; ¡podemos decirnos que Dog extends Animal y elegir number o string! Pero el poder de los tipos condicionales proviene de su uso con generics.

Por ejemplo, tomemos la siguiente función createLabel:

Prueba este código ↗

interface IdLabel {
  id: number /* algunos campos */;
}
interface NameLabel {
  name: string /* otros campos */;
}
 
function createLabel(id: number): IdLabel;
function createLabel(name: string): NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel;
function createLabel(nameOrId: string | number): IdLabel | NameLabel {
  throw "unimplemented";
}

Estas sobrecargas para createLabel describen una única función de JavaScript que realiza una elección según los tipos de sus entradas. Ten en cuenta algunas cosas:

  1. Si una biblioteca tiene que hacer el mismo tipo de elección una y otra vez en toda su API, esto se vuelve engorroso.
  2. Tenemos que crear tres sobrecargas: una para cada caso en el que estemos seguros del tipo (una para string y otra para number), y otra para el caso más general (tomando un string | number). Por cada nuevo tipo que createLabel puede manejar, el número de sobrecargas crece exponencialmente.

En lugar de eso, podemos codificar esa lógica en un tipo condicional:

Prueba este código ↗

type NameOrId<T extends number | string> = T extends number
  ? IdLabel
  : NameLabel;

Luego podemos usar ese tipo condicional para simplificar nuestras sobrecargas a una sola función sin sobrecargas.

Prueba este código ↗

function createLabel<T extends number | string>(idOrName: T): NameOrId<T> {
  throw "unimplemented";
}
 
let a = createLabel("typescript");
   
let a: NameLabel
 
let b = createLabel(2.8);
   
let b: IdLabel
 
let c = createLabel(Math.random() ? "hello" : 42);

let c: NameLabel | IdLabel

Restricciones de tipo condicional

A menudo, las comprobaciones de tipo condicional nos proporcionarán información nueva. Así como el estrechamiento con protecciones de tipo puede darnos un tipo más específico, la rama verdadera de un tipo condicional restringirá aún más los generics según el tipo que comparamos.

Por ejemplo, tomemos lo siguiente:

Prueba este código ↗

type MessageOf<T> = T["message"];
Error generado
Type '"message"' cannot be used to index type 'T'.

En este ejemplo, se producen errores de TypeScript porque no se sabe si T tiene una propiedad llamada message. Podríamos restringir T y TypeScript ya no se quejaría:

Prueba este código ↗

type MessageOf<T extends { message: unknown }> = T["message"];
 
interface Email {
  message: string;
}
 
type EmailMessageContents = MessageOf<Email>;
              
type EmailMessageContents = string

Sin embargo, ¿qué pasaría si quisiéramos que MessageOf tomara cualquier tipo y que el valor predeterminado fuera algo como never si una propiedad message no está disponible? Podemos hacer esto eliminando la restricción e introduciendo un tipo condicional:

Prueba este código ↗

type MessageOf<T> = T extends { message: unknown } ? T["message"] : never;
 
interface Email {
  message: string;
}
 
interface Dog {
  bark(): void;
}
 
type EmailMessageContents = MessageOf<Email>;
              
type EmailMessageContents = string
 
type DogMessageContents = MessageOf<Dog>;
             
type DogMessageContents = never

Dentro de la rama verdadera, TypeScript sabe que T tendrá una propiedad message.

Como otro ejemplo, también podríamos escribir un tipo llamado Flatten que aplana los tipos de arrays a sus tipos de elementos, pero de lo contrario los deja tal cual:

Prueba este código ↗

type Flatten<T> = T extends any[] ? T[number] : T;
 
// Extrae el tipo de elemento.
type Str = Flatten<string[]>;
     
type Str = string
 
// Deja el tipo tal cual.
type Num = Flatten<number>;
     
type Num = number

Cuando a Flatten se le asigna un tipo de array, utiliza un acceso indexado con number para obtener el tipo de elemento de string[]. De lo contrario, simplemente devuelve el tipo que se le proporcionó.

Inferir dentro de tipos condicionales

Nos encontramos usando tipos condicionales para aplicar restricciones y luego extraer tipos. Esta termina siendo una operación tan común que los tipos condicionales la hacen más fácil.

Los tipos condicionales nos brindan una manera de inferir a partir de los tipos que comparamos en la rama verdadera usando la palabra clave infer. Por ejemplo, podríamos haber inferido el tipo de elemento en Flatten en lugar de recuperarlo “manualmente” con un tipo de acceso indexado:

Prueba este código ↗

type Flatten<Type> = Type extends Array<infer Item> ? Item : Type;

Aquí usamos la palabra clave infer para introducir declarativamente una nueva variable de tipo genérico llamada Item en lugar de especificar cómo recuperar el tipo de elemento de Type dentro de la rama verdadera. Esto nos libera de tener que pensar en cómo profundizar y sondear la estructura de los tipos que nos interesan.

Podemos escribir algunos alias de tipos de ayuda útiles usando la palabra clave infer. Por ejemplo, para casos simples, podemos extraer el tipo de retorno de los tipos de funciones:

Prueba este código ↗

type GetReturnType<Type> = Type extends (...args: never[]) => infer Return
  ? Return
  : never;
 
type Num = GetReturnType<() => number>;
     
type Num = number
 
type Str = GetReturnType<(x: string) => string>;
     
type Str = string
 
type Bools = GetReturnType<(a: boolean, b: boolean) => boolean[]>;
      
type Bools = boolean[]

Cuando se infiere a partir de un tipo con múltiples firmas de llamada (como el tipo de una función sobrecargada), las inferencias se hacen a partir de la última firma (que, presumiblemente, es el caso más permisivo). No es posible realizar una resolución de sobrecarga basada en una lista de tipos de argumentos.

Prueba este código ↗

declare function stringOrNum(x: string): number;
declare function stringOrNum(x: number): string;
declare function stringOrNum(x: string | number): string | number;
 
type T1 = ReturnType<typeof stringOrNum>;
     
type T1 = string | number

Tipos condicionales distributivos

Cuando los tipos condicionales actúan sobre un tipo genérico, se vuelven distributivos cuando se les da un tipo de unión. Por ejemplo, toma lo siguiente:

Prueba este código ↗

type ToArray<Type> = Type extends any ? Type[] : never;

Si conectamos un tipo de unión en ToArray, entonces el tipo condicional se aplicará a cada miembro de esa unión.

Prueba este código ↗

type ToArray<Type> = Type extends any ? Type[] : never;
 
type StrArrOrNumArr = ToArray<string | number>;
           
type StrArrOrNumArr = string[] | number[]

Lo que sucede aquí es que ToArray se distribuye en:

Prueba este código ↗

  string | number;

y mapea cada tipo de miembro de la unión, a lo que es efectivamente:

Prueba este código ↗

  ToArray<string> | ToArray<number>;

lo cual nos deja con:

Prueba este código ↗

  string[] | number[];

Normalmente, la distributividad es el comportamiento deseado. Para evitar ese comportamiento, puedes rodear cada lado de la palabra clave extends entre corchetes.

Prueba este código ↗

type ToArrayNonDist<Type> = [Type] extends [any] ? Type[] : never;
 
// 'ArrOfStrOrNum' no es más una unión.
type ArrOfStrOrNum = ToArrayNonDist<string | number>;
          
type ArrOfStrOrNum = (string | number)[]
Última actualización