Esta página del manual ha sido reemplazada, vaya a la nueva página

Esta página enumera algunas de las formas más avanzadas en las que puede modelar tipos, funciona en conjunto con el documento de Tipos de utilidad, que incluye tipos que se incluyen en TypeScript y están disponibles a nivel mundial.

Protectores de tipo y tipos de diferenciación

Los tipos de unión son útiles para modelar situaciones en las que los valores pueden superponerse en los tipos que pueden asumir. ¿Qué sucede cuando necesitamos saber específicamente si tenemos un Fish? Un modismo común en JavaScript para diferenciar entre dos valores posibles es verificar la presencia de un miembro. Como mencionamos, solo puede acceder a miembros que estén garantizados en todos los constituyentes de un tipo de sindicato.

let pet = getSmallPet();
// You can use the 'in' operator to check
if ("swim" in pet) {
  pet.swim();
}
// However, you cannot use property access
if (pet.fly) {
  pet.fly();
}

Para que el mismo código funcione a través de los descriptores de acceso a la propiedad, necesitaremos usar una aserción de tipo:

let pet = getSmallPet();
let fishPet = pet as Fish;
let birdPet = pet as Bird;
if (fishPet.swim) {
  fishPet.swim();
} else if (birdPet.fly) {
  birdPet.fly();
}

Sin embargo, este no es el tipo de código que le gustaría tener en su base de código.

Protectores de tipo definidos por el usuario

Sería mucho mejor si una vez que realizamos la verificación, pudiéramos saber el tipo de pet dentro de cada rama.

Da la casualidad de que TypeScript tiene algo llamado guardia de tipo. Una protección de tipo es una expresión que realiza una verificación en tiempo de ejecución que garantiza el tipo en algún ámbito.

Usando predicados de tipo

Para definir un tipo de protección, simplemente necesitamos definir una función cuyo tipo de retorno sea un tipo de predicado:

function isFish(pet: Fish | Bird): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

pet is Fish es nuestro predicado de tipo en este ejemplo. Un predicado toma la forma parameterName is Type, dónde parameterName debe ser el nombre de un parámetro de la firma de la función actual.

Cualquier momento isFish se llama con alguna variable, TypeScript estrecho esa variable a ese tipo específico si el tipo original es compatible.

// Both calls to 'swim' and 'fly' are now okay.
let pet = getSmallPet();
if (isFish(pet)) {
  pet.swim();
} else {
  pet.fly();
}

Tenga en cuenta que TypeScript no solo sabe que pet es un Fish en el if rama; también sabe que en el else rama, tu no tener un Fish, entonces debes tener un Bird.

Puede utilizar la protección de tipos isFish para filtrar una matriz de Fish | Bird y obtener una matriz de Fish:

const zoo: (Fish | Bird)[] = [getSmallPet(), getSmallPet(), getSmallPet()];
const underWater1: Fish[] = zoo.filter(isFish);
// or, equivalently
const underWater2: Fish[] = zoo.filter<Fish>(isFish);
const underWater3: Fish[] = zoo.filter<Fish>((pet) => isFish(pet));

Utilizando el in operador

los in El operador también actúa como una expresión de restricción para los tipos.

Para n in x expresión, donde n es un literal de cadena o un tipo de literal de cadena y x es un tipo de unión, la rama “verdadera” se reduce a tipos que tienen una propiedad opcional o requerida n, y la rama “falsa” se reduce a tipos que tienen una propiedad opcional o faltante n.

function move(pet: Fish | Bird) {
  if ("swim" in pet) {
    return pet.swim();
  }
  return pet.fly();
}

typeof guardias de tipo

Regresemos y escribamos el código para una versión de padLeft que utiliza tipos de unión. Podríamos escribirlo con predicados de tipo de la siguiente manera:

