Sistema de tipo avanzado
Introducción
Este capítulo explica conceptos avanzados sobre el sistema de tipos. Las características presentadas en este capítulo permitirán que sus programas alcancen un alto nivel de tipado, lo que reducirá en gran medida el riesgo de errores durante su ejecución.
Alias de tipo
Los alias de tipo permiten definir tipos reutilizables o cambiar el nombre de los tipos nativos.
Sintaxis:
type alias = type;
Ejemplo:
type Salary = number;
type Employee = {
firstName: string;
lastName: string;
salary: Salary;
};
const increaseSalary = (percent: number, employee: Employee) => {
const increase = employee.salary * (percent / 100);
return employee.salary + increase;
};
const evelyn: Employee = {
firstName: "Evelyn",
lastName: "Miller",
salary: 2000
};
const newSalary = increaseSalary(2, evelyn);
// Log: 2040
console.log(newSalary);
Los alias de tipo permiten describir estructuras y tienen mucho en común con las interfaces. La mayoría de las funciones relacionadas con el tipado de una interfaz se puede usar con un alias de tipo: propiedad opcional, propiedad de solo lectura, genérica...
Ejemplo:
type Employee = {
firstname: string;
lastname: string;
readonly salary: number;
};
type ListType<T> = Array<T>; ...
Tipo never
El tipo never representa un tipo que podría describirse como imposible y que nunca debe suceder. Utilizado en la firma de una función, indica que no puede devolver un resultado. Esto sucede en caso de que la función siempre arroje un error o si contiene un bucle infinito.
Ejemplo:
function throwError(message: string): never {
throw new Error(message);
}
function worker(): never {
while (true) {}
}
A diferencia del tipo void (consulte el capítulo Tipos e instrucciones básicas), el compilador TypeScript no siempre deduce correctamente el tipo never. A veces es necesario especificarlo explícitamente.
El tipo never también se utiliza para escribir variables. Una variable de tipo never no acepta ningún tipo, pero todos los tipos la aceptan. Esto significa que es imposible asignar un valor a una variable de tipo never, pero sí se puede asignar a una variable de otro tipo.
Ejemplo:
let never: never;
let any: any;
let unknown: unknown;
// Compilation error TS2322: Type 'any' is
// not assignable to type 'any'.
const a: never = any;
// Compilation error TS2322: Type 'any' is
// not assignable to type 'unknown'.
const b: never...
Tipo unión e intersección
1. Unión
En TypeScript, es posible expresar uniones de tipos utilizando el operador «|». Se interpreta como un operador «O» («OR» en inglés) y permite indicar que una variable puede ser de typeA o de typeB o de typeN.
Sintaxis:
let/const variable: typeA | typeB | ... ;
El compilador de TypeScript solo permite el acceso a propiedades comunes a todos los tipos especificados en la unión.
Ejemplo (Visual Studio Code):

