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:
- el tipo de función
prototype
propiedad si su tipo no esany
- 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:
- La variable de tipo
K
, que se vincula a cada propiedad a su vez. - La unión literal de cadena
Keys
, que contiene los nombres de las propiedades sobre las que iterar. - 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.