Tabla de contenidos
- Tipos de intersección
- Tipos de Unión
- Guardias tipo y tipos diferenciadores
- Tipos anulables
- Parámetros y propiedades opcionales
- Type guards y type assertions
- Nombres alternativos de especies (de tipo alias)
- Tipos de literales de cadena
- Tipos de literales numéricos
- Tipos de miembros de enum
- Uniones Discriminados
- Tipos de polimorfos this
- Tipos de índice
- Deducción de tipos de esquema
- Tipos condicionales
- Recursos del Artículo
Tipos de intersección
Un tipo de intersección combina varios tipos en uno solo. Esto le permite combinar tipos existentes en un tipo que tiene todas las funciones que necesita. Por ejemplo, un tipo es del tipo Person & Serializable & Loggable
, y el tipo Person
están todos agrupados en el mismo tipo. Esto significa que un objeto de este tipo contendrá los tres tipos de elementos Serializable
.
Los tipos de intersección a menudo se usan en mixins y otros principios que pueden ser ajenos a la programación normal orientada a objetos (y a menudo se encuentran en JavaScript). Aquí hay un ejemplo que muestra cómo crear una mezcla:
function extend<T, U>(first: T, second: U): T & U { let result = <T & U>{}; for (let id in first) { (<any>result)[id] = (<any>first)[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (<any>result)[id] = (<any>second)[id]; } } return result; } class Person { constructor(public name: string) { } } interface Loggable { log(): void; } class ConsoleLogger implements Loggable { log() { // ... } } var jim = extend(new Person("Jim"), new ConsoleLogger()); var n = jim.name; jim.log();
Tipos de Unión
Los tipos de unión están relacionados con los tipos de intersección, pero funcionan mediante un mecanismo completamente diferente. A veces, una función en una biblioteca espera un parámetro que puede ser de tipo number
o de tipo string
. Por ejemplo, vea la siguiente función que toma una cadena de texto y le agrega un relleno a la izquierda.Si el valor del parámetro padding
es una cadena, su valor se agregará a la izquierda del valor value
, pero si el valor del padding
es un número, este valor es el número de espacios a la izquierda:
/** * Toma una cadena y agrega "paddingo" a la izquierda. * Si 'padding' es una cadena, entonces 'padding' se añade al lado izquierdo. * Si 'padding' es un número, entonces ese número de espacios se agrega al lado izquierdo. */ function padLeft(value: string, padding: any) { 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}'.`); } // Ejemplo de uso de la función padLeft("Hello world", 4); // "Hello world"
El problema aquí es que el tipo de operando padding
es any
. Esto significa que podemos llamar a la función padLeft
con un argumento de tipo diferente a ambos tipos number
y string
, pero no ocurrirá ningún error durante la compilación:
let indentedString = padLeft("Hello world", true); // El error se pasará al compilador pero fallará durante la ejecución
En el código de programación normal orientado a objetos, los dos tipos se pueden abstraer creando una jerarquía de tipos. Aunque la solución será más obvia, es una exageración y no la necesitamos. La ventaja de la versión original es que recibe directamente las primitivas. Su uso fue breve y sencillo. Además, esta solución no ayudará si queremos usar una función padLeft que ya existe en otro lugar. En lugar de usar tipo any
, podemos usar un tipo de unión para el operando padding
:
/** * Toma una cadena y agrega "relleno" a la izquierda. * Si 'relleno' es una cadena, entonces 'relleno' se añade al lado izquierdo. * Si 'relleno' es un número, entonces ese número de espacios se agrega al lado izquierdo. */ function padLeft(value: string, padding: string | number) { // ... } let indentedString = padLeft("Hello world", true); // arroja un error durante la compilación
El tipo de unión describe un valor que puede ser de muchos tipos. Usamos un carácter para |
separar cada tipo, por lo que un tipo number | string | boolean
es un tipo de valor que puede ser un número, una cadena o un valor booleano. Si hay un valor de un tipo de unión, podemos acceder solo a los elementos que son comunes a todos los tipos en la unión:
interface Bird { fly(); layEggs(); } interface Fish { swim(); layEggs(); } function getSmallPet(): Fish | Bird { // ... } let pet = getSmallPet(); pet.layEggs(); // Permisible pet.swim(); // Error
Los tipos de unión en este caso pueden ser difíciles de entender, pero solo necesitan un poco de práctica para acostumbrarse. Si un valor tiene tipo A | B
, sólo sabemos con certeza que contiene los elementos en ambos A
y B
al mismo tiempo. En este ejemplo, el tipo Bird
tiene un elemento llamado fly
. No podemos estar seguros de que una variable de tipo Bird | Fish
tenga una función llamada fly
. Si la variable es de tipo Fish
durante la ejecución, la llamada a pet.fly()
.
Guardias tipo y tipos diferenciadores
Los tipos de unión son útiles en los casos en que los valores pueden ser de más de un tipo. Pero, ¿qué pasa con los momentos en que necesitamos saber si el tipo de un valor es exactamente Fish? Verificar si un elemento particular existe en un objeto es una forma común de distinguir entre dos valores que pueden diferir. Como ya se mencionó, solo podemos acceder a los elementos que sabemos con certeza que están presentes en todos los tipos que están en el tipo de unión.
let pet = getSmallPet(); // Todos los intentos de acceder a las siguientes propiedades fallarán if (pet.swim) { pet.swim(); } else if (pet.fly) { pet.fly(); }
Para arreglar el código y hacer que funcione, necesitaremos usar una aserción de tipo como esta:
let pet = getSmallPet(); if ((<Fish>pet).swim) { (<Fish>pet).swim(); } else { (<Bird>pet).fly(); }
Guard de tipos definidos por el programador
Observe en el ejemplo anterior que tuvimos que usar pruebas de tipo muchas veces. Sería bueno si pudiéramos averiguar el tipo de pet
_ automáticamente después de verificarlo sin tener que volver a hacer la misma prueba de tipo.
TypeScript tiene una característica llamada protección de tipo. Una protección de tipos es una expresión que realiza una verificación en tiempo de ejecución que asegura que el tipo es inmutable en un campo determinado (es decir, sabremos que es solo de un tipo en el alcance y que el tipo no cambiará mientras estemos en el mismo campo). Para definir una protección de tipo, solo necesitamos definir una función cuyo tipo de retorno sea un predicado de tipo booleano:
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined; }
La sentencia pet is Fish
es de tipo booleano en este ejemplo. La instrucción booleana tiene el formato parameterName is Type
, donde el nombre del parámetro parameterName
debe ser de la firma de la función del tipo actual.
Cada vez que se llama a la función con una isFish
variable , TypeScript limitará esa variable a su tipo, el tipo dado si el tipo original es compatible.
// ambos // 'swin' y 'fly' if (isFish(pet)) { pet.swim(); } else { pet.fly(); }
Tenga en cuenta que TypeScript infirió que un tipo pet
es el tipo Bird
en un campo else
, porque el lenguaje entiende que un tipo pet
es el tipo Fish
en un campo if
, por lo que si el pet no es del tipo Fish
, entonces debe ser el tipo Bird
.
Guard usando typeof
Volvamos a la versión padLeft
que utiliza la unión de tipos. Podemos reescribirlo en una expresión de tipo booleano 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}'.`); }
Pero aun así, definir una función solo para ver si un tipo de valor es un tipo inicial que complica las cosas. Afortunadamente, no necesita abstraer la condición typeof x === "number"
en una función privada porque TypeScript la reconocerá como un tipo de protección por sí mismo. Lo que significa que podemos verificar los tipos en la misma línea de la siguiente manera:
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}'.`); }
Los protectores de especies typeof
se forman 2: typeof v === "typename"
y typeof v !== "typename"
; por lo que el valor "typename"
debe ser "number"
, o ,"string"
. Aunque TypeScript no lo detendrá si intenta comparar con otra cadena, el lenguaje no reconocerá la expresión como protección de tipo "boolean""symbol"
.
Guard usando instanceof
Si comprende el párrafo anterior y está familiarizado con el operador instanceof
en JavaScript, lo más probable es que tenga una idea general sobre este párrafo. La protección de tipos mediante el modificador instanceof
es un método de enumeración de tipos por parte del constructor del type. Por ejemplo, volvamos al ejemplo de host de nota al pie anterior:
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(" "); } // El tipo es // 'SpaceRepeatingPadder | StringPadder' let padder: Padder = getRandomPadder(); if (padder instanceof SpaceRepeatingPadder) { // Restringir el tipo a // 'SpaceRepeatingPadder' padder; } if (padder instanceof StringPadder) { // Restringir el tipo a // 'StringPadder' padder; }
El lado izquierdo del operador instanceof
debe ser un constructor y TypeScript limitará el tipo a:
- El tipo de
prototype
si no es tipoany
. - Construir firmas de un tipo de especie (construct signatures).
El orden es importante aquí.
Tipos anulables
Hay dos tipos distintos en TypeScript, null
y undefined
, sus valores están en el mismo orden (es decir, son dos tipos y dos valores al mismo tiempo). Ya mencionamos los tipos básicos que el verificador de tipos es y asignable a cualquier tipo. Esto significa que y son válidos para todos los tipos. Esto significa que el proceso de mapeo no se puede detener para ningún tipo, incluso si quisieras. Tony Hoare, el inventor del valor, llama a este problema el error nullundefined del billón de dólares.
La opción del compilador --strictNullChecks
tiene una solución a este problema: cuando se declara una variable, no incluirá automáticamente null
, y undefined
. Pero puede agregarlos explícitamente usando un tipo de unión como este:
let s = "foo"; // Falso, no se puede asignar // 'null' al tipo 'string' s = null; let sn: string | null = "bar"; sn = null; // Permisible // Falso, no se puede establecer // 'undefined' para tipo // 'string | null' sn = undefined;
Tenga en cuenta que TypeScript trata null
y undefined
de manera diferente para que coincida con el funcionamiento de JavaScript. El tipo string | null
es lo contrario de tipo string | undefined
y tipo string | undefined | null
.
Parámetros y propiedades opcionales
Al usar la opción --strictNullChecks
, los parámetros opcionales se agregan automáticamente | undefined
:
function f(x: number, y?: number) { return x + (y || 0); } f(1, 2); f(1); f(1, undefined); // Falso, tipo 'null' // no asignable a tipo // 'number | undefined' f(1, null);
Lo mismo es cierto para las propiedades opcionales:
class C { a: number; b?: number; } let c = new C(); c.a = 12; // Error, no se puede establecer el tipo // 'undefined' // El valor por defecto / 'number' c.a = undefined; c.b = 13; c.b = undefined; // Permisible // Error, no se puede establecer el tipo // 'null' por tipo // 'number | undefined' c.b = null;
Type guards y type assertions
Debido a que los tipos que aceptan que aceptan tipos de valores NULL se implementan en una unión de tipos, deberá usar una protección de tipo para deshacerse del null
. Este es el mismo código que escribirías en JavaScript:
function f(sn: string | null): string { if (sn == null) { return "default"; } else { return sn; } }
Deshacerse del null
, en ejemplo anterior es bastante sencillo, pero también podría usar el operador ||
:
function f(sn: string | null): string { return sn || "default"; }
En los casos en que el compilador no pueda deshacerse de null
o undefined
, puede usar el operador de tipo prueba para eliminarlo manualmente. Agregando el carácter !
. Por ejemplo: identifier!
eliminar null
y undefined
de tipo identifier
:
function broken(name: string | null): string { function postfix(epithet: string) { // mal, podría ser 'name' // de una especie de null return name.charAt(0) + '. the ' + epithet; } name = name || "Bob"; return postfix("great"); } function fixed(name: string | null): string { function postfix(epithet: string) { return name!.charAt(0) + '. the ' + epithet; // Permisible } name = name || "Bob"; return postfix("great"); }
El ejemplo usa una función anidada aquí porque el compilador no puede omitir el tipo null
dentro de una función anidada (excepto las funciones que se llaman directamente [función invocada inmediatamente]). Esto se debe a que el compilador no puede realizar un seguimiento de todas las llamadas a funciones anidadas, especialmente si las devuelve desde la función externa. Debido a que el compilador no sabe dónde se llamará a la función, no es posible saber el tipo de _ name
en momento en que se ejecuta el cuerpo de la función.
Nombres alternativos de especies (de tipo alias)
Los nombres de tipos alternativos crean un nuevo nombre para un tipo. Los nombres de tipos alternativos a veces son similares a las interfaces, excepto que pueden nombrar tipos primitivos, tipos de unión, tuplas y cualquier otro tipo que necesite escribir a mano:
type Name = string; type NameResolver = () => string; type NameOrResolver = Name | NameResolver; function getName(n: NameOrResolver): Name { if (typeof n === "string") { return n; } else { return n(); } }
Nombrar tipos con nombres alternativos en realidad no crea un nuevo tipo, solo crea un nuevo nombre para referirse a ese tipo. Dar un tipo primitivo con un nombre alternativo no es útil, aunque podría ser una forma de documentación. Al igual que las interfaces, las alias también pueden ser genéricos, solo necesitamos agregar y usar parámetros de tipo en el lado derecho de la declaración del alias:
type Container<T> = { value: T };
Un sustantivo alternativo también puede referirse a sí mismo en un atributo:
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>; }
Y al combinarlo con tipos de intersección, podemos obtener tipos más complejos:
type LinkedList<T> = T & { next: LinkedList<T> }; interface Person { name: string; } var people: LinkedList<Person>; var s = people.name; var s = people.next.name; var s = people.next.next.name; var s = people.next.next.next.name;
Pero un nombre alternativo no puede aparecer en ningún otro lugar del lado derecho de la declaración:
type Yikes = Array<Yikes>; // Error
Interfaces frente a nombres de tipos alternativos
Como ya se mencionó, los nombres de tipos alternativos pueden funcionar de manera similar a las interfaces. Pero hay algunas pequeñas diferencias. Una diferencia es que las interfaces crean un nuevo nombre que se usa en todas partes.
Los nombres de tipos alternativos no crean un nuevo nombre, por ejemplo, los mensajes de error no usarán el alias. Si pasa el cursor sobre la función interfaced
en código siguiente en el editor de código, el editor mostrará que devolverá un objeto de tipo Interface
, pero en el caso del objeto aliased
devuelto será un tipo de objeto literal.
type Alias = { num: number } interface Interface { num: number; } declare function aliased(arg: Alias): Alias; declare function interfaced(arg: Interface): Interface;
Otra diferencia importante es que los nombres de tipos alternativos no pueden extenderse ni implementarse de un tipo (no pueden extenderse ni implementar otro tipo). Debido a que una de las propiedades útiles del software que es extensible, siempre es mejor usar interfaces en lugar de nombres de tipos alternativos si es posible. Pero si no puede expresar una forma con una interfaz y necesita una tupla o un tipo de unión, los nombres de tipos alternativos son una buena solución.
Tipos de literales de cadena
Los tipos de literales de cadena le permiten especificar el valor exacto que debe contener una cadena. En la práctica, los tipos de literales de cadena se combinan bien con los tipos de unión, las protecciones de tipos y los nombres de tipos alternativos. Puede usar estas funciones para obtener cadenas que se comporten como enumeraciones.
type Easing = "ease-in" | "ease-out" | "ease-in-out"; class UIElement { animate(dx: number, dy: number, easing: Easing) { if (easing === "ease-in") { // ... } else if (easing === "ease-out") { } else if (easing === "ease-in-out") { } else { // Falso, no pasar // nulo o indefinido } } } let button = new UIElement(); button.animate(0, 0, "ease-in"); // Error, el tipo no está permitido // "uneasy" button.animate(0, 0, "uneasy");
Puede pasar cualquiera de las tres cadenas permitidas, pero cualquier otra cadena generará un error:
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
Los tipos de literales de cadena se pueden usar de la misma manera para distinguir entre sobrecargas:
function createElement(tagName: "img"): HTMLImageElement; function createElement(tagName: "input"): HTMLInputElement; // ... más sobrecargas ... function createElement(tagName: string): Element { // ... cuerpo de la función ... }
Tipos de literales numéricos
Los tipos escalares literales también se pueden usar en TypeScript:
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 { // ... }
Estos tipos numéricos rara vez se escriben directamente, pero pueden identificar errores:
function foo(x: number) { if (x !== 1 || x !== 2) { // ~~~~~~~ // El operador '!==' // no se puede aplicar a ambos tipos // '1' y '2' } }
En otras palabras, la variable x
debe contener el valor 1
cuando se compara con el valor 2
, por lo que la validación realiza una comparación no válida.
Tipos de miembros de enum
Se mencionó en la página de constantes polimórficas que los elementos poli constantes tienen tipos cuando cada elemento se inicializa con un valor literal. En la mayoría de los casos, cuando hablamos de tipos singleton, nos referimos tanto a los tipos de elementos constantes múltiples como a los tipos escalares y literales, pero la mayoría de los usuarios se refieren a los tipos singleton y literales con el mismo significado.
Uniones Discriminados
Los tipos impares, los tipos de unión, los tipos de protectores y los nombres de tipos alternativos se pueden combinar para crear un patrón avanzado llamado uniones discontinuas, también conocidas como uniones etiquetadas o tipos de datos algebraicos. Las asociaciones discretas son útiles en la programación funcional. Algunos idiomas convierten las uniones en uniones discontinuas automáticamente. TypeScript se basa en patrones de JavaScript existentes. Hay tres componentes:
- Especies que tienen una propiedad de tipo singular común: el discriminante.
- Un nombre de tipo alternativo que toma la unión de estos tipos: unión.
- Tipos de guardias en la propiedad común.
interface Square { kind: "square"; size: number; } interface Rectangle { kind: "rectangle"; width: number; height: number; } interface Circle { kind: "circle"; radius: number; }
Primero le informaremos sobre las interfaces que se agregarán. Cada interfaz tiene una propiedad llamada kind
, cada propiedad tiene un tipo diferente de literal de cadena. El atributo kind
aquí se llama discriminante o etiqueta. Las demás funciones son específicas de cada interfaz. Tenga en cuenta que las interfaces no están actualmente enhebradas. Pongamoslos en una unión:
type Shape = Square | Rectangle | Circle;
Ahora usemos una unión discreta:
function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
Comprobación de exhaustividad
Podemos equivocarnos y olvidarnos de verificar una forma de unión discontinua, lo que significa que la validación no será completa. Queremos que el traductor le diga cuándo no verificamos todas las uniones intermitentes. Por ejemplo, si agregamos el tipo Triangle
a Shape
y area
también :
type Shape = Square | Rectangle | Circle | Triangle; function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } // should error here - we didn't handle case "triangle" // debería arrojar un error aquí porque no manejamos el caso // "triangle" }
Hay dos maneras de hacerlo. El primero es habilitar la opción --strictNullChecks
y especificar tipo de retorno:
// Error, la función regresa // number | undefined function area(s: Shape): number { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } }
Debido a que la sintaxis switch
ya no es inclusiva, TypeScript sabe que la función puede devolver el valor undefined
. Si el tipo de devolución se establece number
explícitamente, obtendrá un error que indica que el tipo de devolución es number | undefined
, no del tipo number
. Sin embargo, este método es algo ambiguo y la opción --strictNullChecks
no siempre funciona con el código heredado.
El segundo método utiliza never
el utilizado por el compilador para verificar la exhaustividad:
function assertNever(x: never): never { throw new Error("Unexpected object: " + x); } function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; default: return assertNever(s); // aquí se arroja un error si faltan estados } }
Aquí la función verifica assertNever
si s
es de tipo never
( el tipo que permanece después de que se eliminan todas las demás instancias). Si olvida un estado, s
será de tipo real y obtendrá un error. Este método requiere la definición de una función adicional, pero este método es más sencillo que el anterior.
Tipos de polimorfos this
Un tipo polimórfico es this
un subtipo de una clase contenedora o interfaz. Esto se llama polimorfismo limitado F-bounded polymorphism. Esto permite que las interfaces fluidas jerárquicas se expresen más fácilmente, por ejemplo. Tomemos una calculadora simple que regresa this
después de cada aritmética:
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; } // ... aquí se pueden agregar otras operaciones ... } let v = new BasicCalculator(2) .multiply(5) .add(1) .currentValue();
Debido a que la clase usa tipos this
, puede extenderla y la nueva clase podrá 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; } // ... aquí se pueden agregar otras operaciones ... } let v = new ScientificCalculator(2) .multiply(5) .sin() .add(1) .currentValue();
Si los tipos no se usarán this
aquí, entonces la clase ScientificCalculator
no podría extender la clase BasicCalculator
mantener y la interfaz simple. El subordinado devuelve multiply
la clase BasicCalculator
que no tiene el subordinado sin
. Pero con tipos this
, la función multiply
devuelve this
, que en este caso es de tipo ScientificCalculator
.
Tipos de índice
El uso de tipos de índice puede hacer que el compilador verifique qué código está usando nombres de propiedades dinámicas. Por ejemplo, elegir un conjunto de propiedades de un objeto dado de patrones comunes de Javascript:
function pluck(o, names) { return names.map(n => o[n]); }
A continuación se explica cómo escribir y usar esta función en TypeScript, mediante consulta de tipo de índice y acceso a índice.
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] { return names.map(n => o[n]); } interface Person { name: string; age: number; } let person: Person = { name: 'Jarid', age: 35 }; let strings: string[] = pluck(person, ['name']); // permitido, string[]
El compilador verifica que ya exista name
una propiedad en el archivo Person
. El ejemplo proporciona algunos operadores de nuevos tipos. Primero keyof T
, que es un operador de consulta de tipo índice. Para cada tipo T
, keyof T
es la unión de los nombres de las propiedades generales conocidas en el tipo T
, por ejemplo:
let personProps: keyof Person; // 'name' | 'age'
Se puede keyof Person
intercambiar 'name'|'age'
porque tienen el mismo valor. La diferencia aparece si agregamos otra propiedad a Person
, por ejemplo si agregamos address: string
, entonces el valor de keyof Person
actualizará automáticamente para convertirse en 'name'|'age'|'address'
. Puede usarlo keyof
en contextos globales como hicimos con la función pluck
, es decir, en casos en los que aún no puede conocer los nombres de las propiedades. Esto significa que el compilador verificará que el conjunto de nombres de propiedad pasados a la función sea pluck
un conjunto válido:
// Falso, valor // 'unknown' // no existe en unión // 'name' | 'age' pluck(person, ['age', 'unknown']);
El segundo operador es operator T[K]
, que es el operador de acceso al índice. Aquí, la sintaxis de tipo refleja la sintaxis de expresión. Esto significa que person['name']
de tipo Person['name']
(es decir string
en este ejemplo). Pero puede usar T[K]
(como en una consulta de tipo de índice), en un contexto global, lo que realmente muestra el poder de este operador. Solo debe asegurarse de que la variable de tipo K
expanda la unión de forma keyof T
, K extends keyof T
. Aquí hay otro ejemplo de una función llamada getProperty
:
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] { return o[name]; // o[name] is of type T[K] }
En la función getProperty
tenemos o: T
(el objeto) y name: K
( el nombre de la propiedad deseada), lo que significa o[name]: T[K]
. Una vez que se devuelva el resultado T[K]
, compilador inicializará el tipo real de la clave, por lo que el tipo de retorno de la función getProperty
variará según la propiedad que requiera.
let name: string = getProperty(person, 'name'); let age: number = getProperty(person, 'age'); // Falso, valor 'unknown' // no existe en unión 'name' | 'age' let unknown = getProperty(person, 'unknown');
Tipos de índice y firmas de índice de cadena
Operador keyof
y T[K]
con firmas de índice de texto. Si tiene un tipo con una firma de índice de texto, el tipo keyof T
es string
simplemente el tipo T[string]
y es el tipo de firma de índice:
interface Map<T> { [key: string]: T; } let keys: keyof Map<number>; // string let value: Map<number>['foo']; // number
Tipos asignados
Convertir cada propiedad de un tipo en opcional es una práctica común en la programación:
interface PersonPartial { name?: string; age?: number; }
O podemos necesitar una versión de solo lectura:
interface PersonReadonly { readonly name: string; readonly age: number; }
Esto es tan común en Javascript que TypeScript proporciona una forma de crear nuevos tipos basados en tipos antiguos, y esta función se denomina tipos planificados. En un tipo de esquema, el nuevo tipo transforma todas las propiedades del tipo anterior de la misma manera. Por ejemplo, todos los atributos de un tipo particular se pueden convertir en atributos opcionales o readonly
. Aquí hay unos ejemplos:
type Readonly<T> = { readonly [P in keyof T]: T[P]; } type Partial<T> = { [P in keyof T]?: T[P]; }
Y para usarlo:
type PersonPartial = Partial<Person>; type ReadonlyPerson = Readonly<Person>;
Veamos el tipo rayado más simple y sus partes:
type Keys = 'option1' | 'option2'; type Flags = { [K in Keys]: boolean };
La sintaxis aquí es similar a la de las firmas de índice con la cláusula for .. in
dentro de la firma. Estas son las tres partes:
- Una variable de tipo
K
, que está vinculada a cada uno de los atributos a la vez. Keys
que son las claves de unión de los literales string, que tienen los nombres de propiedad que se repiten en ellos.- El tipo resultante de la propiedad.
En este ejemplo simple, escribimos las claves Keys
explícitamente como una lista de nombres de propiedad y el tipo de propiedad siempre es type boolean
, por lo que este tipo asignado es equivalente a:
type Flags = { option1: boolean; option2: boolean; }
Pero las aplicaciones reales se parecen a Readonly
las Partial
anteriores. Es decir, se basa en un tipo preexistente y transforma las propiedades de cierta manera. Aquí podemos explotar keyof
y acceder a tipos a través de índices:
type NullablePerson = { [P in keyof Person]: Person[P] | null } type PartialPerson = { [P in keyof Person]?: Person[P] }
Y una versión genérica es más útil por ser reutilizable con más de un tipo:
type Nullable<T> = { [P in keyof T]: T[P] | null } type Partial<T> = { [P in keyof T]?: T[P] }
En estos dos ejemplos, la lista de propiedades es keyof T
y el tipo es una versión de forma diferente del T[P]
. Esta es una buena plantilla para cualquier uso general de tipos de esquema. Esto se debe a que este tipo es homomórfico, lo que significa que el diseño se aplica solo de T
. El compilador sabe que puede copiar todos los modificadores de propiedades antes de agregar otros nuevos. Por ejemplo, si una propiedad Person.name
es de solo lectura y convertimos una propiedad opcional usando una propiedad en Person
, la propiedad será de solo lectura y opcional al mismo tiempo PartialPartial<Person>.name
.
Aquí hay un ejemplo final, aquí rodeamos una T[P]
clase Proxy<T>
:
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> { // ... el cuerpo de la función ... } let proxyProps = proxify(props);
Tenga en cuenta que Readonly<T>
y Partial<T>
son muy útiles, por lo que se encuentran 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 string, T> = { [P in K]: T; }
Readonly
, Partial
y Pick
son simétricas, pero no lo son Record
. Esto indica que no Record
es simétrico en el sentido de que no necesita un tipo de entrada para copiar propiedades desde:
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
Los tipos que no son simétricos crean nuevas propiedades, por lo que no pueden copiar modificadores de propiedad de ningún otro lugar.
Deducción de tipos de esquema
Ahora que sabemos cómo encerrar las propiedades de un tipo en particular, el siguiente paso es descomprimirlo para devolverlo a lo que era antes. Y eso es 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 la inferencia de tipos al desacoplarlos solo funciona en tipos de rayas simétricas. Si su tipo de esquema es asimétrico, deberá agregar un parámetro de tipo explícito a su decodificador delimitador.
Tipos condicionales
TypeScript 2.8 agregó una función de tipo condicional que nos permite expresar asociaciones de tipos no uniformes. El tipo condicional elige uno de dos tipos posibles de acuerdo con una condición que se expresa probando una relación entre dos tipos de la siguiente manera:
T extends U ? X : Y
El tipo anterior significa: si el tipo T
se puede asignar al tipo U
, entonces el tipo especificado final será el tipo X
, pero si no se cumple la condición, el tipo es Y
. El tipo condicional T extends U ? X : Y
se decide a X
o Y
, se pospone la decisión si la condición depende de una o más variables de tipo. Si ambos T
o U
contienen variables de tipo, la decisión final de tipo de ser X
o Y
diferir está determinada por si el sistema de tipos tiene suficiente información para inferir que el tipo T
es asignable al tipo U
en todos los casos (es decir, la condición debe ser permanente). Ejemplo de tipos que se deciden inmediatamente:
declare function f<T extends boolean>(x: T): T extends true ? string : number; // El tipo especificado es // string | number let x = f(Math.random() < 0.5)
Nombre de tipo alternativo TypeName
otro ejemplo, esta vez estamos usando 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>; // "string" type T1 = TypeName<"a">; // "string" type T2 = TypeName<true>; // "boolean" type T3 = TypeName<() => void>; // "function" type T4 = TypeName<string[]>; // "object"
Ejemplo de caso en el que se aplaza la determinación de tipos condicionales (y en el que los tipos condicionales quedan sin determinación de un tipo concreto):
interface Foo { propA: boolean; propB: boolean; } declare function f<T>(x: T): T extends Foo ? string : number; function foo<U>(x: U) { // El tipo final es el tipo condicional // 'U extends Foo ? string : number' let a = f(x); // pero esta asignación está permitida let b: string | number = a; }
Si se llama a la función foo
, el tipo U
será reemplazado por otro y TypeScript volverá a estimar el tipo condicional y decidirá su tipo final. Pero aún podemos asignar un tipo condicional a cualquier tipo de destino incluso si la determinación del tipo se aplaza siempre que cada rama de la condición se pueda asignar a ese tipo de destino. Entonces pudimos asignar el tipo al U extends Foo ? string : number
tipo string | number
en nuestro ejemplo anterior, porque sabemos que la condición terminará en el tipo string
o el tipo 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. Son tipos que se distribuyen automáticamente a los tipos de unión tras la inicialización. Por ejemplo, inicializar el tipo T extends U ? X : Y
pasando el tipo A | B | C
como el valor del parámetro T
se distribuye en el formulario (A extends U ? X : Y) | (B extends U ? X : Y) | (C extends U ? X : Y)
.
Ejemplo:
type T10 = TypeName<string | (() => void)>; // "string" | "function" type T12 = TypeName<string | string[] | undefined>; // "string" | "object" | "undefined" type T11 = TypeName<string[] | number[]>; // "object"
Al inicializar el tipo condicional distributivo T extends U ? X : Y
, referencias al tipo T
dentro del tipo condicional se deciden a los componentes individuales del tipo de unión (es decir, el tipo T
se refiere a los componentes individuales después de que el tipo condicional se distribuye sobre el tipo de unión). Además, las referencias a tipo T
dentro tipo X
tienen una restricción de parámetro de tipo adicional, el parámetro de tipo U
( tipo T
es asignable a tipo 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 T20 = Boxed<string>; // BoxedValue<string>; type T21 = Boxed<number[]>; // BoxedArray<number>; type T22 = Boxed<string | number[]>; // BoxedValue<string> | BoxedArray<number>;
Tenga en cuenta que el tipo T
tiene la restricción adicional any[]
en la rama entera del tipo, Boxed<T>
por lo que es posible indicar el tipo de los elementos de la matriz con la expresión T[number]
. Observe también cómo se distribuye el tipo condicional sobre el tipo de unión en la última línea.
La función de análisis se puede utilizar en tipos condicionales para filtrar tipos de unión:
// Remove types from T that are assignable to U // Eliminar tipos de tipo // T // Asignables al tipo // type Diff<T, U> = T extends U ? never : T; // Remove types from T that are not assignable to U // Eliminar tipos de tipo T // que no se pueden asignar al tipo // Filtro de tipo U type Filter<T, U> = T extends U ? T : never; type T30 = Diff<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type T31 = Filter<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c" type T32 = Diff<string | number | (() => void), Function>; // string | number type T33 = Filter<string | number | (() => void), Function>; // () => void // Remove null and undefined from T // Eliminar ambos tipos null y undefined de tipo T type NonNullable<T> = Diff<T, null | undefined>; type T34 = NonNullable<string | number | undefined>; // string | number type T35 = NonNullable<string | string[] | null | undefined>; // string | string[] function f1<T>(x: T, y: NonNullable<T>) { x = y; // Permitido y = x; // Error } function f2<T extends string | undefined>(x: T, y: NonNullable<T>) { x = y; // Permitido y = x; // Error let s1: string = x; // Error let s2: string = y; // Permitido }
Los tipos condicionales son muy útiles cuando se mezclan con tipos rayados:
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 T40 = FunctionPropertyNames<Part>; // "updatePart" type T41 = NonFunctionPropertyNames<Part>; // "id" | "name" | "subparts" type T42 = FunctionProperties<Part>; // { updatePart(newName: string): void } type T43 = NonFunctionProperties<Part>; // { id: number, name: string, subparts: Part[] }
No se puede hacer referencia recursivamente a los tipos condicionales como es el caso de los tipos de unión y los tipos de intersección. El siguiente ejemplo es incorrecto:
type ElementType<T> = T extends any[] ? ElementType<T[number]> : T; // Error
Inferir tipos en tipos condicionales
Las declaraciones se pueden colocar infer
dentro de extends
cláusula de tipo condicional. Las declaraciones infer
se utilizan para inferir una variable de un tipo dado. Las variables de tipos inferidos se pueden referenciar en la rama entera del tipo condicional. También es posible usar declaraciones infer
en múltiples lugares del mismo tipo de variable. Por ejemplo, el siguiente tipo extrae el tipo de retorno de un tipo de función:
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
Aquí el tipo se infiere R
y automáticamente el tipo de retorno que se encuentra en el tipo de función dada. Por ejemplo, si la función devuelve una cadena, la variable de tipo R
se referirá al tipo string
. Los tipos condicionales se pueden anidar para formar una serie de coincidencias de patrones que se estiman 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>; // string type T1 = Unpacked<string[]>; // string type T2 = Unpacked<() => string>; // string type T3 = Unpacked<Promise<string>>; // string type T4 = Unpacked<Promise<string>[]>; // Promise<string> type T5 = Unpacked<Unpacked<Promise<string>[]>>; // string
El siguiente ejemplo muestra cómo deducir el tipo de una unión si hay varios candidatos de la misma variable de tipo en lugares covariantes:
type Foo<T> = T extends { a: infer U, b: infer U } ? U : never; type T10 = Foo<{ a: string, b: string }>; // string type T11 = Foo<{ a: string, b: number }>; // string | number
De manera similar, varios tipos de candidatos del mismo tipo de variable en lugares contravariantes conducen a una inferencia de tipo de intersección:
type Bar<T> = T extends { a: (x: infer U) => void, b: (x: infer U) => void } ? U : never; type T20 = Bar<{ a: (x: string) => void, b: (x: string) => void }>; // string type T21 = Bar<{ a: (x: string) => void, b: (x: number) => void }>; // string & number
Cuando se deduce de un tipo con varias firmas de llamada (por ejemplo, un tipo de función sobrecargado), la inferencia se toma de la última firma (que se supone que es la firma más permisiva). No es posible informar sobre la resolución de sobrecarga en función de una lista de tipos de parámetros.
declare function foo(x: string): number; declare function foo(x: number): string; declare function foo(x: string | number): string | number; type T30 = ReturnType<typeof foo>; // string | number
No puede usar cláusulas infer
de restricción para parámetros de tipos regulares:
type ReturnType<T extends (...args: any[]) => infer R> = R; // Error, operación imposible
Pero se puede obtener el mismo efecto al omitir las variables de tipo en la restricción y usar un tipo condicional en su lugar:
type AnyFunction = (...args: any[]) => any; type ReturnType<T extends AnyFunction> = T extends (...args: any[]) => infer R ? R : any;
Tipos condicionales predefinidos
TypeScript 2.8 agregó varios tipos condicionales a su biblioteca estándar lib.d.ts
:
Exclude<T, U>
:Excluir tipoT
del tipoU
.Extract<T, U>
: Extraer los tiposT
de typeU
.NonNullable<T>
: excluirnull
yundefined
de tipoT
.ReturnType<T>
: Obtiene el tipo de retorno de una función.InstanceType<T>
:
Obtiene el tipo de instancia de un tipo de función constructora.
Ejemplos:
type T00 = Exclude<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "b" | "d" type T01 = Extract<"a" | "b" | "c" | "d", "a" | "c" | "f">; // "a" | "c" type T02 = Exclude<string | number | (() => void), Function>; // string | number type T03 = Extract<string | number | (() => void), Function>; // () => void type T04 = NonNullable<string | number | undefined>; // string | number type T05 = NonNullable<(() => string) | string[] | null | undefined>; // (() => string) | string[] function f1(s: string) { return { a: 1, b: s }; } class C { x = 0; y = 0; } type T10 = ReturnType<() => string>; // string type T11 = ReturnType<(s: string) => void>; // void type T12 = ReturnType<(<T>() => T)>; // {} type T13 = ReturnType<(<T extends U, U extends number[]>() => T)>; // number[] type T14 = ReturnType<typeof f1>; // { a: number, b: string } type T15 = ReturnType<any>; // any type T16 = ReturnType<never>; // any type T17 = ReturnType<string>; // Error type T18 = ReturnType<Function>; // Error type T20 = InstanceType<typeof C>; // C type T21 = InstanceType<any>; // any type T22 = InstanceType<never>; // any type T23 = InstanceType<string>; // Error type T24 = InstanceType<Function>; // Error
Note que Exclude es lo mismo que el tipo Diff que ya conocíamos, llamado Exclude para evitar problemas que puedan ocurrir en código donde el tipo Diff está predefinido.
Recursos del Artículo
- Múltiples Constantes en TypeScript
- Iteradores y generadores en TypeScript
- Símbolo en TypeScript (Symbol)
- Tipos Avanzados en TypeScript
- Tipos de Compatibilidad en TypeScript
- Inferir Tipos en TypeScript
- Tipos Generalizados (Generics) en TypeScript
- Tipos Básicos de Datos en TypeScript
- Interfaces en TypeScript
- Declaración de Variables en TypeScript
- Funciones en TypeScript
- Categorías en TypeScript
- Introducción a TypeScript