Sin embargo, es posible restringir una unión a un tipo contenido en ella, en cuyo caso hablamos de discriminación.
Ejemplo:
interface Manager {
salary: number;
}
interface Salesman {
salary: number;
bonus: number;
}
function getSalary(person: Manager | Salesman) {
if ((person as Salesman).bonus) {
return person.salary + (person as Salesman).bonus;
}
return person.salary;
}
En este ejemplo, el parámetro person está restringido al tipo Salesman mediante una aserción de tipo. Esto permite el acceso a propiedades específicas de la interfaz Salesman para aplicar un comportamiento específico. Hay otra forma de realizar...
Type guards
1. Introducción
El compilador solo proporciona acceso a las propiedades comunes de una unión de tipos. Para obtener información más precisa sobre la naturaleza de los tipos contenidos en la unión, es necesario utilizar la discriminación. Para lograr esto, es imperativo utilizar una condición (bloque if...else, ternario o bloque switch). Estas condiciones discriminatorias se denominan type guards («guardias de tipo» en español).
Como recordatorio, los tipos definidos en TypeScript solo son útiles en la compilación y no existen en el código JavaScript generado. Los type guards también se utilizan para afirmar el tipo de una variable durante la ejecución de un programa. Hay varios operadores que permiten crear un type guard nativo: typeof, instanceof e in.
También es posible utilizar una propiedad de tipo literal (o un modelo de tipo literal; consulte la sección Modelo de tipo literal) que será común a los tipos que componen la unión y se utilizará como propiedad discriminante (consulte la sección Unión discriminante).
Finalmente, existe una última posibilidad que le permite crear sus propias type guards utilizando funciones llamadas user defined type guard functions (funciones de protección de tipo definidas por el usuario). En estas funciones, corresponde al usuario definir cómo discriminar un tipo y devolver un booleano de confirmación (consulte la sección Type guards definidas por el usuario).
2. Operador typeof
Este operador unario devuelve en forma de cadena de caracteres el tipo del operando que le sigue. Este tipo es el que determina el intérprete de JavaScript durante la ejecución del programa (y no el tipo estático utilizado en TypeScript).
Ejemplo:
const personn = {
firstName: "Evelyn",
lastName: "Miller"
};
const age = 34;
// Log : object
console.log(typeof person);
// Log : string
console.log(typeof person.firstName);
// Log : number
console.log(typeof age);
En el contexto de los type guards, el operador typeof se puede utilizar para discriminar entre tipos primitivos (string, number, boolean, undefined, null, symbol y bigint) y tipos por referencia (object y function).
Ejemplo:
function getSalary(salary: number...
Tipos literales
Los tipos literales permiten restringir los valores posibles para un parámetro o variable. Hay tres familias de tipos literales: cadenas de caracteres, numéricos y booleanos.
Ejemplo:
let firstName: "Evelyn" = "Evelyn";
// Compilation Error TS2322: Type 'Miller' is
// not assignable to type 'Evelyn'.
firstName = "Miller";
let age: 34 = 34;
// Compilation Error TS2322: Type '35' is
// not assignable to type '34'.
age = 35;
let isCeo: true = true;
// Compilation Error TS2322: Type 'false' is
// not assignable to type 'true'.
isCeo = false;
Este ejemplo ayuda a comprender mejor la unicidad intrínseca de un tipo literal. De hecho, los tipos literales representan subconjuntos lógicos de otros tipos (number para el tipo 34, string para el tipo "Evelyn" y boolean para el tipo false). Esta unicidad no excluye las características que se aplican al tipo de referencia. Es posible, por ejemplo, utilizar todos los métodos string en el tipo "Evelyn".
Ejemplo (Visual Studio Code):

Como se ha visto anteriormente en este capítulo, los tipos literales se pueden utilizar como propiedad discriminante (consulte la sección...
Modelo de tipo literal
Los modelos de tipos literales (Template Literal Types en inglés) se basan en los que hemos visto anteriormente, en combinación con la sintaxis de interpolación de cadenas de caracteres. Permiten utilizar una o más expresiones de tipo o alias de tipo previamente definidos dentro de otro tipo.
Ejemplo:
type employeeName = "Evelyn";
// type checkEmployee = "Check employee: Evelyn"
type checkEmployee = `Check employee: ${employeeName}`;
Se pueden utilizar para escribir la firma de una función.
Ejemplo:
function greetingEmployee(
firstname: string,
lastname: string
): `Welcome ${string} ${string}` {
return `Welcome ${firstname} ${lastname}`;
}
Se pueden utilizar en combinación con otras funciones de TypeScript relacionadas con los tipos.
Ejemplo 1 (Union):
type employeeName = "Evelyn" | "Patrick";
// type checkEmployee = "Check employee: Evelyn" |
// "Check employee: Patrick"
type checkEmployee = `Check employee: ${employeeName}`;
Ejemplo...
Tipo index
Cuando se define un tipo de objeto en TypeScript, sus propiedades deben declararse explícitamente. Una propiedad no declarada en el tipo no se puede agregar de forma dinámica más adelante.
Ejemplo:
interface Employee {
name: string;
}
// Compilation error TS2322:
// Type '{ name: string; salary: number; }'
// is not assignable to type 'Employee'.
// Object literal may only specify known properties,
// and 'salary' does not exist in type 'Employee'.
const employee1: Employee = {
name: "Evelyn",
salary: 10000
};
El tipo index permite tipar el índice de un objeto.
Sintaxis:
interface InterfaceName {
[key: type] : type;
}
type TypeName = {
[key: type] : type;
}
class ClassName = {
[key: type] : type;
}
Este tipo permite agregar propiedades arbitrarias a un objeto y restringir su tipo.
Ejemplo:
class Employee {
constructor(public readonly name: string) { }
}
class Manager {
constructor(public readonly title: string) { }
}
interface EmployeeList {
[key: string]: Employee; // Index Signature
}
const list: EmployeeList = {};
list["evelyn"] = new Employee("Evelyn");
list.evelyn = new Employee("Evelyn");
// Compilation...
Mapped type
1. Antes de TypeScript 2.1
Los mapped types (tipos mapeados) son tipos que toman un genérico como parámetro (consulte el capítulo Genericidad) tomando un tipo objeto como parámetro. Permiten iterar sobre las propiedades de un tipo y modificarlas, creando así un nuevo tipo. Para comprender completamente su interés, he aquí un primer ejemplo escrito en TypeScript 2.0, antes de la existencia de los mapped types. Este ejemplo utiliza el método Object.freeze definido en el archivo de declaración base de TypeScript (lib.d.ts).
Ejemplo (TypeScript 2.0):
freeze<T>(o: T): T;
El método Object.freeze() evita cualquier modificación de un objeto. Ya no será posible modificar, añadir o eliminar las propiedades de este objeto.
Ejemplo (TypeScript 2.0):
interface Employee {
name: string;
salary: number;
}
interface ReadonlyEmployee {
readonly name: string;
readonly salary: number;
}
function freezeEmployee(p: Employee): ReadonlyEmployee {
return Object.freeze(p);
}
const employee: Employee = {
name: "Evelyn",
salary: 10000
};
const immutableEmployee1 = Object.freeze(employee);
immutableEmployee1.name = "John";
const immutableEmployee2 = freezeEmployee(employee);
// Compilation error TS2540:
// Cannot assign to 'name' because
// it is a read-only property.
immutableEmployee2.name = "John";
Antes de TypeScript 2.1, el archivo lib.d.ts definía el método Object.freeze para devolver el tipo T del objeto que se le pasaba como parámetro. Como muestra el ejemplo anterior, el objeto employeeImmutable1 no es, por tanto, inmutable. El cambio de su propiedad name provocará un error al ejecutar el programa, pero ningún error al compilar.
Para corregir esto, era imperativo:
-
Agregar una interfaz que describiera el mismo objeto, pero que tuviera todas sus propiedades en readonly (ReadonlyEmployee en el ejemplo).
-
Agregar una función freezeEmployee() que solo...
Afirmación constante
La afirmación constante permite hacer que una variable sea inmutable. Solo se puede utilizar con variables de tipo primitivo, arreglos u objetos literales. No tiene los mismos efectos dependiendo de si es una variable de tipo primitivo o un objeto. Al igual que la afirmación de tipo, tiene dos notaciones.
Sintaxis 1:
let/const variable = value as const;
Sintaxis 2:
let/const variable = <const> value;
En TypeScript, se puede declarar una variable usando la palabra clave const o let. Hay una diferencia notable a nivel de tipado.
Ejemplo (let - Visual Studio Code):

Ejemplo (const - Visual Studio Code):

Cuando se utiliza la palabra clave const, el tipo se vuelve literal, lo que impide que se reasigne. Es posible lograr el mismo resultado con la palabra clave let para que la variable no sea modificable mediante una aserción constante.
Ejemplo (Visual Studio Code):

En el caso de objetos, la afirmación constante aplicará la palabra clave readonly a todas las propiedades del objeto.
Ejemplo:
let employee = {
name: "Evelyn",
salary: 10000,
supplies: ["laptop", "bag"]
} as const;
// Compilation error TS2540:
// Cannot assign to 'name' because
// it is a read-only property.
employee.name = "John";
...
Tuplas variádicas
Las tuplas ya se han presentado en el capítulo Tipos e instrucciones básicas. Pero hay una particularidad, presente desde TypeScript 4.0, que aún no se ha abordado: las tuplas variádicas. Antes de la aparición de esta funcionalidad, el uso del operador rest (consulte el capítulo Tipos e instrucciones básicas) en tuplas no permitía un tipado eficiente.
Ejemplo:
function concat(tuple1, tuple2) {
return [...tuple1, ...tuple2];
}
En el ejemplo anterior, se define una función para concatenar dos tuplas. Sin tuplas variádicas, la única forma de escribir esta función es mediante sobrecargas.
Ejemplo:
function concat(tuple1: [], tuple2: []): [];
function concat<A>(tuple1: [A], tuple2: []): [A];
function concat<A, B>(tuple1: [A, B], tuple2: []): [A, B];
function concat<A, B, C>(tuple1: [A, B, C], tuple2: []): [A, B, C];
function concat<A2>(tuple1: [], tuple2: [A2]): [A2];
function concat<A1, A2>(tuple1: [A1], tuple2: [A2]): [A1, A2];
function concat<A1, B1, A2>(tuple1: [A1, B1], tuple2: [A2]): [A1, B1, A2];
//...
Cada sobrecarga generará un posible escenario. La primera sobrecarga tiene en cuenta el caso en el que ambas tuplas están vacías; la segunda...
Tipo condicional
1. Conceptos básicos
Los tipos condicionales permiten ir más allá de los mapped types en la manipulación de tipos. También son tipos genéricos que toman un tipo T como parámetro. La particularidad de los tipos condicionales es que utilizan una condición dentro de su declaración. La condición probará la compatibilidad entre el tipo T y un tipo elegido; luego, el tipo condicional devolverá un nuevo tipo según el resultado.
Sintaxis:
T extends U ? A : B
Ejemplo (Visual Studio Code):

Ejemplo (Visual Studio Code):

En este ejemplo, la expresión T extends Employee significa que el tipo T se puede asignar al tipo Employee. Por lo tanto, el operador ternario se puede traducir de la siguiente manera: si el tipo T es un subconjunto de Employee, entonces se devuelve el tipo «yes»; en caso contrario, se devuelve «no».
Ejemplo (Visual Studio Code):

En este nuevo ejemplo, Salesman se puede asignar a Employee porque contiene las propiedades name y salary. Por lo tanto, la condición devuelve el tipo «yes».
Un tipo condicional puede hacer referencia a sí mismo y, por tanto, ser recursivo. Esta técnica debe usarse con moderación porque puede resultar costosa rápidamente en el tiempo de compilación. Se introdujo con TypeScript 4.1 en particular para permitir la expresión de ciertos tipos con una profundidad costosa, como el tipo de utilidad Awaited definido en el archivo lib.d.ts.
2. Distributividad
Un tipo T pasado como argumento a un tipo condicional puede ser una unión. Si este tipo T se usa en el operando izquierdo de la palabra clave extends, entonces la condición se distribuye entre todos los tipos de unión.
Ejemplo:
interface Employee {
name: string;
salary: number;
}
interface Manager {
name: string;
salary: number;
team: Employee[];
}
interface CEO {
name: string;
salary: number;
team:...
Operador satisfies
A veces sucede que se desea conservar la inferencia del compilador al usar un objeto literal y verificar que se ajuste a un tipo. Cuando este tipo contiene una unión, el compilador de TypeScript tendrá en cuenta posteriormente solo los elementos comunes a la unión (consulte la sección Tipo unión e intersección - Unión). Luego, el compilador utilizará el tipo explícito en lugar de la inferencia para validar el código.
En tal caso, ya no es posible utilizar un elemento que podría haberse determinado mediante inferencia de tipos.
Ejemplo:
interface Manager {
salary: number;
}
interface Salesman {
salary: number;
bonus: number;
}
type Employee = Manager | Salesman;
type Team = {
employees: Employee[],
};
function getSalary(salary: number, bonus?: number) {
return bonus ? salary + bonus : salary;
}
const team: Team = {
employees: [{
salary: 2000,
bonus: 10
}, {
salary: 2000, ...