function isNumber(x: any): x is number {
  return typeof x === "number";
}
function isString(x: any): x is string {
  return typeof x === "string";
}
function padLeft(value: string, padding: string | number) {
  if (isNumber(padding)) {
    return Array(padding + 1).join(" ") + value;
  }
  if (isString(padding)) {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

Sin embargo, tener que definir una función para averiguar si un tipo es primitivo es una especie de molestia. Afortunadamente, no necesitas abstraer typeof x === "number" en su propia función porque TypeScript lo reconocerá como un tipo de protección por sí solo. Eso significa que podríamos escribir estos cheques en línea.

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got '${padding}'.`);
}

Estas typeof guardias de tipo se reconocen de dos formas diferentes: typeof v === "typename" y typeof v !== "typename", dónde "typename" puede ser uno de typeof valores de retorno del operador ("undefined", "number", "string", "boolean", "bigint", "symbol", "object", o "function"). Si bien TypeScript no le impedirá comparar con otras cadenas, el lenguaje no reconocerá esas expresiones como protectores de tipo.

instanceof guardias de tipo

Si has leído sobre typeof protectores de tipo y están familiarizados con el instanceof operador en JavaScript, probablemente tenga alguna idea de lo que trata esta sección.

instanceof guardias de tipo son una forma de restringir tipos usando su función constructora. Por ejemplo, tomemos prestado nuestro ejemplo de padder de cuerdas de fuerza industrial de antes:

interface Padder {
  getPaddingString(): string;
}
class SpaceRepeatingPadder implements Padder {
  constructor(private numSpaces: number) {}
  getPaddingString() {
    return Array(this.numSpaces + 1).join(" ");
  }
}
class StringPadder implements Padder {
  constructor(private value: string) {}
  getPaddingString() {
    return this.value;
  }
}
function getRandomPadder() {
  return Math.random() < 0.5
    ? new SpaceRepeatingPadder(4)
    : new StringPadder("  ");
}
let padder: Padder = getRandomPadder();
if (padder instanceof SpaceRepeatingPadder) {
  padder;
}
if (padder instanceof StringPadder) {
  padder;
}

El lado derecho del instanceof debe ser una función de constructor, y TypeScript se reducirá a:

  1. el tipo de función prototype propiedad si su tipo no es any
  2. la unión de tipos devueltos por las firmas de construcción de ese tipo

en ese orden.

Tipos que aceptan valores NULL

TypeScript tiene dos tipos especiales, null y undefined, que tienen los valores nulo e indefinido respectivamente. Los mencionamos brevemente en la sección Tipos básicos.

De forma predeterminada, el verificador de tipos considera null y undefined asignable a cualquier cosa. Efectivamente, null y undefined son valores válidos de todo tipo. Eso significa que no es posible parada que no se asignen a ningún tipo, incluso cuando desee evitarlo. El inventor de null, Tony Hoare, llama a esto su “Error de mil millones de dólares”.

los --strictNullChecks flag corrige esto: cuando declaras una variable, no incluye automáticamente null o undefined. Puede incluirlos explícitamente usando un tipo de unión:

let exampleString = "foo";
exampleString = null;
let stringOrNull: string | null = "bar";
stringOrNull = null;
stringOrNull = undefined;

Tenga en cuenta que TypeScript trata null y undefined de forma diferente para que coincida con la semántica de JavaScript. string | null es de un tipo diferente a string | undefined y string | undefined | null.

Desde TypeScript 3.7 en adelante, puede usar encadenamiento opcional para simplificar el trabajo con tipos que aceptan valores NULL.

Parámetros y propiedades opcionales

Con --strictNullChecks, un parámetro opcional agrega automáticamente | undefined:

function f(x: number, y?: number) {
  return x + (y ?? 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null);

Lo mismo ocurre con las propiedades opcionales:

class C {
  a: number;
  b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined;
c.b = 13;
c.b = undefined;
c.b = null;

Guardias de tipo y afirmaciones de tipo

Dado que los tipos que aceptan valores NULL se implementan con una unión, debe usar un tipo de protección para deshacerse de la null. Afortunadamente, este es el mismo código que escribirías en JavaScript:

function f(stringOrNull: string | null): string {
  if (stringOrNull === null) {
    return "default";
  } else {
    return stringOrNull;
  }
}

los null la eliminación es bastante obvia aquí, pero también puede usar operadores terser:

function f(stringOrNull: string | null): string {
  return stringOrNull ?? "default";
}

En los casos en que el compilador no puede eliminar null o undefined, puede utilizar el operador de aserción de tipo para eliminarlos manualmente. La sintaxis es postfix !: identifier! elimina null y undefined del tipo de identifier:

interface UserAccount {
  id: number;
  email?: string;
}
const user = getUser("admin");
user.id;
if (user) {
  user.email.length;
}
// Instead if you are sure that these objects or fields exist, the
// postfix ! lets you short circuit the nullability
user!.email!.length;

Alias ​​de tipo

Los alias de tipo crean un nuevo nombre para un tipo. Los alias de tipo a veces son similares a las interfaces, pero pueden nombrar primitivas, uniones, tuplas y cualquier otro tipo que de otro modo tendría que escribir a mano.

type Second = number;
let timeInSecond: number = 10;
let time: Second = 10;

En realidad, el alias no crea un nuevo tipo, crea un nuevo nombre para referirse a ese tipo. Poner un alias en una primitiva no es muy útil, aunque puede usarse como una forma de documentación.

Al igual que las interfaces, los alias de tipo también pueden ser genéricos; simplemente podemos agregar parámetros de tipo y usarlos en el lado derecho de la declaración de alias:

type Container<T> = { value: T };

También podemos hacer que un alias de tipo se refiera a sí mismo en una propiedad:

type Tree<T> = {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
};

Junto con los tipos de intersección, podemos hacer algunos tipos bastante alucinantes:

type LinkedList<Type> = Type & { next: LinkedList<Type> };
interface Person {
  name: string;
}
let people = getDriversLicenseQueue();
people.name;
people.next.name;
people.next.next.name;
people.next.next.next.name;

Interfaces frente a alias de tipo

Como mencionamos, los alias de tipo pueden actuar como interfaces; sin embargo, existen algunas diferencias sutiles.

Casi todas las características de un interface están disponibles en type, la distinción clave es que un tipo no se puede volver a abrir para agregar nuevas propiedades frente a una interfaz que siempre es ampliable.

Interface Type

Ampliando una interfaz

interface Animal {
  name: string
}
interface Bear extends Animal {
  honey: boolean
}
const bear = getBear() 
bear.name
bear.honey
        

Extender un tipo a través de intersecciones

type Animal = {
  name: string
}
type Bear = Animal & { 
  honey: Boolean 
}
const bear = getBear();
bear.name;
bear.honey;
        

Agregar nuevos campos a una interfaz existente

interface Window {
  title: string
}
interface Window {
  ts: import("typescript")
}
const src = 'const a = "Hello World"';
window.ts.transpileModule(src, {});
        

No se puede cambiar un tipo después de haber sido creado

type Window = {
  title: string
}
type Window = {
  ts: import("typescript")
}
// Error: Duplicate identifier 'Window'.
        

Porque una interfaz mapea más de cerca cómo funcionan los objetos de JavaScript estando abierto a la extensión, recomendamos utilizar una interfaz sobre un alias de tipo cuando sea posible.

Por otro lado, si no puede expresar alguna forma con una interfaz y necesita usar un tipo de unión o tupla, los alias de tipo suelen ser el camino a seguir.

Tipos de miembros de enumeración

Como se mencionó en nuestra sección sobre enumeraciones, los miembros de enumeración tienen tipos cuando cada miembro se inicializa literalmente.

La mayor parte del tiempo cuando hablamos de “tipos singleton”, nos referimos tanto a tipos de miembros enum como a tipos literales numéricos / de cadena, aunque muchos usuarios usarán “tipos singleton” y “tipos literales” indistintamente.

Polimórfico this tipos

Un polimórfico this tipo representa un tipo que es el subtipo de la clase o interfaz contenedora. Se llama Fpolimorfismo limitado, mucha gente lo conoce como el API fluida patrón. Esto hace que las interfaces jerárquicas fluidas sean mucho más fáciles de expresar, por ejemplo. Tome una calculadora simple que devuelva this después de cada operación:

class BasicCalculator {
  public constructor(protected value: number = 0) {}
  public currentValue(): number {
    return this.value;
  }
  public add(operand: number): this {
    this.value += operand;
    return this;
  }
  public multiply(operand: number): this {
    this.value *= operand;
    return this;
  }
  // ... other operations go here ...
}
let v = new BasicCalculator(2).multiply(5).add(1).currentValue();

Dado que la clase usa this tipos, puede extenderlo y la nueva clase puede usar los métodos antiguos sin cambios.

class ScientificCalculator extends BasicCalculator {
  public constructor(value = 0) {
    super(value);
  }
  public sin() {
    this.value = Math.sin(this.value);
    return this;
  }
  // ... other operations go here ...
}
let v = new ScientificCalculator(2).multiply(5).sin().add(1).currentValue();

Sin this tipos, ScientificCalculator no hubiera podido extender BasicCalculator y mantenga la interfaz fluida. multiply hubiera regresado BasicCalculator, que no tiene el sin método. Sin embargo, con this tipos, multiply devoluciones this, cual es ScientificCalculator aquí.

Tipos de índice

Con los tipos de índice, puede hacer que el compilador verifique el código que usa nombres de propiedades dinámicas. Por ejemplo, un patrón de JavaScript común es elegir un subconjunto de propiedades de un objeto:

function pluck(o, propertyNames) {
  return propertyNames.map((n) => o[n]);
}

Así es como escribiría y usaría esta función en TypeScript, usando el consulta de tipo de índice y acceso indexado operadores:

function pluck<T, K extends keyof T>(o: T, propertyNames: K[]): T[K][] {
  return propertyNames.map((n) => o[n]);
}
interface Car {
  manufacturer: string;
  model: string;
  year: number;
}
let taxi: Car = {
  manufacturer: "Toyota",
  model: "Camry",
  year: 2014,
};
// Manufacturer and model are both of type string,
// so we can pluck them both into a typed string array
let makeAndModel: string[] = pluck(taxi, ["manufacturer", "model"]);
// If we try to pluck model and year, we get an
// array of a union type: (string | number)[]
let modelYear = pluck(taxi, ["model", "year"]);

El compilador comprueba que manufacturer y model son en realidad propiedades en Car. El ejemplo presenta un par de nuevos operadores de tipo. Primero es keyof T, los operador de consulta de tipo de índice. Para cualquier tipo T, keyof T es la unión de nombres de propiedad pública conocidos de T. Por ejemplo:

let carProps: keyof Car;

keyof Car es completamente intercambiable con "manufacturer" | "model" | "year". La diferencia es que si agrega otra propiedad a Car, decir ownersAddress: string, luego keyof Car se actualizará automáticamente para ser "manufacturer" | "model" | "year" | "ownersAddress". Y puedes usar keyof en contextos genéricos como pluck, donde posiblemente no pueda conocer los nombres de las propiedades con anticipación. Eso significa que el compilador verificará que le pase el conjunto correcto de nombres de propiedad a pluck:

// error, Type '"unknown"' is not assignable to type '"manufacturer" | "model" | "year"'
pluck(taxi, ["year", "unknown"]);

El segundo operador es T[K], los operador de acceso indexado. Aquí, la sintaxis de tipo refleja la sintaxis de la expresión. Eso significa que taxi["manufacturer"] tiene el tipo Car["manufacturer"] – que en nuestro ejemplo es solo string. Sin embargo, al igual que las consultas de tipo índice, puede utilizar T[K] en un contexto genérico, que es donde cobra vida su verdadero poder. Solo tienes que asegurarte de que la variable de tipo K extends keyof T. Aquí hay otro ejemplo con una función llamada getProperty.

function getProperty<T, K extends keyof T>(o: T, propertyName: K): T[K] {
  return o[propertyName]; // o[propertyName] is of type T[K]
}

En getProperty, o: T y propertyName: K, entonces eso significa o[propertyName]: T[K]. Una vez que devuelva el T[K] Como resultado, el compilador creará una instancia del tipo real de clave, por lo que el tipo de retorno de getProperty variará según la propiedad que solicite.

let manufacturer: string = getProperty(taxi, "manufacturer");
let year: number = getProperty(taxi, "year");
let unknown = getProperty(taxi, "unknown");

Tipos de índice y firmas de índice

keyof y T[K] interactuar con firmas de índice. Un tipo de parámetro de firma de índice debe ser ‘cadena’ o ‘número’. Si tiene un tipo con una firma de índice de cadena, keyof T estarán string | number (y no solo string, ya que en JavaScript puede acceder a una propiedad de objeto utilizando cadenas (object["42"]) o números (object[42])). Y T[string] es solo el tipo de firma de índice:

interface Dictionary<T> {
  [key: string]: T;
}
let keys: keyof Dictionary<number>;
let value: Dictionary<number>["foo"];

Si tiene un tipo con una firma de índice numérico, keyof T solo será number.

interface Dictionary<T> {
  [key: number]: T;
}
let keys: keyof Dictionary<number>;
let numberValue: Dictionary<number>[42];
let value: Dictionary<number>["foo"];

Tipos mapeados

Una tarea común es tomar un tipo existente y hacer que cada una de sus propiedades sea opcional:

interface PersonSubset {
  name?: string;
  age?: number;
}

O podríamos querer una versión de solo lectura:

interface PersonReadonly {
  readonly name: string;
  readonly age: number;
}

Esto sucede con suficiente frecuencia en JavaScript que TypeScript proporciona una forma de crear nuevos tipos basados ​​en tipos antiguos: tipos mapeados. En un tipo mapeado, el nuevo tipo transforma cada propiedad en el tipo antiguo de la misma manera. Por ejemplo, puede hacer que todas las propiedades sean opcionales o de un tipo readonly. Aquí hay un par de ejemplos:

type Partial<T> = {
  [P in keyof T]?: T[P];
};
type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Y para usarlo:

type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

Tenga en cuenta que esta sintaxis describe un tipo en lugar de un miembro. Si desea agregar miembros, puede usar un tipo de intersección:

// Use this:
type PartialWithNewMember<T> = {
  [P in keyof T]?: T[P];
} & { newMember: boolean }
// This is an error!
type WrongPartialWithNewMember<T> = {
  [P in keyof T]?: T[P];
  newMember: boolean;
}

Echemos un vistazo al tipo mapeado más simple y sus partes:

type Keys = "option1" | "option2";
type Flags = { [K in Keys]: boolean };

La sintaxis se asemeja a la sintaxis de las firmas de índice con un for .. in dentro. Hay tres partes:

  1. La variable de tipo K, que se vincula a cada propiedad a su vez.
  2. La unión literal de cadena Keys, que contiene los nombres de las propiedades sobre las que iterar.
  3. El tipo de propiedad resultante.

En este sencillo ejemplo, Keys es una lista codificada de nombres de propiedad y el tipo de propiedad es siempre boolean, por lo que este tipo mapeado es equivalente a escribir:

type Flags = {
  option1: boolean;
  option2: boolean;
};

Las aplicaciones reales, sin embargo, parecen Readonly o Partial encima. Se basan en algún tipo existente y transforman las propiedades de alguna manera. Ahí es donde keyof y los tipos de acceso indexados vienen en:

type NullablePerson = { [P in keyof Person]: Person[P] | null };
type PartialPerson = { [P in keyof Person]?: Person[P] };

Pero es más útil tener una versión general.

type Nullable<T> = { [P in keyof T]: T[P] | null };
type Partial<T> = { [P in keyof T]?: T[P] };

En estos ejemplos, la lista de propiedades es keyof T y el tipo resultante es una variante de T[P]. Esta es una buena plantilla para cualquier uso general de tipos mapeados. Eso es porque este tipo de transformación es homomórfico, lo que significa que el mapeo se aplica solo a las propiedades de T y no otros. El compilador sabe que puede copiar todos los modificadores de propiedad existentes antes de agregar nuevos. Por ejemplo, si Person.name fue de solo lectura, Partial<Person>.name sería de solo lectura y opcional.

Aquí hay un ejemplo más, en el que T[P] está envuelto en un Proxy<T> clase:

type Proxy<T> = {
  get(): T;
  set(value: T): void;
};
type Proxify<T> = {
  [P in keyof T]: Proxy<T[P]>;
};
function proxify<T>(o: T): Proxify<T> {
  // ... wrap proxies ...
}
let props = { rooms: 4 };
let proxyProps = proxify(props);

Tenga en cuenta que Readonly<T> y Partial<T> son tan útiles que se incluyen en la biblioteca estándar de TypeScript junto con Pick y Record:

type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

type Record<K extends keyof any, T> = {
  [P in K]: T;
};

Readonly, Partial y Pick son homomórficos mientras que Record no es. Una pista que Record no es homomórfico es que no se necesita un tipo de entrada para copiar propiedades desde:

type ThreeStringProps = Record<"prop1" | "prop2" | "prop3", string>;

Los tipos no homomórficos esencialmente están creando nuevas propiedades, por lo que no pueden copiar modificadores de propiedad desde cualquier lugar.

Tenga en cuenta que keyof any representa el tipo de cualquier valor que se puede utilizar como índice de un objeto. En otras palabras, keyof any es actualmente igual a string | number | symbol.

Inferencia de tipos mapeados

Ahora que sabe cómo ajustar las propiedades de un tipo, lo siguiente que querrá hacer es desenvolverlas. Afortunadamente, eso es bastante fácil:

function unproxify<T>(t: Proxify<T>): T {
  let result = {} as T;
  for (const k in t) {
    result[k] = t[k].get();
  }
  return result;
}
let originalProps = unproxify(proxyProps);

Tenga en cuenta que esta inferencia de desenvolvimiento solo funciona en tipos mapeados homomórficos. Si el tipo mapeado no es homomórfico, tendrá que dar un parámetro de tipo explícito a su función de desenvolvimiento.

Tipos condicionales

Un tipo condicional selecciona uno de dos tipos posibles en función de una condición expresada como una prueba de relación de tipos:

T extends U ? X : Y

El tipo anterior significa cuando T es asignable a U el tipo es X, de lo contrario el tipo es Y.

Un tipo condicional T extends U ? X : Y es cualquiera resuelto para X o Y, o diferido porque la condición depende de una o más variables de tipo. Cuando T o U contiene variables de tipo, ya sea para resolver X o Y, o diferir, se determina por si el sistema de tipos tiene suficiente información para concluir que T siempre es asignable a U.

Como ejemplo de algunos tipos que se resuelven de inmediato, podemos echar un vistazo al siguiente ejemplo:

declare function f<T extends boolean>(x: T): T extends true ? string : number;
// Type is 'string | number'
let x = f(Math.random() < 0.5);

Otro ejemplo sería el TypeName type alias, que utiliza tipos condicionales anidados:

type TypeName<T> = T extends string
  ? "string"
  : T extends number
  ? "number"
  : T extends boolean
  ? "boolean"
  : T extends undefined
  ? "undefined"
  : T extends Function
  ? "function"
  : "object";
type T0 = TypeName<string>;
type T1 = TypeName<"a">;
type T2 = TypeName<true>;
type T3 = TypeName<() => void>;
type T4 = TypeName<string[]>;

Pero como ejemplo de un lugar donde los tipos condicionales se aplazan, donde se quedan en lugar de elegir una rama, sería el siguiente:

interface Foo {
  propA: boolean;
  propB: boolean;
}
declare function f<T>(x: T): T extends Foo ? string : number;
function foo<U>(x: U) {
  // Has type 'U extends Foo ? string : number'
  let a = f(x);
  // This assignment is allowed though!
  let b: string | number = a;
}

En lo anterior, la variable a tiene un tipo condicional que aún no ha elegido una rama. Cuando otro fragmento de código termina llamando foo, sustituirá en U con algún otro tipo, y TypeScript volverá a evaluar el tipo condicional, decidiendo si realmente puede elegir una rama.

Mientras tanto, podemos asignar un tipo condicional a cualquier otro tipo de objetivo siempre que cada rama del condicional sea asignable a ese objetivo. Entonces, en nuestro ejemplo anterior, pudimos asignar U extends Foo ? string : number para string | number ya que no importa a qué se evalúe el condicional, se sabe que es string o number.

Tipos condicionales distributivos

Los tipos condicionales en los que el tipo marcado es un parámetro de tipo desnudo se denominan tipos condicionales distributivos. Los tipos condicionales distributivos se distribuyen automáticamente entre los tipos de unión durante la instanciación. Por ejemplo, una instanciación de T extends U ? X : Y con el argumento de tipo A | B | C por T se resuelve como (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y).

Ejemplo

type T5 = TypeName<string | (() => void)>;
type T6 = TypeName<string | string[] | undefined>;
type T7 = TypeName<string[] | number[]>;

En instanciaciones de un tipo condicional distributivo T extends U ? X : Y, referencia a T dentro del tipo condicional se resuelven en constituyentes individuales del tipo de unión (es decir, T se refiere a los componentes individuales después el tipo condicional se distribuye sobre el tipo de unión). Además, las referencias a T dentro de X tener una restricción de parámetro de tipo adicional U (es decir T se considera asignable a U dentro de X).

Ejemplo

type BoxedValue<T> = { value: T };
type BoxedArray<T> = { array: T[] };
type Boxed<T> = T extends any[] ? BoxedArray<T[number]> : BoxedValue<T>;
type T1 = Boxed<string>;
type T2 = Boxed<number[]>;
type T3 = Boxed<string | number[]>;

Darse cuenta de T tiene la restricción adicional any[] dentro de la verdadera rama de Boxed<T> y por lo tanto es posible referirse al tipo de elemento de la matriz como T[number]. Además, observe cómo se distribuye el tipo condicional sobre el tipo de unión en el último ejemplo.

La propiedad distributiva de los tipos condicionales se puede utilizar convenientemente para filtrar tipos de unión:

// Remove types from T that are assignable to U
type Diff<T, U> = T extends U ? never : T;
// Remove types from T that are not assignable to U
type Filter<T, U> = T extends U ? T : never;
type T1 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">;
type T2 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c"
type T3 = Diff<string | number | (() => void), Function>; // string | number
type T4 = Filter<string | number | (() => void), Function>; // () => void
// Remove null and undefined from T
type NotNullable<T> = Diff<T, null | undefined>;
type T5 = NotNullable<string | number | undefined>;
type T6 = NotNullable<string | string[] | null | undefined>;
function f1<T>(x: T, y: NotNullable<T>) {
  x = y;
  y = x;
}
function f2<T extends string | undefined>(x: T, y: NotNullable<T>) {
  x = y;
  y = x;
  let s1: string = x;
  let s2: string = y;
}

Los tipos condicionales son particularmente útiles cuando se combinan con tipos mapeados:

type FunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? K : never;
}[keyof T];
type FunctionProperties<T> = Pick<T, FunctionPropertyNames<T>>;
type NonFunctionPropertyNames<T> = {
  [K in keyof T]: T[K] extends Function ? never : K;
}[keyof T];
type NonFunctionProperties<T> = Pick<T, NonFunctionPropertyNames<T>>;
interface Part {
  id: number;
  name: string;
  subparts: Part[];
  updatePart(newName: string): void;
}
type T1 = FunctionPropertyNames<Part>;
type T2 = NonFunctionPropertyNames<Part>;
type T3 = FunctionProperties<Part>;
type T4 = NonFunctionProperties<Part>;

Tenga en cuenta que los tipos condicionales no pueden hacer referencia a sí mismos de forma recursiva. Por ejemplo, lo siguiente es un error.

Ejemplo

type ElementType<T> = T extends any[] ? ElementType<T[number]> : T; // Error

Inferencia de tipos en tipos condicionales

Dentro de extends cláusula de un tipo condicional, ahora es posible tener infer declaraciones que introducen una variable de tipo a inferir. Se puede hacer referencia a tales variables de tipo inferido en la rama verdadera del tipo condicional. Es posible tener múltiples infer ubicaciones para el mismo tipo de variable.

Por ejemplo, lo siguiente extrae el tipo de retorno de un tipo de función:

type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;

Los tipos condicionales se pueden anidar para formar una secuencia de coincidencias de patrones que se evalúan en orden:

type Unpacked<T> = T extends (infer U)[]
  ? U
  : T extends (...args: any[]) => infer U
  ? U
  : T extends Promise<infer U>
  ? U
  : T;
type T0 = Unpacked<string>;
type T1 = Unpacked<string[]>;
type T2 = Unpacked<() => string>;
type T3 = Unpacked<Promise<string>>;
type T4 = Unpacked<Promise<string>[]>;
type T5 = Unpacked<Unpacked<Promise<string>[]>>;

El siguiente ejemplo demuestra cómo varios candidatos para el mismo tipo de variable en posiciones covariantes hacen que se infiera un tipo de unión:

type Foo<T> = T extends { a: infer U; b: infer U } ? U : never;
type T1 = Foo<{ a: string; b: string }>;
type T2 = Foo<{ a: string; b: number }>;

Del mismo modo, múltiples candidatos para el mismo tipo de variable en posiciones contravariantes hacen que se infiera un tipo de intersección:

type Bar<T> = T extends { a: (x: infer U) => void; b: (x: infer U) => void }
  ? U
  : never;
type T1 = Bar<{ a: (x: string) => void; b: (x: string) => void }>;
type T2 = Bar<{ a: (x: string) => void; b: (x: number) => void }>;

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

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

No es posible utilizar infer declaraciones en cláusulas de restricción para parámetros de tipo regular:

type ReturnedType<T extends (...args: any[]) => infer R> = R;

Sin embargo, se puede obtener el mismo efecto borrando las variables de tipo en la restricción y, en su lugar, especificando un tipo condicional:

type AnyFunction = (...args: any[]) => any;
type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R
  ? R
  : any;

Tipos condicionales predefinidos

TypeScript agrega varios tipos condicionales predefinidos, puede encontrar la lista completa y ejemplos en Tipos de utilidad.