Primeros Pasos
Hola Mundo y Compilación
Los archivos TypeScript usan la extensión .ts. El compilador tsc transpila TS a JS, borrando todas las anotaciones de tipo en tiempo de ejecución. Usa --strict para máxima type safety. ts-node o bun pueden ejecutar archivos .ts directamente sin un paso de compilación separado.
// hello.ts
const message: string = "Hello, TypeScript!";
console.log(message);
// Compile to JavaScript (type annotations erased):
// tsc hello.ts -> hello.js
// tsc --strict hello.ts // enable all strict checks
// tsc --watch hello.ts // recompile on change
// Run directly with ts-node or bun:
// ts-node hello.tstsconfig.json
tsconfig.json configura el compilador de TypeScript. 'strict: true' habilita noImplicitAny, strictNullChecks, strictFunctionTypes y más. 'target' controla la versión JS de salida. 'esModuleInterop' habilita los imports por defecto de módulos CommonJS como los built-ins de Node.
{
"compilerOptions": {
"target": "ES2020", // JS version to emit
"module": "ESNext", // module system
"strict": true, // enable all strict checks
"outDir": "./dist", // output directory
"rootDir": "./src", // source root
"esModuleInterop": true, // allow default imports from CJS
"skipLibCheck": true, // skip .d.ts type checking
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}Anotaciones de Tipo e Inferencia
Las anotaciones de tipo especifican explícitamente el tipo de una variable. TypeScript también puede inferir tipos desde los valores. Usa anotaciones explícitas para firmas de función y APIs públicas; confía en la inferencia para variables locales. Evita 'any': opta por salir de la comprobación de tipos por completo.
// Explicit type annotations
let count: number = 0;
const name: string = "Alice";
let isDone: boolean = true;
let ids: number[] = [1, 2, 3];
let tuple: [string, number] = ["age", 30];
// Type inference (let TS figure it out)
let age = 30; // inferred as number
let items = [1, 2, 3]; // inferred as number[]
let mixed = [1, "a"]; // inferred as (string | number)[]
// any disables type checking (avoid!)
let anything: any = 42;
anything = "string"; // no errorComprobaciones de Modo Estricto
El modo estricto habilita comprobaciones críticas: strictNullChecks (null/undefined no asignables a otros tipos), noImplicitAny (los parámetros deben tener tipos), strictPropertyInitialization (los campos de clase deben inicializarse). Usa '!' (asignación definitiva) cuando estés seguro de que un campo se establecerá más tarde.
// strictNullChecks: null/undefined not assignable to other types
let n: string = null; // Error in strict mode
// noImplicitAny: must annotate parameters
function greet(name) { } // Error: implicit any
// strictPropertyInitialization
class User {
name: string; // Error: not initialized
constructor() {}
}
// Fix: initialize or use definite assignment
class Fixed {
name!: string; // definite assignment assertion
age: number = 0;
}Archivos de Declaración (.d.ts)
Los archivos de declaración (.d.ts) proporcionan tipos para librerías de JavaScript sin definiciones de TypeScript. 'declare' le dice al compilador que una variable/función existe en tiempo de ejecución. Usa paquetes @types de DefinitelyTyped para librerías populares (por ejemplo, @types/node, @types/react).
// types.d.ts - type declarations only, no implementation
declare module "my-lib" {
export function greet(name: string): string;
export const version: string;
}
// global.d.ts - extend global scope
declare global {
interface Window {
myApp: { version: string };
}
}
// Usage in .ts files:
// import { greet } from "my-lib"; // now typed
// Install @types packages: npm i -D @types/node @types/reactTipos Básicos
Primitivos y Tipos Especiales
Primitivos de TypeScript: string, number, boolean, bigint, symbol. 'void' indica que una función no devuelve valor. 'never' representa valores que nunca ocurren: funciones que lanzan o se ejecutan para siempre. Usa 'never' para comprobaciones exhaustivas en sentencias switch.
let str: string = "hello";
let num: number = 42;
let bool: boolean = true;
let big: bigint = 100n;
let sym: symbol = Symbol("id");
// void: function returns nothing
function log(msg: string): void { console.log(msg); }
// never: function never returns
function fail(msg: string): never { throw new Error(msg); }
function infinite(): never { while (true) {} }Arrays y Tuples
Los arrays usan la sintaxis T[] o Array<T>. ReadonlyArray previene mutaciones. Las tuples son arrays de longitud fija con tipos específicos en cada índice: útiles para pares clave-valor o datos tipo CSV. Las labeled tuples mejoran la legibilidad con posiciones con nombre.
// Two syntaxes for arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b"];
// ReadonlyArray (immutable)
const ro: ReadonlyArray<number> = [1, 2, 3];
// ro.push(4); // Error: not mutable
// Tuple (fixed length, known types)
let tuple: [string, number] = ["Alice", 30];
let name = tuple[0]; // string
let age = tuple[1]; // number
// Labeled tuple elements (TS 4.0+)
let entry: [name: string, age: number] = ["Bob", 25];Enums
Los enums definen un conjunto de constantes con nombre. Los string enums son recomendados para depuración (los valores son legibles en la salida). Los numeric enums admiten reverse mapping. 'const enum' se borra en tiempo de compilación (inlined) para coste cero en runtime. Prefiere union types para casos simples.
// Numeric enum (default starts at 0)
enum Direction { Up, Down, Left, Right }
let d: Direction = Direction.Up; // 0
// String enum (recommended for readability)
enum Status {
Pending = "PENDING",
Success = "SUCCESS",
Failed = "FAILED",
}
// Reverse mapping (numeric only)
console.log(Direction[0]); // "Up"
// Const enum (inlined, no runtime object)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;any vs unknown vs never
'any' deshabilita toda comprobación de tipos: evítalo. 'unknown' es la alternativa type-safe: debes acotarlo (vía typeof, instanceof) antes de usarlo. 'never' representa valores que nunca ocurren, usado para comprobación de exhaustividad en sentencias switch para captar cases faltantes en tiempo de compilación.
// any: opt out of type checking (dangerous!)
let a: any = 42;
a = "string"; // OK
a.toUpperCase(); // OK (no check, may fail at runtime)
// unknown: type-safe alternative to any
let u: unknown = 42;
// u.toUpperCase(); // Error: unknown type
if (typeof u === "string") {
u.toUpperCase(); // OK after narrowing
}
// never: impossible value (exhaustiveness check)
type Shape = "circle" | "square";
function area(s: Shape) {
switch (s) {
case "circle": return Math.PI;
case "square": return 1;
default:
const _exhaustive: never = s; // Error if case missing
}
}Type Assertions
Las type assertions le dicen al compilador 'confía en mí, conozco el tipo'. Usa la sintaxis 'as'. La non-null assertion (!) le dice a TS que un valor no es null/undefined. 'as const' hace todas las propiedades readonly literals: útil para objetos de configuración y tipos de acción de Redux. Las assertions no cambian el comportamiento en runtime.
// as syntax (preferred, works in .tsx)
let val: unknown = "hello";
let len: number = (val as string).length;
// Non-null assertion (!)
let el = document.querySelector("#app")!;
el.innerHTML = "Hi"; // el is HTMLElement, not null
// Double assertion (for unsafe casts)
let value = "42" as unknown as number;
// const assertion (literal types)
const req = { method: "GET", url: "/api" } as const;
// req.method has type "GET" (not string)
// req is readonlyTipos Literal y Union
Los tipos literal restringen un valor a un string, número o booleano específico. Combinados con unions, crean tipos precisos como dirección o métodos HTTP. Los template literal types (TS 4.1+) construyen tipos string a partir de otros tipos: potentes para generar claves type-safe y nombres de eventos.
// String literal types
let direction: "left" | "right" | "up" | "down";
direction = "left"; // OK
// direction = "sideways"; // Error
// Numeric literal types
let dice: 1 | 2 | 3 | 4 | 5 | 6;
dice = 4; // OK
// Boolean literal
let flag: true = true;
// Template literal types (TS 4.1+)
type Color = "red" | "blue";
type Size = "small" | "large";
type Variant = `${Size}-${Color}`;
// "small-red" | "small-blue" | "large-red" | "large-blue"Interfaces y Objetos
Fundamentos de Interfaces
Las interfaces describen la forma de los objetos. '?' marca propiedades opcionales (pueden ser undefined). 'readonly' previene la reasignación tras la inicialización. Las interfaces son solo de tiempo de compilación: se borran en el JavaScript de salida. Úsalas para definir contratos para objetos y clases.
interface User {
id: number;
name: string;
email?: string; // optional property
readonly createdAt: Date; // immutable
}
const user: User = {
id: 1,
name: "Alice",
createdAt: new Date(),
};
// user.createdAt = new Date(); // Error: readonly
// user.email; // string | undefinedIndex Signatures
Las index signatures permiten objetos con claves arbitrarias de un tipo dado. Todos los valores de propiedad deben ser asignables al tipo de índice. Útiles para diccionarios, cachés y datos dinámicos. Combina con propiedades conocidas para configs tipadas con opciones extra.
// Index signature: arbitrary string keys
interface StringMap {
[key: string]: string;
}
const dict: StringMap = {
name: "Alice",
city: "NYC",
// count: 42, // Error: value must be string
};
// Mixed: known + index signature
interface Config {
name: string;
[key: string]: string | number;
}
// Readonly index signature
interface ReadonlyMap {
readonly [key: string]: number;
}Extender Interfaces
Las interfaces pueden extender una o más interfaces, combinando sus miembros. Esto habilita composición y reutilización de código. A diferencia de las clases, las interfaces admiten herencia múltiple. Cuando implementas una interfaz, la clase debe proporcionar todos los miembros requeridos.
interface Animal {
name: string;
eat(): void;
}
interface Pet extends Animal {
owner: string;
play(): void;
}
interface Swimmer {
swim(): void;
}
// Multiple inheritance
interface Duck extends Pet, Swimmer {
quack(): void;
}
const duck: Duck = {
name: "Donald",
owner: "Walt",
eat() {},
play() {},
swim() {},
quack() {},
};Tipos de Función en Interfaces
Las interfaces pueden describir firmas de función, habilitando callbacks type-safe. Las interfaces híbridas (callable + propiedades) se usan para funciones estilo jQuery que también tienen métodos. Este patrón es común en librerías que devuelven funciones con helpers adjuntos.
interface SearchFn {
(source: string, keyword: string): boolean;
}
const contains: SearchFn = (src, kw) => src.includes(kw);
console.log(contains("hello world", "world")); // true
// Interface with mixed members (hybrid)
interface Counter {
(start: number): void; // callable
count: number; // property
reset(): void; // method
}
// Function with properties (jQuery-style)
const counter: any = (n: number) => { counter.count = n; };
counter.count = 0;
counter.reset = () => { counter.count = 0; };Interface vs Type Alias
Las interfaces admiten declaration merging (interfaces del mismo nombre se combinan), mejores mensajes de error y son preferidas para formas de objetos/clases. Los type aliases son más flexibles (pueden representar unions, primitivos, tuples) pero no pueden combinarse. Usa interfaces para APIs extensibles, type aliases para unions y tipos calculados.
// Interface: extendable, better error messages
interface Window { title: string; }
interface Window { size: number; } // declaration merging
const w: Window = { title: "App", size: 800 };
// Type alias: more flexible (unions, primitives, etc.)
type ID = string | number;
type Callback<T> = (value: T) => void;
// Both can describe object shapes
interface UserI { name: string; }
type UserT = { name: string; };
// Use interface for objects/classes, type for unions/aliasesOptional Chaining y Nullish Coalescing
Optional chaining (?.) accede de forma segura a propiedades anidadas: devuelve undefined en lugar de lanzar si cualquier enlace es null/undefined. Nullish coalescing (??) proporciona un default solo para null/undefined (no 0 o ''). Estos operadores reducen drásticamente el código verboso de comprobación de null.
interface User {
profile?: {
address?: {
city?: string;
};
};
}
const user: User = {};
// Optional chaining (?.) - safe property access
const city = user.profile?.address?.city; // string | undefined
// Nullish coalescing (??) - default value
const name = user.profile?.address?.city ?? "Unknown";
// Non-null assertion (!) - you're sure it's not null
// const c = user.profile!.address!.city!; // risky
// Optional method call
const result = user.profile?.address?.city?.toUpperCase();Type Aliases y Unions
Type Aliases
Los type aliases crean referencias con nombre a cualquier tipo, incluyendo unions, intersections, primitivos y genéricos. A diferencia de las interfaces, los aliases no pueden combinarse ni extenderse, pero son más flexibles. Usa aliases para unions, tuples y utility types; usa interfaces para formas de objetos.
// Basic alias
type ID = string | number;
type Point = { x: number; y: number };
// Generic alias
type Container<T> = { value: T };
// Function type alias
type Handler<T> = (event: T) => void;
// Usage
const id: ID = 42;
const p: Point = { x: 1, y: 2 };
const box: Container<string> = { value: "hi" };
const onClick: Handler<string> = (e) => console.log(e);Union Types
Los union types (A | B) permiten que un valor sea de uno de varios tipos. TypeScript acota el tipo dentro de bloques condicionales usando typeof, instanceof o comprobaciones in. Nota: (string | number)[] es diferente de string[] | number[]: el primero es un array mixto, el segundo es todo-strings O todo-números.
// Union: value can be one of several types
type ID = string | number;
function display(id: ID) {
if (typeof id === "string") {
console.log(id.toUpperCase()); // narrowed to string
} else {
console.log(id.toFixed(2)); // narrowed to number
}
}
display("abc"); // ABC
display(42); // 42.00
// Union of arrays vs array of unions
type Mixed = (string | number)[];
type Either = string[] | number[];Intersection Types
Los intersection types (A & B) combinan todos los miembros de múltiples tipos: el resultado debe satisfacer cada tipo. Útiles para mixins, composición y fusionar utility types. A diferencia de union (OR), intersection es AND: el valor debe tener todas las propiedades de todos los tipos.
// Intersection: combine multiple types into one
interface BusinessPartner {
name: string;
credit: number;
}
interface Identity {
id: number;
email: string;
}
type Employee = BusinessPartner & Identity;
const emp: Employee = {
name: "Alice",
credit: 1000,
id: 1,
email: "[email protected]",
};
// All properties required
// const bad: Employee = { name: "Bob" }; // Error: missing propsTipos Nullable
En modo estricto, null y undefined no son asignables a otros tipos: debes incluirlos explícitamente con unions (string | null). Los parámetros opcionales (param?) son implícitamente T | undefined. Usa ?? para defaults seguros y ! para afirmar non-null (usar con moderación).
// In strict mode, null/undefined are separate types
let name: string | null = null;
name = "Alice"; // OK
// Optional parameters are implicitly | undefined
function greet(name?: string) {
// name is string | undefined
console.log(name ?? "Guest");
}
// Return type can be null
function find(id: number): string | null {
return id === 1 ? "Alice" : null;
}
// Non-null assertion
const result = find(1)!.toUpperCase(); // "ALICE"Operadores Keyof y Typeof
'keyof T' extrae las claves del tipo T como un union de literales string. 'typeof x' extrae el tipo de un valor (útil para inferir desde objetos). 'keyof typeof obj' combina ambos para obtener las claves de un objeto existente: común en tipos de acción de Redux y accessors de propiedades type-safe.
interface User {
id: number;
name: string;
email: string;
}
// keyof: extract keys as a union
type UserKey = keyof User; // "id" | "name" | "email"
function getProp(obj: User, key: keyof User) {
return obj[key];
}
// typeof: extract type from a value
const config = { port: 3000, host: "localhost" };
type Config = typeof config; // { port: number; host: string }
// keyof typeof: keys of an object
type ConfigKey = keyof typeof config; // "port" | "host"Mapped Types
Los mapped types iteran sobre claves para transformar un tipo. Las utilidades integradas como Readonly, Partial y Pick son mapped types. Usa modificadores + y - para añadir/quitar readonly u opcional. Key remapping (TS 4.1+) renombra claves usando template literal types: potente para generar tipos getter/setter.
// Map over keys to create a new type
type Readonly<T> = {
readonly [K in keyof T]: T[K];
};
type Optional<T> = {
[K in keyof T]?: T[K];
};
interface User { id: number; name: string; }
type ReadonlyUser = Readonly<User>; // all readonly
type OptionalUser = Optional<User>; // all optional
// Modifiers: +add, -remove
type Mutable<T> = { -readonly [K in keyof T]: T[K]; };
type Required<T> = { [K in keyof T]-?: T[K]; };
// Key remapping (TS 4.1+)
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};Funciones
Tipos y Firmas de Función
TypeScript añade anotaciones de tipo a parámetros de función y valores de retorno. El tipo de retorno a menudo puede inferirse, pero la anotación explícita es recomendada para APIs públicas. Los parámetros por defecto hacen argumentos opcionales con un valor de respaldo. Usa void cuando una función no devuelve nada.
// Named function with types
function add(a: number, b: number): number {
return a + b;
}
// Arrow function with types
const multiply = (a: number, b: number): number => a * b;
// Function type alias
type MathOp = (a: number, b: number) => number;
const divide: MathOp = (a, b) => a / b;
// Void return (no return value)
function log(msg: string): void { console.log(msg); }
// Optional and default parameters
function greet(name: string, greeting: string = "Hi"): string {
return `${greeting}, ${name}!`;
}
greet("Alice"); // "Hi, Alice!"
greet("Bob", "Hello"); // "Hello, Bob!"Rest Parameters y Tuples
Los rest parameters (...args) recogen múltiples argumentos en un array. TypeScript los tipa como T[] o como tuple para funciones variádicas de longitud fija. El operador spread (...) hace lo inverso: expande un array en argumentos individuales. Los rest types de tuple habilitan firmas variádicas precisas.
// Rest parameters (variadic)
function sum(...nums: number[]): number {
return nums.reduce((a, b) => a + b, 0);
}
console.log(sum(1, 2, 3, 4)); // 10
// Tuple rest (fixed prefix + variadic tail)
function pair(name: string, ...scores: number[]): void {
console.log(name, scores);
}
// Spread call
const nums = [1, 2, 3];
console.log(sum(...nums));
// Typed rest as tuple
function f(...args: [string, number, boolean]): void {
const [s, n, b] = args;
}Function Overloads
Los function overloads proporcionan múltiples firmas de tipo para la misma función, habilitando tipos de retorno precisos basados en la entrada. La firma de implementación está oculta a los llamadores. Los overloads se resuelven top-down: pon las firmas más específicas primero. Común en librerías como jQuery y lodash.
// Overload signatures (what callers see)
function parse(input: string): string[];
function parse(input: number): number[];
// Implementation signature (not visible to callers)
function parse(input: string | number): string[] | number[] {
if (typeof input === "string") {
return input.split(",");
}
return [input, input * 2];
}
const strs = parse("a,b,c"); // string[]
const nums = parse(42); // number[]
// Overloads with different param counts
function makeDate(timestamp: number): Date;
function makeDate(y: number, m: number, d: number): Date;
function makeDate(yOrTs: number, m?: number, d?: number): Date {
return m === undefined
? new Date(yOrTs)
: new Date(yOrTs, m - 1, d);
}Tipo this
TypeScript permite declarar el tipo 'this' como primer parámetro. Esto asegura que la función se llame con el contexto correcto: útil para métodos pasados como callbacks. Las arrow functions capturan 'this' léxicamente, evitando la necesidad de .bind(). Usa 'noImplicitThis' para captar errores de 'this' sin tipar.
interface Card {
suit: string;
rank: string;
isFaceUp(): boolean;
}
// Explicit 'this' parameter
function format(this: Card): string {
return `${this.rank} of ${this.suit}`;
}
const card: Card = {
suit: "Hearts",
rank: "A",
isFaceUp() { return true; },
format,
};
// 'this' in callbacks with bind
class Handler {
private count = 0;
increment = () => { this.count++; }; // arrow binds this
}Callbacks y Funciones de Orden Superior
TypeScript tipa completamente las funciones de orden superior (funciones que toman o devuelven funciones). Usa parámetros de tipo genérico (T, U) para preservar relaciones de tipo entre entrada y salida. Los tipos de callback se definen comúnmente como type aliases para reutilización. El currying (devolver funciones) es completamente type-safe.
// Function as parameter
type Callback<T> = (value: T, index: number) => void;
function forEach<T>(arr: T[], cb: Callback<T>): void {
for (let i = 0; i < arr.length; i++) {
cb(arr[i], i);
}
}
forEach(["a", "b"], (v, i) => console.log(i, v));
// Function returning function (curry)
function add(a: number): (b: number) => number {
return (b) => a + b;
}
const add5 = add(5);
console.log(add5(3)); // 8
// Generic map
function map<T, U>(arr: T[], fn: (x: T) => U): U[] {
return arr.map(fn);
}Destructuring de Parámetros
TypeScript admite destructuring en parámetros de función: anota la forma desestructurada inline o vía una interfaz. Extraer a una interfaz mejora la legibilidad y reutilización. El destructuring de array/tuple también funciona. Este patrón es común en props de componentes React y handlers de API.
// Destructured parameters with types
function createUser({ name, age, email }: {
name: string;
age: number;
email?: string;
}): void {
console.log(name, age, email);
}
createUser({ name: "Alice", age: 30 });
// Extract to interface for reuse
interface UserOpts {
name: string;
age: number;
email?: string;
}
function updateUser({ name, age }: UserOpts): void {}
// Array destructuring in params
function swap([a, b]: [number, number]): [number, number] {
return [b, a];
}Clases y POO
Clase y Constructor
Las clases de TypeScript admiten parameter properties: prefijar parámetros de constructor con modificadores de acceso (public/private/protected/readonly) auto-crea y asigna campos. Esta abreviatura reduce boilerplate. Los métodos pueden tener anotaciones de tipo en valores de retorno. Los campos son public por defecto.
class Person {
// Parameter properties (shorthand)
constructor(
public name: string, // auto-creates this.name
private age: number, // private field
readonly id: number, // immutable
) {}
greet(): string {
return `Hi, I'm ${this.name}`;
}
}
const p = new Person("Alice", 30, 1);
console.log(p.name); // "Alice" (public)
// p.age; // Error: private
// p.id = 2; // Error: readonlyModificadores de Acceso
Modificadores de acceso: public (por defecto, en todas partes), private (solo clase), protected (clase + subclases), readonly (inmutable). El 'private' de TypeScript es solo de tiempo de compilación; los campos privados '#' de ES son verdaderamente privados en runtime. Usa private para detalles de implementación, protected para puntos de extensión.
class BankAccount {
public owner: string; // accessible everywhere
private balance: number; // class only
protected rate: number; // class + subclasses
readonly id: string; // immutable after init
#secret: string; // ES private (runtime)
constructor(owner: string) {
this.owner = owner;
this.balance = 0;
this.rate = 0.05;
this.id = crypto.randomUUID();
this.#secret = "hidden";
}
deposit(amount: number): void {
this.balance += amount;
}
}Herencia y Clases Abstractas
Las clases abstractas no pueden instanciarse directamente: definen una base para subclases. Los métodos abstractos no tienen implementación en la clase base; las subclases deben implementarlos. Usa 'extends' para herencia y 'super()' para llamar al constructor padre. Las clases abstractas habilitan polimorfismo: el código puede funcionar con cualquier subclase de Shape.
abstract class Shape {
constructor(public color: string) {}
abstract area(): number; // must be implemented
describe(): string {
return `${this.color} shape, area ${this.area()}`;
}
}
class Circle extends Shape {
constructor(color: string, private r: number) {
super(color);
}
area(): number { return Math.PI * this.r ** 2; }
}
class Square extends Shape {
constructor(color: string, private side: number) {
super(color);
}
area(): number { return this.side ** 2; }
}
const c = new Circle("red", 5);
console.log(c.describe()); // "red shape, area 78.54..."
// new Shape("blue"); // Error: cannot instantiate abstractInterfaces e Implements
Una clase puede implementar múltiples interfaces (separadas por comas). La clase debe proporcionar todos los miembros de la interfaz. A diferencia de extends (herencia simple), implements admite múltiples contratos. Es la forma de TypeScript de lograr comportamiento similar a herencia múltiple. Usa interfaces para definir contratos, clases para implementarlos.
interface Printable {
toString(): string;
}
interface Comparable<T> {
compareTo(other: T): number;
}
class Money implements Printable, Comparable<Money> {
constructor(private amount: number) {}
toString(): string {
return `$${this.amount.toFixed(2)}`;
}
compareTo(other: Money): number {
return this.amount - other.amount;
}
}
const a = new Money(10);
const b = new Money(20);
console.log(a.toString()); // "$10.00"
console.log(a.compareTo(b)); // -10Getters y Setters
Los getters y setters interceptan el acceso a propiedades para validación, cálculo o efectos secundarios. Usa un campo de respaldo privado (convención: prefijo guion bajo). Los getters habilitan propiedades calculadas (como fahrenheit desde celsius). Los setters habilitan validación. Accede a ellos como propiedades regulares: sin paréntesis.
class Temperature {
private _celsius: number = 0;
get celsius(): number {
return this._celsius;
}
set celsius(value: number) {
if (value < -273.15) throw new Error("Below absolute zero");
this._celsius = value;
}
get fahrenheit(): number {
return this._celsius * 9 / 5 + 32;
}
set fahrenheit(value: number) {
this.celsius = (value - 32) * 5 / 9;
}
}
const temp = new Temperature();
temp.celsius = 25;
console.log(temp.fahrenheit); // 77
// temp.celsius = -300; // throwsMiembros Estáticos y Singletons
Los miembros estáticos pertenecen a la clase, no a las instancias: accedidos vía ClassName.member. Usa static para constantes, funciones utilitarias y métodos factory. Un constructor privado + static getInstance() implementa el patrón Singleton. 'as const' hace los arrays estáticos readonly con tipos literales.
class Logger {
static instance: Logger;
private logs: string[] = [];
private constructor() {} // prevent direct new
static getInstance(): Logger {
if (!Logger.instance) {
Logger.instance = new Logger();
}
return Logger.instance;
}
static readonly LEVELS = ["INFO", "WARN", "ERROR"] as const;
log(msg: string): void {
this.logs.push(msg);
}
}
const logger = Logger.getInstance();
console.log(Logger.LEVELS); // ["INFO", "WARN", "ERROR"]
// new Logger(); // Error: private constructorGenéricos
Funciones Genéricas
Los genéricos (<T>) permiten escribir funciones que funcionan con cualquier tipo preservando la type safety. El parámetro de tipo T es un placeholder rellenado en tiempo de llamada: ya sea explícitamente (identity<number>) o inferido desde los argumentos. Los genéricos habilitan estructuras de datos y algoritmos reutilizables type-safe.
// Generic function: preserves type relationship
function identity<T>(value: T): T {
return value;
}
const n = identity<number>(42); // T = number, returns number
const s = identity("hi"); // T inferred as string
// Generic with multiple type params
function pair<A, B>(a: A, b: B): [A, B] {
return [a, b];
}
const p = pair("Alice", 30); // [string, number]
// Generic with array
function first<T>(arr: T[]): T | undefined {
return arr[0];
}Clases Genéricas
Las clases genéricas (<T>) crean contenedores type-safe. Cada instancia fija un tipo específico: un Stack<number> solo acepta números. Esto captura errores de tipo en tiempo de compilación sin sobrecarga en runtime (los genéricos se borran). Común en colecciones (Stack, Queue, Map) y wrappers reactivos (Observable<T>).
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
get size(): number {
return this.items.length;
}
}
const numStack = new Stack<number>();
numStack.push(1);
numStack.push(2);
console.log(numStack.pop()); // 2
const strStack = new Stack<string>();
strStack.push("hello");Restricciones Genéricas
Las restricciones (T extends SomeType) limitan qué tipos puede aceptar un genérico. 'T extends HasLength' asegura que T tiene una propiedad 'length'. 'K extends keyof T' (restricción keyof) asegura que una clave existe en un objeto, devolviendo el tipo de valor correcto. Las restricciones habilitan acceso a propiedades y llamadas a métodos type-safe en genéricos.
// Constraint: T must have a 'length' property
interface HasLength {
length: number;
}
function logLength<T extends HasLength>(item: T): void {
console.log(item.length);
}
logLength("hello"); // 5 (string has length)
logLength([1, 2, 3]); // 3 (array has length)
// logLength(42); // Error: number has no length
// Constraint with keyof
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Alice", age: 30 };
const name = getProperty(user, "name"); // string
// getProperty(user, "email"); // Error: not a keyParámetros de Tipo por Defecto
Los parámetros de tipo por defecto proporcionan un tipo de respaldo cuando no se especifica ninguno. Útiles para APIs con un caso común (por ejemplo, ApiResponse por defecto a string). Los defaults pueden depender de parámetros anteriores. Combina con restricciones (T extends X = DefaultType) para genéricos opcionales type-safe.
// Default type parameter
interface ApiResponse<T = string> {
data: T;
status: number;
}
const r1: ApiResponse = { data: "hello", status: 200 }; // T = string
const r2: ApiResponse<number> = { data: 42, status: 200 }; // T = number
// Multiple defaults
class Container<T = string, U = number> {
constructor(public a: T, public b: U) {}
}
const c1 = new Container("hi", 42); // Container<string, number>
const c2 = new Container<boolean>(true); // Container<boolean, number>
// Default with constraint
interface Box<T extends object = { id: number }> {
value: T;
}Interfaces y Tipos Genéricos
Las interfaces y type aliases genéricos crean contratos reutilizables type-safe. Repository<T> abstrae el acceso a datos con una API consistente. Result<T, E> es un discriminated union para manejo de errores sin excepciones. Los parámetros de tipo por defecto (E = Error) reducen el boilerplate para casos comunes.
// Generic interface
interface Repository<T> {
findById(id: string): Promise<T>;
save(item: T): Promise<void>;
delete(id: string): Promise<void>;
}
// Implement with concrete type
class UserRepo implements Repository<User> {
async findById(id: string): Promise<User> { /* ... */ }
async save(user: User): Promise<void> { /* ... */ }
async delete(id: string): Promise<void> { /* ... */ }
}
// Generic type alias with conditional
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
const success: Result<number> = { ok: true, value: 42 };
const failure: Result<string> = { ok: false, error: "not found" };Conditional Types
Los conditional types (T extends U ? X : Y) son if-statements a nivel de tipo. 'infer R' extrae un tipo de dentro de otro tipo (por ejemplo, el tipo de retorno de una función). Los conditional types se distribuyen sobre unions: ToArray<string | number> se convierte en string[] | number[]. Las utilidades integradas como Exclude, Extract y NonNullable usan esto.
// Conditional type: if T extends U, use X, else Y
type IsString<T> = T extends string ? true : false;
type A = IsString<"hi">; // true
type B = IsString<42>; // false
// Extract return type
type AsyncReturnType<T> =
T extends (...args: any[]) => Promise<infer R> ? R : never;
async function fetchUser(): Promise<User> { /* ... */ }
type U = AsyncReturnType<typeof fetchUser>; // User
// Distributive conditional types
type ToArray<T> = T extends any ? T[] : never;
type R = ToArray<string | number>; // string[] | number[]
// Exclude / Extract built on conditionals
type NonNullable<T> = T extends null | undefined ? never : T;Tipos Avanzados
Utility Types
TypeScript proporciona utility types integrados para transformaciones comunes: Partial (todo opcional), Pick (seleccionar claves), Omit (excluir claves), Record (map clave-valor), Required (quitar opcional), ReturnType (retorno de función), Parameters (params de función como tuple). Estos eliminan definiciones de tipo repetitivas.
interface User {
id: number;
name: string;
email: string;
age: number;
}
// Partial: all optional (for updates)
type UserUpdate = Partial<User>;
const patch: UserUpdate = { name: "Bob" };
// Pick: select specific keys
type UserSummary = Pick<User, "id" | "name">;
// Omit: exclude specific keys
type CreateUser = Omit<User, "id">;
// Record: key-value map
type UserMap = Record<string, User>;
// Required: all required (remove ?)
type StrictUser = Required<Partial<User>>;
// ReturnType: function return type
type R = ReturnType<() => string>; // string
// Parameters: function parameter types as tuple
type P = Parameters<(a: number, b: string) => void>; // [number, string]Template Literal Types
Los template literal types (TS 4.1+) construyen tipos string interpolando otros tipos. Combinados con unions, generan productos cartesianos de strings. Capitalize/Uppercase transforman el caso. Úsalos para nombres de eventos type-safe, generación de getter/setter y tipado de rutas de API. La cláusula 'as' en mapped types habilita el renombrado de claves.
// Build string types from other types
type Vertical = "top" | "bottom";
type Horizontal = "left" | "right";
type Position = `${Vertical}-${Horizontal}`;
// "top-left" | "top-right" | "bottom-left" | "bottom-right"
// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Cap = Capitalize<"foo">; // "Foo"
// Getter names from keys
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }
// Event listener types
type EventName = `on${Capitalize<"click" | "hover">}`;
// "onClick" | "onHover"Palabra Clave infer
La palabra clave 'infer' declara una variable de tipo dentro de la cláusula 'extends' de un conditional type, capturando un tipo para reutilización. Es la base de utility types como ReturnType, Parameters y Awaited. Usa infer para extraer tipos de estructuras complejas (arrays, promises, funciones) sin descomponerlas manualmente.
// Extract return type of a function
type MyReturnType<T> =
T extends (...args: any[]) => infer R ? R : never;
type R1 = MyReturnType<() => string>; // string
type R2 = MyReturnType<(x: number) => boolean>; // boolean
// Extract element type of an array
type ElementOf<T> = T extends (infer E)[] ? E : never;
type E = ElementOf<string[]>; // string
// Extract Promise value
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type V = Unwrap<Promise<number>>; // number
// Extract first parameter
type FirstParam<T> =
T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FP = FirstParam<(name: string, age: number) => void>; // stringDiscriminated Unions
Los discriminated unions (tagged unions) usan un campo literal compartido ('type', 'kind', 'tag') para distinguir variantes. TypeScript acota el tipo en cada case del switch, dando acceso a campos específicos del case. El default 'never' habilita comprobación de exhaustividad: si añades un case nuevo, el compilador da error hasta que lo manejes. Esencial para reducers de Redux y máquinas de estados.
// Discriminated union: shared 'type' (or 'kind') field
type Action =
| { type: "ADD_TODO"; text: string }
| { type: "DELETE_TODO"; id: number }
| { type: "TOGGLE_TODO"; id: number };
function reducer(state: Todo[], action: Action): Todo[] {
switch (action.type) {
case "ADD_TODO":
return [...state, { id: Date.now(), text: action.text }];
case "DELETE_TODO":
return state.filter(t => t.id !== action.id);
case "TOGGLE_TODO":
return state.map(t =>
t.id === action.id ? { ...t, done: !t.done } : t
);
default:
const _: never = action; // exhaustiveness check
return state;
}
}Type Guards: typeof e instanceof
Los type guards acotan tipos en runtime. 'typeof' funciona para primitivos (string, number, boolean, symbol, bigint, undefined, function, object). 'instanceof' comprueba prototipos de clase/constructor. Array.isArray() acota a un array tipado. Estos son guards integrados: sin código personalizado necesario. TypeScript rastrea el tipo acotado en cada rama.
// typeof: narrow primitives
function process(value: string | number) {
if (typeof value === "string") {
return value.toUpperCase(); // string
}
return value.toFixed(2); // number
}
// instanceof: narrow class instances
class Dog { bark() {} }
class Cat { meow() {} }
function speak(pet: Dog | Cat) {
if (pet instanceof Dog) {
pet.bark(); // Dog
} else {
pet.meow(); // Cat
}
}
// Array.isArray
function flatten(arr: (number | number[])[]) {
return arr.flatMap(x =>
Array.isArray(x) ? x : [x]
);
}Custom Type Guards y Operador in
El operador 'in' comprueba si una propiedad existe en un objeto, acotando al tipo que la tiene. Los type predicates (x is T) son funciones guard personalizadas que devuelven boolean pero también acotan el tipo. Usa 'unknown' como tipo de entrada para parseo seguro de datos externos (JSON.parse, respuestas de API). Los predicates habilitan comprobaciones de tipo reutilizables y componibles.
// 'in' operator: check for property existence
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // Fish
} else {
animal.fly(); // Bird
}
}
// Type predicate: custom guard function
function isString(x: unknown): x is string {
return typeof x === "string";
}
function isUser(x: any): x is { name: string; age: number } {
return x && typeof x.name === "string" && typeof x.age === "number";
}
const val: unknown = "hello";
if (isString(val)) {
console.log(val.toUpperCase()); // narrowed to string
}Estructuras de Datos
Arrays y ReadonlyArray
Los arrays de TypeScript están tipados: métodos como map, filter y reduce preservan los tipos de elemento. Usa readonly T[] o ReadonlyArray<T> para inmutabilidad. Las tuples tienen longitud fija y posiciones tipadas. El destructuring y spread de arrays son completamente type-safe. El sistema de tipos captura index-out-of-bounds y asignaciones de tipo incorrecto.
// Typed arrays
let nums: number[] = [1, 2, 3];
let strs: Array<string> = ["a", "b", "c"];
// ReadonlyArray (immutable)
const frozen: readonly number[] = [1, 2, 3];
// frozen.push(4); // Error: readonly
// Tuple (fixed length)
let pair: [string, number] = ["Alice", 30];
// Array methods are typed
const doubled = nums.map(n => n * 2); // number[]
const evens = nums.filter(n => n % 2 === 0); // number[]
const sum = nums.reduce((a, b) => a + b, 0); // number
// Spread and destructuring
const [first, ...rest] = nums;
const combined = [...nums, ...[4, 5]];Objetos y Records
Record<K, V> crea un map tipado con claves de tipo K y valores de tipo V. Partial<T> hace todas las propiedades opcionales: ideal para operaciones de update/patch. Object.entries/keys/values devuelven arrays tipados. Usa Pick y Omit para derivar tipos enfocados de existentes, manteniendo los tipos DRY.
// Object type
const user: { name: string; age: number } = { name: "Alice", age: 30 };
// Record: typed key-value map
const scores: Record<string, number> = {
math: 90,
science: 85,
};
// Partial: all optional (for patches)
const patch: Partial<typeof user> = { age: 31 };
// Pick / Omit
type Summary = Pick<typeof user, "name">;
type WithoutAge = Omit<typeof user, "age">;
// Object.entries / keys / values typed
const entries = Object.entries(scores); // [string, number][]
const keys = Object.keys(scores); // string[]
const values = Object.values(scores); // number[]Maps y Sets
Map y Set son colecciones ES6 con soporte completo de TypeScript. Las claves de Map pueden ser de cualquier tipo (a diferencia de los objetos, que coercionan claves a strings). Set almacena valores únicos. WeakMap/WeakSet permiten garbage collection de claves: útiles para metadatos adjuntos a elementos DOM u objetos sin prevenir el GC.
// Map: keyed collection (any key type)
const map = new Map<string, User>();
map.set("alice", { name: "Alice", age: 30 });
const user = map.get("alice"); // User | undefined
// Set: unique values
const unique = new Set<number>([1, 2, 2, 3]);
console.log(unique.size); // 3
console.log(unique.has(2)); // true
// Iteration (typed)
for (const [key, value] of map) {
console.log(key, value.name);
}
for (const num of unique) {
console.log(num);
}
// WeakMap / WeakSet (keys must be objects, GC-friendly)
const weak = new WeakMap<object, string>();Tuples y Labeled Tuples
Las tuples son arrays de longitud fija con posiciones tipadas. Las labeled tuples (TS 4.0+) añaden nombres para legibilidad: útiles para valores de retorno y datos tipo CSV. Las tuples habilitan múltiples valores de retorno sin crear una interfaz. Usa 'readonly' para prevenir mutación. Las tuples difieren de los arrays: [string, number] NO es (string | number)[].
// Basic tuple
let point: [number, number] = [10, 20];
// Labeled tuple (TS 4.0+)
let user: [id: number, name: string, active: boolean] = [1, "Alice", true];
// Destructuring with labels
const [id, name, active] = user;
// Tuple in function returns
function divmod(a: number, b: number): [quotient: number, remainder: number] {
return [Math.floor(a / b), a % b];
}
const [q, r] = divmod(17, 5);
console.log(q, r); // 3 2
// Readonly tuple
const fixed: readonly [string, number] = ["a", 1];
// fixed.push(2); // ErrorEnums y Const Enums
Los enums crean constantes con nombre. Los string enums son recomendados (legibles en salida, sin problemas de reverse mapping). Los const enums se borran en tiempo de compilación (coste cero en runtime). Para casos simples, los union types ('a' | 'b') suelen ser mejores: sin código en runtime, mejor tree-shaking. Usa enums para constantes agrupadas y documentadas.
// Numeric enum
enum Direction { North = 0, South = 1, East = 2, West = 3 }
// String enum (recommended)
enum HttpStatus {
OK = "200 OK",
NotFound = "404 Not Found",
ServerError = "500 Internal Server Error",
}
// Const enum (inlined at compile time)
const enum Color { Red, Green, Blue }
let c = Color.Red; // compiles to: let c = 0;
// Union type alternative (no runtime code)
type Status = "pending" | "success" | "error";
const s: Status = "pending";
// Exhaustive switch
function handle(s: Status) {
switch (s) {
case "pending": return "loading";
case "success": return "done";
case "error": return "failed";
}
}Datos Inmutables
TypeScript ofrece múltiples herramientas de inmutabilidad: 'readonly' para propiedades, ReadonlyArray para arrays, la utility Readonly<T>, y 'as const' para readonly profundo con tipos literales. Los datos inmutables previenen mutaciones accidentales y habilitan detección de cambios (React, Redux). Usa spread (...) para updates inmutables: crea un objeto nuevo con campos modificados.
// Readonly modifier
interface User {
readonly id: number;
name: string;
}
// ReadonlyArray
const nums: ReadonlyArray<number> = [1, 2, 3];
// nums[0] = 9; // Error
// Readonly utility type
type FrozenUser = Readonly<User>;
// as const: deep readonly with literal types
const config = {
endpoint: "/api",
methods: ["GET", "POST"],
} as const;
// config.endpoint: "/api" (literal, not string)
// config.methods: readonly ["GET", "POST"]
// Immutable update pattern
const user = { name: "Alice", age: 30 };
const updated = { ...user, age: 31 }; // new objectMódulos y Namespaces
ES Modules: Import y Export
TypeScript usa sintaxis de ES module (import/export). Los named exports son explícitos; el default export es el único export 'principal'. Usa 'import * as' para imports de namespace. La resolución de módulos sigue las convenciones de Node.js (node_modules, extensiones). Configura 'module' y 'moduleResolution' en tsconfig.json.
// math.ts - exporting
export function add(a: number, b: number): number {
return a + b;
}
export const PI = 3.14159;
export default function multiply(a: number, b: number): number {
return a * b;
}
// main.ts - importing
import multiply, { add, PI } from "./math";
import * as math from "./math"; // namespace import
console.log(add(1, 2)); // 3
console.log(multiply(3, 4)); // 12 (default)
console.log(math.PI); // 3.14159Imports Solo de Tipo
'import type' importa solo tipos (borrados en tiempo de compilación, sin código en runtime). Esto evita dependencias circulares e imports innecesarios en runtime. TS 4.5+ admite modificadores 'type' inline en imports mixtos. Usa imports solo de tipo para interfaces, type aliases y enums (si const) para reducir el tamaño del bundle.
// Type-only import (erased at runtime)
import type { User, Config } from "./types";
// Mixed import (TS 4.5+)
import { render, type Component } from "./ui";
// Re-export types
export type { User } from "./types";
// Import type for interfaces and types
interface UserService {
get(id: string): User; // User from type-only import
}
// 'import type' ensures no runtime dependency
// Useful when the module has side effects you want to avoidDynamic Imports
Los dynamic imports (import()) cargan módulos bajo demanda, devolviendo una Promise. Esto habilita code splitting y lazy loading: crítico para rendimiento en apps web. TypeScript infiere el tipo del módulo automáticamente. Úsalo para características opcionales, librerías grandes y code splitting basado en rutas (React.lazy, Next.js dynamic).
// Dynamic import returns a Promise
async function loadModule() {
const module = await import("./heavy-module");
module.doWork();
}
// Conditional loading
if (featureEnabled) {
const { Feature } = await import("./feature");
new Feature().init();
}
// Type the dynamic import
type HeavyModule = typeof import("./heavy-module");
// With error handling
try {
const lib = await import("./optional-lib");
lib.run();
} catch (e) {
console.log("Library not available");
}Archivos de Declaración y Module Augmentation
Los archivos de declaración (.d.ts) describen tipos para módulos JS, imports de CSS/PNG y variables globales. La module augmentation extiende tipos de módulos existentes: útil para añadir propiedades a Express Request, Express Response o tipos de terceros. Así es como middleware como passport añade el tipado de req.user.
// global.d.ts - declare ambient modules
declare module "*.css" {
const content: string;
export default content;
}
declare module "*.png" {
const src: string;
export default src;
}
// Module augmentation - extend existing types
import express from "express";
declare module "express" {
interface Request {
user?: { id: string; name: string };
}
}
// Now req.user is typed
app.get("/", (req, res) => {
console.log(req.user?.name);
});Namespaces (Legacy)
Los namespaces son el sistema de módulos de TypeScript anterior a ES6. Agrupan código relacionado bajo un objeto con nombre. Para proyectos nuevos, prefiere ES modules (import/export): están estandarizados, son tree-shakeables y funcionan con bundlers. Los namespaces siguen siendo útiles en archivos de declaración .d.ts para declaraciones de tipo globales y código legacy.
// Namespace: pre-ES6 way to organize code
namespace Validation {
export interface StringValidator {
isValid(s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isValid(s: string): boolean {
return lettersRegexp.test(s);
}
}
}
// Usage
const validator = new Validation.LettersOnlyValidator();
console.log(validator.isValid("Hello")); // true
// Prefer ES modules over namespaces for new code
// Namespaces are useful in .d.ts files for global declarationsConfiguración de Módulos en tsconfig
La configuración de módulos de tsconfig controla cómo TypeScript maneja los imports. 'moduleResolution: node' usa la resolución de Node.js (lookup de node_modules). 'esModuleInterop' habilita los imports por defecto desde CommonJS. 'paths' crea alias de import (@/components) para imports más limpios. 'resolveJsonModule' permite importar archivos .json con tipos inferidos.
{
"compilerOptions": {
"module": "ESNext", // ES module output
"moduleResolution": "node", // Node-style resolution
"esModuleInterop": true, // allow default imports from CJS
"allowSyntheticDefaultImports": true,
"resolveJsonModule": true, // import .json files
"isolatedModules": true, // each file is independent
"baseUrl": "./src", // base for non-relative imports
"paths": {
"@/*": ["./*"], // path alias
"@components/*": ["./components/*"]
}
}
}
// With paths config, you can import:
// import { Button } from "@/components/Button";
// instead of relative paths like "../../components/Button"Async y Promesas
Tipos Promise
Promise<T> es el tipo async core: T es el tipo del valor resuelto. TypeScript infiere tipos a través de cadenas .then(). Usa 'new Promise()' para envolver APIs basados en callbacks. Tipa siempre los valores resolve/reject. Prefiere async/await sobre cadenas .then() raw para legibilidad y manejo de errores.
// Promise<T> represents an async value
function fetchUser(id: number): Promise<User> {
return new Promise((resolve, reject) => {
setTimeout(() => {
if (id === 1) {
resolve({ id: 1, name: "Alice" });
} else {
reject(new Error("User not found"));
}
}, 100);
});
}
// Consuming a Promise
fetchUser(1)
.then(user => console.log(user.name))
.catch(err => console.error(err))
.finally(() => console.log("done"));
// Promise typing: then callbacks are typed
fetchUser(1).then(user => {
// user is User, not any
console.log(user.name); // string
});async/await
async/await es azúcar sintáctico sobre Promesas: 'await' pausa hasta que la Promise se resuelve. Las funciones async devuelven siempre una Promise. Usa Promise.all() para ejecución paralela (mucho más rápido que awaits secuenciales). Top-level await funciona en módulos ES con ES2022+. TypeScript comprueba que los valores awaited sean Promises.
// async function returns Promise<T>
async function getUser(id: number): Promise<User> {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP ${response.status}`);
}
return response.json() as Promise<User>;
}
// Top-level await (ES2022, TS 4.7+)
// const user = await getUser(1);
// Sequential vs parallel
async function loadAll() {
// Sequential (slow)
const a = await getUser(1);
const b = await getUser(2);
// Parallel (fast)
const [x, y] = await Promise.all([getUser(1), getUser(2)]);
return { a, b, x, y };
}Manejo de Errores en Async
Usa try/catch con async/await para manejo de errores: es más limpio que .catch(). TypeScript no soporta throws tipados (todos los errores son unknown en catch), así que acota con instanceof. Para errores predecibles, considera el patrón Result type (union ok/error) en lugar de excepciones: hace el manejo de errores explícito en la firma de tipo.
// Try/catch with async/await
async function riskyOperation(): Promise<string> {
try {
const data = await fetch("/api/data");
if (!data.ok) throw new Error(`HTTP ${data.status}`);
return await data.text();
} catch (error) {
if (error instanceof Error) {
console.error(error.message);
}
return "fallback";
} finally {
console.log("cleanup");
}
}
// Typed errors (TypeScript doesn't have typed throws)
class ApiError extends Error {
constructor(public status: number, message: string) {
super(message);
}
}
// Result type as alternative to exceptions
type Result<T> =
| { ok: true; value: T }
| { ok: false; error: string };
async function safeFetch(url: string): Promise<Result<string>> {
try {
const res = await fetch(url);
return { ok: true, value: await res.text() };
} catch (e) {
return { ok: false, error: String(e) };
}
}Promise Combinators
Los promise combinators orquestan múltiples operaciones async: all() (paralelo, fail-fast), allSettled() (paralelo, espera a todas), race() (primera en settled), any() (primera con éxito). Usa all() para carga de datos dependiente, allSettled() cuando quieres resultados parciales, race() para timeouts, any() para fetches redundantes.
// Promise.all: wait for all (rejects if any rejects)
const [users, posts] = await Promise.all([
fetchUsers(),
fetchPosts(),
]);
// Promise.allSettled: wait for all (never rejects)
const results = await Promise.allSettled([
fetch("/api/a"),
fetch("/api/b"),
]);
results.forEach(r => {
if (r.status === "fulfilled") console.log(r.value);
else console.log(r.reason);
});
// Promise.race: first to settle (resolve or reject)
const fastest = await Promise.race([
fetch("/api/fast"),
fetch("/api/slow"),
]);
// Promise.any: first to resolve (ignores rejections)
const first = await Promise.any([
fetch("/api/primary"),
fetch("/api/fallback"),
]);Event Loop y Microtasks
El event loop de JavaScript procesa microtasks (callbacks de Promise, queueMicrotask) antes que macrotasks (setTimeout, setInterval). Por eso las Promises se resuelven antes que los timeouts. La iteración async (for await...of) consume iterables async: útil para streams. Los async generators (async function*) producen iterables async, habilitando secuencias async perezosas.
// Microtasks (Promises) run before macrotasks (setTimeout)
console.log("1: sync");
setTimeout(() => console.log("4: macrotask"), 0);
Promise.resolve().then(() => console.log("3: microtask"));
console.log("2: sync");
// Output: 1, 2, 3, 4
// queueMicrotask for custom microtasks
queueMicrotask(() => console.log("microtask"));
// Async iteration (for await)
async function processStream(stream: AsyncIterable<Buffer>) {
for await (const chunk of stream) {
console.log(chunk.length);
}
}
// Async generators
async function* generate() {
for (let i = 0; i < 3; i++) {
await new Promise(r => setTimeout(r, 100));
yield i;
}
}Patrones de Concurrencia
Estos patrones son completamente type-safe en TypeScript. Debounce retrasa la ejecución hasta que las llamadas se detienen durante N ms (input de búsqueda). Throttle limita a una llamada por N ms (handlers de scroll). Semaphore/mapLimit controla la concurrencia: útil para APIs con rate limiting. Parameters<T> y ReturnType<T> preservan firmas de función en wrappers.
// Debounce with TypeScript
function debounce<T extends (...args: any[]) => void>(
fn: T,
delay: number
): (...args: Parameters<T>) => void {
let timer: ReturnType<typeof setTimeout>;
return (...args) => {
clearTimeout(timer);
timer = setTimeout(() => fn(...args), delay);
};
}
// Throttle
function throttle<T extends (...args: any[]) => void>(
fn: T,
limit: number
): (...args: Parameters<T>) => void {
let inThrottle = false;
return (...args) => {
if (!inThrottle) {
fn(...args);
inThrottle = true;
setTimeout(() => (inThrottle = false), limit);
}
};
}
// Semaphore (limit concurrency)
async function mapLimit<T, U>(
items: T[],
limit: number,
fn: (item: T) => Promise<U>
): Promise<U[]> {
const results: U[] = [];
const executing: Promise<void>[] = [];
for (const item of items) {
const p = fn(item).then(r => results.push(r));
executing.push(p);
if (executing.length >= limit) {
await Promise.race(executing);
executing.splice(executing.findIndex(e => e === p), 1);
}
}
await Promise.all(executing);
return results;
}Manejo de Errores y Testing
Try/Catch con Unknown
Desde TypeScript 4.4 (useUnknownInCatchVariables), los errores capturados son 'unknown': debes acotarlos antes de usarlos. Esto previene acceder a propiedades que no existen. Usa instanceof para comprobar tipos de error específicos, o String() como fallback. Crea un helper getErrorMessage() para extracción de errores consistente.
// In strict mode, catch is 'unknown' (not 'any')
try {
JSON.parse("invalid");
} catch (error: unknown) {
// Must narrow before using
if (error instanceof SyntaxError) {
console.error("JSON error:", error.message);
} else if (error instanceof Error) {
console.error(error.message);
} else {
console.error("Unknown error:", error);
}
}
// Helper function
function getErrorMessage(error: unknown): string {
if (error instanceof Error) return error.message;
return String(error);
}Clases de Error Personalizadas
Las clases de error personalizadas añaden datos estructurados (code, statusCode, field) a los errores. Llama siempre a super(message) y establece el prototype (Object.setPrototypeOf) para arreglar el problema de cadena de prototipos de TypeScript/ES5. Usa instanceof para distinguir tipos de error en bloques catch. Este patrón es esencial para middleware de Express y handlers de errores de API.
class AppError extends Error {
constructor(
message: string,
public code: string,
public statusCode: number = 500
) {
super(message);
this.name = "AppError";
// Fix prototype chain (TS quirk)
Object.setPrototypeOf(this, AppError.prototype);
}
}
class ValidationError extends AppError {
constructor(message: string, public field: string) {
super(message, "VALIDATION_ERROR", 400);
this.name = "ValidationError";
}
}
// Usage
function createUser(input: unknown) {
if (typeof input !== "object" || input === null) {
throw new ValidationError("Invalid input", "body");
}
}
try {
createUser("bad");
} catch (e) {
if (e instanceof ValidationError) {
console.log(e.field, e.statusCode); // "body" 400
}
}Patrón Result Type
El Result type (de Rust) hace los errores explícitos en la firma de tipo: los llamadores deben manejar tanto el éxito como el fallo. A diferencia de las excepciones, el compilador impone el manejo de errores. Úsalo para fallos esperados (validación, not-found) donde las excepciones serían excesivas. Reserva las excepciones para errores verdaderamente inesperados (bugs, fallos del sistema).
// Result type: explicit error handling without exceptions
type Result<T, E = string> =
| { ok: true; value: T }
| { ok: false; error: E };
function divide(a: number, b: number): Result<number> {
if (b === 0) {
return { ok: false, error: "Division by zero" };
}
return { ok: true, value: a / b };
}
// Usage: forced to handle both cases
const result = divide(10, 0);
if (result.ok) {
console.log(result.value); // number
} else {
console.error(result.error); // string
}
// Utility helpers
function ok<T>(value: T): Result<T, never> {
return { ok: true, value };
}
function err<E>(error: E): Result<never, E> {
return { ok: false, error };
}Assertion Functions
Las assertion functions (asserts X) lanzan si una condición falla Y acotan el tipo después. 'asserts value is string' le dice a TypeScript que después de la llamada, value es string. Es más limpio que comprobaciones if repetidas. Úsalo para validación en runtime en fronteras (input de API, config). Combina con Zod o io-ts para validación de schema.
// Assertion function: throws if condition is false
function assert(condition: unknown, message: string): asserts condition {
if (!condition) {
throw new Error(message);
}
}
// Assert value is a specific type
function assertString(value: unknown): asserts value is string {
if (typeof value !== "string") {
throw new Error(`Expected string, got ${typeof value}`);
}
}
// Usage: narrows type after assertion
const input: unknown = "hello";
assertString(input);
console.log(input.toUpperCase()); // input is now string
// Assert non-null
function assertDefined<T>(value: T | undefined): asserts value is T {
if (value === undefined) throw new Error("undefined");
}Parseo JSON Type-Safe
JSON.parse devuelve 'any': inseguro. Envuélvelo con un type guard para validar la forma en runtime y acotar el tipo. Para schemas complejos, usa Zod, io-ts o yup: generan tanto validadores en runtime como tipos TypeScript desde una única definición de schema. Es crítico para respuestas de API y entrada de usuario.
// Safe JSON parse with type guard
function safeParse<T>(json: string, guard: (x: unknown) => x is T): T | null {
try {
const parsed: unknown = JSON.parse(json);
if (guard(parsed)) return parsed;
return null;
} catch {
return null;
}
}
// Type guard for User
interface User { id: number; name: string; }
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null
&& typeof (x as User).id === "number"
&& typeof (x as User).name === "string";
}
const data = safeParse('{"id":1,"name":"Alice"}', isUser);
if (data) {
console.log(data.name); // User
}
// Using Zod for runtime validation
// import { z } from "zod";
// const UserSchema = z.object({ id: z.number(), name: z.string() });
// const user = UserSchema.parse(JSON.parse(json));Comprobación de Exhaustividad
La comprobación de exhaustividad asegura que manejas todos los cases de un union. Asigna el case por defecto a 'never': si añades una variante nueva al union, TypeScript da error porque el tipo nuevo no es asignable a 'never'. Esto captura cases faltantes en tiempo de compilación. Esencial para discriminated unions, reducers de Redux y máquinas de estados.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; w: number; h: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
case "rectangle":
return shape.w * shape.h;
default:
// If a new shape is added, this errors
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Adding a new variant:
// | { kind: "triangle"; base: number; height: number }
// Now the default case errors: Type 'triangle' is not assignable to 'never'Decoradores y Metadatos
Decoradores de Clase
Los decoradores de clase reciben la función constructora y pueden devolver una clase modificada. Las decorator factories (que devuelven una función) aceptan argumentos. Los decoradores son una característica experimental: habilita 'experimentalDecorators' en tsconfig. Usados intensivamente en NestJS, TypeORM y Angular para inyección de dependencias y metadatos.
// Class decorator: receives the constructor
function Logged<T extends new (...args: any[]) => any>(target: T): T {
return class extends target {
constructor(...args: any[]) {
super(...args);
console.log(`Created ${target.name}`);
}
};
}
@Logged
class Service {
constructor(public name: string) {}
}
const s = new Service("Auth");
// Logs: "Created Service"
// Decorator factory (with arguments)
function Prefix(prefix: string) {
return function <T extends new (...args: any[]) => any>(target: T): T {
return class extends target {
message = prefix + " " + (this as any).name;
};
};
}Decoradores de Método y Propiedad
Los decoradores de método reciben (target, propertyKey, descriptor) y pueden envolver el método original: útil para logging, caché y control de acceso. Los decoradores de propiedad reciben (target, key) y se usan a menudo para registrar metadatos. El descriptor.value es la función original: envuélvela para añadir comportamiento. Común en NestJS (@Get, @Post) y TypeORM (@Column).
// Method decorator: (target, key, descriptor)
function Log(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling ${propertyKey}(${args})`);
return original.apply(this, args);
};
}
class Calculator {
@Log
add(a: number, b: number): number {
return a + b;
}
}
new Calculator().add(2, 3);
// Logs: "Calling add(2,3)"
// Property decorator: (target, key)
function Required(target: any, key: string) {
// Store metadata for validation
console.log(`Required: ${key}`);
}Decoradores de Parámetro y Metadatos
Los decoradores de parámetro reciben (target, key, index) y se usan para inyección de dependencias (NestJS, Angular). El polyfill 'reflect-metadata' habilita metadatos de tipo en runtime: los decoradores pueden acceder a los tipos de parámetros vía Reflect.getMetadata('design:paramtypes'). Así es como los contenedores DI saben qué inyectar. Habilita con 'emitDecoratorMetadata: true' en tsconfig.
import "reflect-metadata";
// Parameter decorator
function Inject(target: any, key: string, index: number) {
console.log(`Inject param ${index} of ${key}`);
}
class Service {
constructor(@Inject private dep: any) {}
}
// Store and retrieve metadata
const METADATA_KEY = "design:type";
class Example {
greet(name: string): void {}
}
// reflect-metadata provides:
// - design:type (property type)
// - design:paramtypes (method parameter types)
// - design:returntype (method return type)
const types = Reflect.getMetadata("design:paramtypes", Example.prototype, "greet");
// types: [String]Accessor Decorators
Los accessor decorators se aplican a getters/setters. El descriptor tiene propiedades get/set que puedes envolver. Úsalos para validación, logging o cambiar la enumerabilidad. El patrón de validación (MaxLength, Min, Max) envuelve el setter para imponer restricciones en runtime. Así es como funciona class-validator (NestJS) para validación de DTO.
// Accessor decorator (getter/setter)
function Enumerable(value: boolean) {
return function (
target: any,
key: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}
class Person {
private _name: string = "";
@Enumerable(true)
get name(): string {
return this._name;
}
set name(value: string) {
this._name = value;
}
}
const p = new Person();
p.name = "Alice";
console.log(Object.keys(p)); // ["name"] (enumerable)
// Validation decorator
function MaxLength(len: number) {
return function (target: any, key: string) {
let value: string;
Object.defineProperty(target, key, {
get() { return value; },
set(v: string) {
if (v.length > len) throw new Error("Too long");
value = v;
},
});
};
}Decoradores Modernos (TC39 Stage 3)
TypeScript 5.0 soporta la propuesta de decorador TC39 Stage 3: una API estandarizada que reemplaza a experimentalDecorators. La nueva API usa un objeto de contexto (ClassMethodDecoratorContext) en lugar de (target, key, descriptor). Es más limpia, type-safe y eventualmente estará en el estándar JS. Úsala para proyectos nuevos; los decoradores experimentales permanecen para compatibilidad con NestJS/Angular.
// TC39 Stage 3 decorators (TS 5.0+, no experimentalDecorators needed)
// Uses a different API: context object
function log<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext<This, (this: This, ...args: Args) => Return>
) {
return function (this: This, ...args: Args): Return {
console.log(`Calling ${String(context.name)}`);
return target.call(this, ...args);
};
}
class Service {
@log
greet(name: string): string {
return `Hello, ${name}`;
}
}
// Class method decorator context provides:
// - name: property name
// - kind: "method" | "field" | "class"
// - access: { get(), set(value) } for fields
// - addInitializer(): run code after constructionDecorador Práctico: Memoize
Este decorador memoize cachea resultados de métodos basándose en argumentos: aceleraciones drásticas para funciones puras costosas como fibonacci. La caché es por instancia (usa un WeakMap para caché compartida). Los decoradores brillan para concerns transversales: logging, caché, validación, control de acceso, lógica de reintento. Mantienen la lógica de negocio limpia separando los concerns de infraestructura.
// Memoize: cache method results
function Memoize<This, Args extends any[], Return>(
target: (this: This, ...args: Args) => Return,
context: ClassMethodDecoratorContext
) {
const cache = new Map<string, Return>();
return function (this: This, ...args: Args): Return {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = target.call(this, ...args);
cache.set(key, result);
return result;
};
}
class MathService {
@Memoize
fibonacci(n: number): number {
if (n < 2) return n;
return this.fibonacci(n - 1) + this.fibonacci(n - 2);
}
}
const svc = new MathService();
console.time("first");
console.log(svc.fibonacci(40)); // slow
console.timeEnd("first");
console.time("second");
console.log(svc.fibonacci(40)); // fast (cached)
console.timeEnd("second");Utility Types
Partial, Required y Readonly
Partial<T> hace todas las propiedades opcionales: perfecto para operaciones de update/patch donde solo cambian algunos campos. Required<T> es la inversa. Readonly<T> hace todas las propiedades inmutables en tiempo de compilación. Estos son los utility types más comúnmente usados y eliminan la necesidad de mantener interfaces paralelas opcional/readonly manualmente.
interface User {
id: number;
name: string;
email: string;
}
// Partial<T>: all properties become optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string }
function updateUser(id: number, changes: UserUpdate) {
// Only provided fields are updated
Object.assign(users[id], changes);
}
updateUser(1, { name: "Alice" }); // OK — only name
// Required<T>: all properties become required (inverse of Partial)
type StrictUser = Required<Partial<User>>; // back to all required
// Readonly<T>: all properties become readonly
type FrozenUser = Readonly<User>;
const frozen: FrozenUser = { id: 1, name: "Alice", email: "[email protected]" };
// frozen.name = "Bob"; // Error: readonly
// Practical: immutable config objects
const config: Readonly<Config> = { /* ... */ };
// config.port = 8081; // Error — prevents accidental mutationPick, Omit y Record
Pick<T, K> extrae un subconjunto de propiedades; Omit<T, K> elimina propiedades: ambos crean tipos derivados sin duplicación. Record<K, V> crea un tipo map/diccionario con claves específicas. Son esenciales para DTOs (data transfer objects): deriva un tipo CreateUser de User omitiendo campos autogenerados como id y createdAt. Esto mantiene los tipos DRY y sincronizados.
interface User {
id: number;
name: string;
email: string;
role: string;
createdAt: Date;
}
// Pick<T, Keys>: select specific properties
type UserSummary = Pick<User, "id" | "name">;
// { id: number; name: string }
// Omit<T, Keys>: remove specific properties
type UserInput = Omit<User, "id" | "createdAt">;
// { name: string; email: string; role: string }
// Record<Keys, Value>: object with specific keys and value type
type UserRole = "admin" | "user" | "guest";
type Permissions = Record<UserRole, string[]>;
const perms: Permissions = {
admin: ["read", "write", "delete"],
user: ["read", "write"],
guest: ["read"],
};
// Combining: create a DTO from a full entity
type UserDTO = Pick<User, "id" | "name" | "email">;
type CreateUserDTO = Omit<User, "id" | "createdAt">;ReturnType, Parameters y Awaited
ReturnType y Parameters extraen tipos de funciones existentes: invaliosables al envolver o llamar funciones cuyas firmas no quieres duplicar. Awaited<T> desenvuelve Promises anidadas (Promise<Promise<T>> se convierte en T), esencial para tipos de retorno de funciones async. InstanceType obtiene el tipo de instancia de un constructor de clase. Estos habilitan composición de funciones type-safe y utilidades de orden superior.
function fetchUser(id: number): Promise<{ name: string; age: number }> {
return Promise.resolve({ name: "Alice", age: 30 });
}
// ReturnType<T>: the return type of a function
type FetchResult = ReturnType<typeof fetchUser>;
// Promise<{ name: string; age: number }>
// Awaited<T>: unwrap a Promise to its inner type
type User = Awaited<ReturnType<typeof fetchUser>>;
// { name: string; age: number }
// Parameters<T>: tuple of parameter types
type FetchParams = Parameters<typeof fetchUser>;
// [id: number]
// First parameter type
type FirstParam = Parameters<typeof fetchUser>[0]; // number
// ConstructorParameters<T>: parameters of a class constructor
class Point {
constructor(public x: number, public y: number) {}
}
type PointArgs = ConstructorParameters<typeof Point>; // [x: number, y: number]
// InstanceType<T>: the instance type of a constructor
type PointInstance = InstanceType<typeof Point>; // PointExclude, Extract y NonNullable
Exclude<T, U> elimina tipos de un union; Extract<T, U> mantiene solo los tipos coincidentes: ambos operan sobre miembros del union. NonNullable<T> elimina null y undefined. Son bloques de construcción: Omit se define como Pick<T, Exclude<keyof T, K>>. Usa Exclude/Extract para filtrar tipos union dinámicamente, por ejemplo, separar tipos de error de tipos de éxito en un union result.
type Role = "admin" | "user" | "guest" | "superadmin";
// Exclude<T, U>: remove types from a union
type NonAdmin = Exclude<Role, "admin" | "superadmin">;
// "user" | "guest"
// Extract<T, U>: keep only matching types from a union
type AdminRoles = Extract<Role, "admin" | "superadmin">;
// "admin" | "superadmin"
// NonNullable<T>: remove null and undefined
type MaybeUser = User | null | undefined;
type DefiniteUser = NonNullable<MaybeUser>; // User
// Practical: filter union types
type EventMap = {
click: MouseEvent;
keydown: KeyboardEvent;
scroll: UIEvent;
};
type EventName = keyof EventMap; // "click" | "keydown" | "scroll"
// Omit<T, K> is actually built from Pick and Exclude:
// type Omit<T, K> = Pick<T, Exclude<keyof T, K>>;Utility Types Personalizados
Los utility types personalizados componen los integrados para necesidades específicas. Optional<T, K> hace opcionales solo ciertos campos (más dirigido que Partial). DeepPartial/DeepReadonly se aplican recursivamente a objetos anidados: útiles para config y árboles de estado. El modificador -readonly en Mutable elimina readonly. Estos patrones muestran cómo mapped types y conditional types se combinan para programación potente a nivel de tipo.
// Make specific properties optional
type Optional<T, K extends keyof T> = Omit<T, K> & Partial<Pick<T, K>>;
interface User {
id: number;
name: string;
email: string;
}
type UserWithOptionalEmail = Optional<User, "email">;
// { id: number; name: string; email?: string }
// Make specific properties required
type Require<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
// Deep partial (recursively make all properties optional)
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};
// Deep readonly
type DeepReadonly<T> = {
readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];
};
// Mutable (remove readonly from all properties)
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
// Non-never keys (filter out never-valued properties)
type NonNeverKeys<T> = {
[K in keyof T]: T[K] extends never ? never : K;
}[keyof T];Conditional Types
Conditional Types Básicos (T extends U ? X : Y)
Los conditional types (T extends U ? X : Y) seleccionan un tipo basándose en una condición a nivel de tipo: como un ternario para tipos. Son la base de la programación a nivel de tipo de TypeScript. Cuando T es un union, la condición se distribuye sobre cada miembro (conditional types distributivos). La palabra clave infer extrae tipos de dentro de un patrón, como sacar el tipo de elemento de un array o el tipo resolve de una Promise.
// Conditional types choose a type based on a condition
// Syntax: T extends U ? X : Y
// Is T an array? Return its element type, else never
type ElementOf<T> = T extends (infer E)[] ? E : never;
type A = ElementOf<string[]>; // string
type B = ElementOf<number[]>; // number
type C = ElementOf<string>; // never (not an array)
// Is T a Promise? Unwrap it, else keep T
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type D = UnwrapPromise<Promise<number>>; // number
type E = UnwrapPromise<string>; // string (not a promise)
// Check if a type is a function
type IsFunction<T> = T extends (...args: any[]) => any ? true : false;
type F = IsFunction<() => void>; // true
type G = IsFunction<number>; // false
// Conditional types are evaluated lazily and distribute over unionsPalabra Clave infer (Extracción de Tipos)
La palabra clave infer declara una variable de tipo dentro de la cláusula extends de un conditional type, capturando cualquier tipo que coincida con esa posición. Es cómo se implementan ReturnType, Parameters y Awaited. infer puede usarse recursivamente (Unwrap<Promise<Promise<T>>>) para desenvolver completamente tipos anidados. Es la herramienta principal para extraer tipos de estructuras genéricas complejas.
// infer extracts a type from within a pattern
// Get the return type of a function (like ReturnType<T>)
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
// Get the first parameter type
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;
type FA = FirstArg<(name: string, age: number) => void>; // string
// Get the resolved value of a Promise (like Awaited)
type Unwrap<T> = T extends Promise<infer U> ? Unwrap<U> : T;
type Deep = Unwrap<Promise<Promise<Promise<number>>>>; // number (recursive!)
// Extract the value type from a Map
type MapValue<M> = M extends Map<any, infer V> ? V : never;
type MV = MapValue<Map<string, number>>; // number
// Extract element type from Set
type SetElement<S> = S extends Set<infer E> ? E : never;
// Get the instance type from a constructor
type Instance<T> = T extends new (...args: any[]) => infer I ? I : never;Conditional Types Distributivos
Los conditional types se distribuyen sobre unions: aplicar ToArray<A | B> da ToArray<A> | ToArray<B>, no (A | B)[]. Así es como Exclude y NonNullable filtran miembros del union: devuelven 'never' para los tipos excluidos, que colapsa en el union. Para prevenir la distribución, envuelve ambos lados en corchetes: [T] extends [U]. La distribución suele ser lo que quieres para filtrado, pero la no distributiva es necesaria para operaciones de 'envolver todo el union'.
// Conditional types DISTRIBUTE over unions
// T extends U ? X : Y applied to A | B becomes
// (A extends U ? X : Y) | (B extends U ? X : Y)
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// ToArray<string> | ToArray<number>
// = string[] | number[]
// WITHOUT distribution (wrap in brackets to prevent):
type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;
type Result2 = ToArrayNonDist<string | number>;
// (string | number)[] — a single array of the union
// Practical: filter out types from a union
type ExcludeNull<T> = T extends null | undefined ? never : T;
type Cleaned = ExcludeNull<string | null | number | undefined>;
// string | number (null and undefined filtered out)
// This is exactly how NonNullable<T> works:
// type NonNullable<T> = T extends null | undefined ? never : T;Restricciones de Conditional Types
Los conditional types pueden anidarse para crear discriminación a nivel de tipo (como una sentencia switch para tipos). Combinados con infer, extraen y derivan tipos de parámetros genéricos. Así es como librerías como React derivan tipos de props desde definiciones de componentes, y cómo las librerías de routing extraen tipos de parámetros de cadenas de ruta. La restricción (T extends any[]) asegura que la entrada es válida antes de que el conditional extraiga el tipo de elemento.
// Use conditional types to constrain and derive types
// Get the props type of a React component
type PropsOf<C> = C extends React.ComponentType<infer P> ? P : never;
type ButtonProps = PropsOf<typeof Button>; // the component's prop type
// Conditional with multiple branches (nested)
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 T1 = TypeName<string>; // "string"
type T2 = TypeName<() => void>; // "function"
type T3 = TypeName<{}>; // "object"
// Constraint + conditional: only allow arrays, get element type
function firstElement<T extends any[]>(arr: T): T extends (infer E)[] ? E : never {
return arr[0] as any;
}
const x: number = firstElement([1, 2, 3]); // numberTemplate Literal Types (Manipulación de Strings)
Los template literal types habilitan manipulación de strings a nivel de tipo: concatenación, conversión de caso y coincidencia de patrones. Combinados con conditional types e infer, pueden parsear cadenas de ruta para extraer parámetros de ruta, generar nombres de event handler o construir accessors de propiedades type-safe. Así es como frameworks como Next.js y tRPC crean APIs end-to-end type-safe desde literales string.
// Template literal types: type-level string operations
type Greeting = `Hello ${string}`;
const g: Greeting = "Hello World"; // OK
// const bad: Greeting = "Hi World"; // Error
// Uppercase, Lowercase, Capitalize, Uncapitalize
type Upper = Uppercase<"hello">; // "HELLO"
type Lower = Lowercase<"WORLD">; // "world"
type Cap = Capitalize<"foo">; // "Foo"
// Build event handler names from event names
type EventName = "click" | "focus" | "blur";
type HandlerName = `on${Capitalize<EventName>}`;
// "onClick" | "onFocus" | "onBlur"
// Extract route parameters
type ExtractParams<Path extends string> =
Path extends `${infer _Start}/${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: Path extends `${infer _Start}/${infer Param}`
? Param
: never;
type Params = ExtractParams<"/users/:id/posts/:postId">;
// ":id" | ":postId"
// Property accessor type: "a.b.c" -> nested type
type Get<T, P extends string> =
P extends `${infer Key}.${infer Rest}`
? Key extends keyof T ? Get<T[Key], Rest> : never
: P extends keyof T ? T[P] : never;Mapped Types
Mapped Types Básicos
Los mapped types iteran sobre las claves de un objeto y transforman cada propiedad: [K in keyof T] es la sintaxis. Son cómo se implementan Partial, Readonly, Pick y otros utility types. Puedes modificar el tipo de propiedad (T[K] | null), añadir modificadores (? o readonly) o reemplazar completamente el tipo de valor. Los mapped types son la columna vertebral del sistema de transformación de tipos de TypeScript.
// Mapped types transform each property of an existing type
// Make all properties optional (like Partial<T>)
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
// Make all properties readonly (like Readonly<T>)
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
// Make all properties nullable
type Nullable<T> = {
[K in keyof T]: T[K] | null;
};
interface User {
id: number;
name: string;
email: string;
}
type NullableUser = Nullable<User>;
// { id: number | null; name: string | null; email: string | null }
// Change all property types to a specific type
type Stringify<T> = {
[K in keyof T]: string;
};
type StringUser = Stringify<User>;
// { id: string; name: string; email: string }Key Remapping vía 'as'
El key remapping (cláusula as, TS 4.1+) permite renombrar o filtrar claves durante el mapping. Usa template literal types para transformar nombres de claves (añadir prefijos, convertir a getters, mayúsculas). Devolver 'never' para una clave la elimina: así es como filtras propiedades. Combinado con conditional types, el key remapping habilita transformaciones potentes como convertir un schema de datos a un schema de validación o un tipo de API a un tipo de formulario.
// Remap keys using 'as' (TypeScript 4.1+)
// Add a prefix to all keys
type Prefix<T, P extends string> = {
[K in keyof T as `${P}${Capitalize<string & K>}`]: T[K];
};
interface User {
id: number;
name: string;
}
type PrefixedUser = Prefix<User, "user">;
// { userId: number; userName: string }
// Convert keys to getters
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string }
// Filter out keys by returning never
type RemoveKindField<T> = {
[K in keyof T as Exclude<K, "kind">]: T[K];
};
type WithoutKind = RemoveKindField<{ kind: "circle"; radius: number }>;
// { radius: number }
// Make keys UPPERCASE
type UpperKeys<T> = {
[K in keyof T as Uppercase<string & K>]: T[K];
};Modificadores: +, -, ?, readonly
Los modificadores + y - añaden o eliminan modificadores de propiedad. -? elimina la opcionalidad (haciendo los campos opcionales requeridos); -readonly elimina la inmutabilidad. Así es como funcionan Required<T> y el patrón Mutable. El prefijo + es opcional (readonly es lo mismo que +readonly), pero - es requerido para eliminación. Esto da control fino sobre las características de propiedad durante las transformaciones de tipo.
// Add (+) or remove (-) modifiers
// Remove optional (?) modifier: -?
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
interface OptionalUser {
id?: number;
name?: string;
}
type RequiredUser = Concrete<OptionalUser>;
// { id: number; name: string } — all required now
// Remove readonly modifier: -readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
interface FrozenConfig {
readonly port: number;
readonly host: string;
}
type EditableConfig = Mutable<FrozenConfig>;
// { port: number; host: string } — mutable now
// Add readonly modifier: +readonly (or just readonly)
type Freeze<T> = {
+readonly [K in keyof T]: T[K];
};
// Add optional modifier: +?
type MakeOptional<T> = {
[K in keyof T]+?: T[K];
};Mapped Types Homomórficos
Los mapped types homomórficos ([K in keyof T]) preservan los modificadores de propiedad (readonly, ?) del tipo fuente: por eso Pick<User, 'id'> mantiene id readonly. Los mappings no homomórficos (por ejemplo, [K in string]) no preservan modificadores. Esto importa al derivar tipos: un Partial homomórfico de un tipo con campos readonly mantiene esos campos readonly (pero opcionales). Entender la homomorfía ayuda a predecir si los modificadores sobreviven a una transformación.
// Homomorphic mapped types preserve modifiers from the original type
interface User {
readonly id: number;
name: string;
email?: string;
}
// Homomorphic: [K in keyof T] — preserves readonly and ?
type Clone<T> = {
[K in keyof T]: T[K];
};
type ClonedUser = Clone<User>;
// { readonly id: number; name: string; email?: string }
// Modifiers are PRESERVED from User
// Non-homomorphic: doesn't preserve modifiers
type NonHomo<T> = {
[K in string]: T; // not keyof T — loses modifiers
};
// Pick is homomorphic — preserves modifiers
type UserPick = Pick<User, "id" | "name">;
// { readonly id: number; name: string } — readonly preserved!
// Practical: create a "patch" type that preserves optionality
type Patch<T> = {
[K in keyof T]?: T[K]; // Partial is homomorphic
};Construir un Tipo de Validación desde un Schema
Así es como las librerías de formularios (React Hook Form, Formik) y de validación (Zod, Yup) mantienen type safety: derivan tipos de validador y formulario desde tus interfaces de datos usando mapped types. Cuando añades un campo a User, los tipos de validador y formulario lo requieren automáticamente también, previniendo drift. Esto demuestra el poder real de los mapped types: una única fuente de verdad (la interfaz) impulsa múltiples tipos derivados.
// Practical: derive a validator type from a data type
// Given a data interface
interface User {
id: number;
name: string;
email: string;
}
// Create a validator type where each field is a validation function
type Validator<T> = {
[K in keyof T]: (value: T[K]) => boolean;
};
const userValidator: Validator<User> = {
id: (v) => v > 0,
name: (v) => v.length > 0,
email: (v) => v.includes("@"),
};
// Create a form type where fields are wrapped in a FormField
type FormField<T> = {
value: T;
error: string | null;
touched: boolean;
};
type Form<T> = {
[K in keyof T]: FormField<T[K]>;
};
const userForm: Form<User> = {
id: { value: 1, error: null, touched: false },
name: { value: "Alice", error: null, touched: true },
email: { value: "", error: "Required", touched: true },
};
// The form type is always in sync with User — add a field to
// User and the form type automatically requires it too.Type Guards y Narrowing
Narrowing con typeof e instanceof
TypeScript acota tipos basándose en comprobaciones de runtime. typeof acota primitivos (string, number, boolean, etc.); instanceof acota instancias de clase. Las comprobaciones de truthiness (if (value)) acotan excluyendo null/undefined/0/''/false. El narrowing se aplica dentro de la rama donde la condición se cumple. Así es como TypeScript hace que las comprobaciones de runtime transporten información de tipo, eliminando la necesidad de casts explícitos.
// typeof narrows primitive types
function padLeft(value: string | number, padding: string | number) {
if (typeof padding === "number") {
return " ".repeat(padding) + value;
// padding is narrowed to 'number' here
}
return padding + value;
// padding is narrowed to 'string' here
}
// instanceof narrows class types
class Cat { meow(): void {} }
class Dog { bark(): void {} }
function speak(animal: Cat | Dog) {
if (animal instanceof Cat) {
animal.meow(); // OK — narrowed to Cat
} else {
animal.bark(); // OK — narrowed to Dog
}
}
// typeof returns: "string" | "number" | "boolean" | "symbol"
// "bigint" | "undefined" | "object" | "function"
// Note: typeof null === "object" (historical JS bug)
// Truthiness narrowing
function process(value?: string) {
if (value) {
console.log(value.toUpperCase()); // value is string (not undefined)
}
}Operador in y Discriminated Unions
El operador 'in' acota basándose en la existencia de propiedades. Los discriminated unions usan una propiedad literal compartida (como 'kind' o 'type') como tag: hacer switch sobre ella acota a la variante correcta con acceso completo a propiedades. Es el equivalente TypeScript de sum types / algebraic data types. Es el patrón estándar para acciones de Redux, máquinas de estados y respuestas de API con múltiples formas. Usa siempre un tipo literal para el discriminante.
// 'in' operator checks for a property — narrows to types that have it
interface Fish { swim(): void; }
interface Bird { fly(): void; }
function move(animal: Fish | Bird) {
if ("swim" in animal) {
animal.swim(); // narrowed to Fish
} else {
animal.fly(); // narrowed to Bird
}
}
// Discriminated unions: a shared literal property (the 'discriminant')
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number }
| { kind: "rectangle"; width: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2; // narrowed by kind
case "square":
return shape.size ** 2;
case "rectangle":
return shape.width * shape.height;
}
}
// Discriminated unions are the idiomatic TS pattern for
// variant data (like Redux actions, AST nodes, API responses)Funciones Custom Type Guard
Los custom type guards (tipo de retorno 'x is Type') permiten encapsular comprobaciones complejas de runtime en funciones reutilizables que acotan tipos. Las assertion functions (asserts x is T) lanzan en lugar de devolver boolean: acotan en todo el código después de la llamada. Usa type guards para validar datos no confiables (JSON.parse, respuestas de API) y llevarlos al sistema de tipos. Esto tiende un puente entre la validación en runtime y los tipos en tiempo de compilación.
// A type guard function has a special return type: 'x is Type'
// It narrows the type when it returns true
interface User {
id: number;
name: string;
}
function isUser(obj: any): obj is User {
return (
typeof obj === "object" &&
obj !== null &&
typeof obj.id === "number" &&
typeof obj.name === "string"
);
}
const data: unknown = JSON.parse('{"id": 1, "name": "Alice"}');
if (isUser(data)) {
console.log(data.name); // data is narrowed to User
}
// Type guard for arrays
function isStringArray(arr: unknown): arr is string[] {
return Array.isArray(arr) && arr.every((x) => typeof x === "string");
}
// Type guard for discriminated unions
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function isSuccess<T>(r: Result<T>): r is { success: true; data: T } {
return r.success;
}
// Assertion functions (TS 3.7+): throw if condition fails
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value === null || value === undefined) {
throw new Error("Expected value to be defined");
}
}Comprobación de Exhaustividad con never
La comprobación de exhaustividad usa el tipo 'never' para asegurar que todas las variantes del union se manejen. Si añades una variante nueva al union pero olvidas un case, la asignación 'never' de la rama por defecto se convierte en un error de compilación. El helper assertNever lanza en runtime y da error en compilación para cases faltantes. Es el patrón más valioso para discriminated unions: hace que el compilador te diga cuándo has olvidado un case.
// The 'never' type ensures all cases are handled
type Shape =
| { kind: "circle"; radius: number }
| { kind: "square"; size: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "square":
return shape.size ** 2;
default:
// If we add a new shape but forget a case, this line
// becomes a compile error because 'shape' would not be 'never'
const _exhaustive: never = shape;
return _exhaustive;
}
}
// Adding a new variant without updating area():
// type Shape = ... | { kind: "triangle"; base: number; height: number }
// Error: Type '{ kind: "triangle"; ... }' is not assignable to type 'never'
// Helper function pattern
function assertNever(x: never): never {
throw new Error("Unexpected: " + JSON.stringify(x));
}
function process(shape: Shape) {
switch (shape.kind) {
case "circle": return;
case "square": return;
default: return assertNever(shape); // compile error if a case is missing
}
}Narrowing con Métodos de Array
Array.filter no acota tipos de elemento por defecto porque su callback devuelve boolean, no un type guard. Para acotar, pasa una función custom type guard (pet is Dog): entonces filter devuelve el tipo de array acotado. TypeScript también acota dentro de cuerpos de callback (forEach, map) basándose en comprobaciones if. Array.isArray es un type guard integrado que acota unknown/any a un tipo array.
// TypeScript narrows through filter, map, and control flow
interface Pet {
name: string;
speak(): void;
}
class Dog implements Pet {
name = "Rex";
speak() { console.log("Woof"); }
fetch() { console.log("Fetching!"); }
}
class Cat implements Pet {
name = "Whiskers";
speak() { console.log("Meow"); }
}
const pets: Pet[] = [new Dog(), new Cat(), new Dog()];
// filter doesn't narrow by default (callback return type is boolean)
// Use a type guard to narrow:
function isDog(pet: Pet): pet is Dog {
return pet instanceof Dog;
}
const dogs = pets.filter(isDog); // Dog[] — properly narrowed!
dogs.forEach((d) => d.fetch()); // OK — d is Dog
// Narrowing in forEach/callbacks
pets.forEach((pet) => {
if (isDog(pet)) {
pet.fetch(); // narrowed to Dog inside callback
}
});
// Array.isArray narrows 'unknown' to an array
function process(input: unknown) {
if (Array.isArray(input)) {
input.length; // OK — input is any[] (or unknown[])
}
}Inferencia de Tipos
Inferencia de Variables y Tipo de Retorno
TypeScript infiere tipos desde inicializadores y sentencias return, así que rara vez necesitas anotaciones explícitas. Las variables se amplían a su tipo general (let x = 10 infiere number, no 10). 'as const' previene el widening: hace que los literales se queden literales, los objetos readonly y los arrays se conviertan en tuples readonly. typeof colors[number] extrae un union de tipos de elemento de tuple: un patrón común para derivar tipos tipo enum desde arrays.
// TypeScript infers types when you don't annotate
// Variable inference
let count = 0; // number
let name = "Alice"; // string
let items = [1, 2, 3]; // number[]
let mixed = [1, "two"]; // (string | number)[]
// Function return type inference
function add(a: number, b: number) {
return a + b; // return type inferred as number
}
// const assertions for literal types
const x = 10; // number (widened)
const y = "hello"; // string (widened)
const z = { a: 1 }; // { a: number }
const x2 = 10 as const; // 10 (literal type)
const y2 = "hello" as const; // "hello" (literal type)
const z2 = { a: 1 } as const; // { readonly a: 10 }
// Array with 'as const' becomes a readonly tuple
const colors = ["red", "green", "blue"] as const;
// readonly ["red", "green", "blue"]
type Color = typeof colors[number]; // "red" | "green" | "blue"Contextual Typing
El contextual typing fluye el tipo esperado hacia atrás en las expresiones. Cuando asignas una función a una variable tipada, los tipos de parámetro se infieren desde el tipo objetivo. Por eso los event handlers, callbacks de array y object literals a menudo no necesitan anotaciones de tipo. La regla general: anota firmas de función (parámetros y tipos de retorno para APIs públicas), pero deja que la inferencia maneje locales y callbacks.
// Contextual typing: the expected type influences the inferred type
// Without context, the parameter type is unknown
window.onmousedown = (mouseEvent) => {
// mouseEvent is inferred as MouseEvent (from onmousedown's type)
console.log(mouseEvent.button);
};
// Array map: callback params inferred from array type
const nums = [1, 2, 3];
const doubled = nums.map((n) => n * 2); // n is number, result is number[]
// Contextual typing for object literals
interface User {
name: string;
age: number;
}
const user: User = {
name: "Alice", // inferred as string (from User.name)
age: 30,
};
// Contextual typing with discriminated unions
type Event =
| { type: "click"; x: number; y: number }
| { type: "scroll"; scrollTop: number };
function handle(e: Event) {
if (e.type === "click") {
console.log(e.x, e.y); // narrowed
}
}
// Best practice: let inference work for you — don't over-annotate
const nums2 = [1, 2, 3].map((n) => n * 2); // number[] — no annotation neededBest Common Type (Inferencia de Union)
Al inferir desde múltiples valores (como array literals), TypeScript encuentra el 'best common type': normalmente un supertipo o un union. Un array de [Dog, Cat] infiere como Animal[] (la base común), no (Dog | Cat)[]. Para obtener un union, anota explícitamente. Los returns condicionales infieren el union de todas las ramas. Entender esto ayuda a predecir cuándo necesitas anotaciones explícitas vs cuándo la inferencia basta.
// When inferring from multiple values, TS finds the best common type
// Array of same type: inferred as that type
const nums = [1, 2, 3]; // number[]
// Array of different types: inferred as union
const mixed = [1, "two", true]; // (string | number | boolean)[]
// Array of subclasses: inferred as the common supertype
class Animal { name: string; }
class Dog extends Animal { bark(): void {} }
class Cat extends Animal { meow(): void {} }
const pets = [new Dog(), new Cat()]; // Animal[] (not (Dog | Cat)[])
// To get a union instead, use an explicit type annotation:
const pets2: (Dog | Cat)[] = [new Dog(), new Cat()];
// Or use 'as const' for readonly tuples:
const tuple = [new Dog(), new Cat()] as const;
// readonly [Dog, Cat]
// Return type inference with conditionals
function getValue(flag: boolean) {
return flag ? 42 : "hello"; // inferred as number | string
}Análisis de Flujo de Control
TypeScript realiza análisis de flujo de control: rastrea cómo los tipos se acotan y amplían a través de if/else, returns, asignaciones y operadores lógicos. Un tipo se acota tras una comprobación y se mantiene acotado hasta que la variable se reasigna. Los returns tempranos (guard clauses) son especialmente efectivos: después de 'if (value === null) return', el resto de la función sabe que value no es null. Por eso el código estilo guard-clause funciona tan bien con TypeScript.
// TypeScript tracks types through control flow
function example(value: string | number | null) {
// After this check, 'value' is string | number in this branch
if (value === null) {
return;
}
// value is now string | number (null narrowed out)
console.log(value);
if (typeof value === "string") {
return value.toUpperCase(); // value is string
}
// value is now number (string and null narrowed out)
return value.toFixed(2);
}
// Narrowing through assignments
let x: string | number;
x = "hello";
console.log(x.toUpperCase()); // x is string
x = 42;
console.log(x.toFixed(2)); // x is number
// Narrowing with && and ||
function process(input?: string) {
const value = input && input.trim(); // string | undefined
const safe = input || "default"; // string (undefined narrowed out)
}
// Narrowing is reset when a variable is reassigned
let v: string | number = "hi";
v.toUpperCase(); // string
v = 42;
// v.toUpperCase(); // Error: number doesn't have toUpperCaseOperador satisfies (TS 4.9+)
El operador 'satisfies' (TS 4.9+) valida que un valor se ajusta a un tipo preservando el tipo inferido más específico, a diferencia de las anotaciones de tipo que lo amplían. Es ideal para configs, route maps y objetos theme: obtienes validación en tiempo de compilación de que la estructura es correcta, pero el acceso a propiedades sigue devolviendo el tipo literal preciso. Combina con 'as const' para preservación literal y validación estructural.
// 'satisfies' checks a value matches a type WITHOUT widening it
// Problem: annotation widens the type
const config1: Record<string, string | number> = {
port: 8080,
host: "localhost",
};
// config1.port is 'string | number' (widened — lost the literal)
// Solution: 'satisfies' validates but preserves the inferred type
const config2 = {
port: 8080,
host: "localhost",
} satisfies Record<string, string | number>;
// config2.port is 'number' (preserved!) but still validated
// Practical: type-safe route configs
type Routes = Record<string, { method: string; handler: () => void }>;
const routes = {
"/users": { method: "GET", handler: () => fetchUsers() },
"/posts": { method: "POST", handler: () => createPost() },
} satisfies Routes;
routes["/users"].method; // string (from Routes)
routes["/users"].handler; // () => void (from Routes)
// 'as const' + 'satisfies': literal types + validation
const colors = {
primary: "#ff0000",
secondary: "#00ff00",
} as const satisfies Record<string, `#${string}`;Archivos de Declaración y Module Augmentation
Escribir Archivos de Declaración .d.ts
Los archivos .d.ts contienen declaraciones de tipo (sin implementación): describen los tipos de código JavaScript. Usa 'declare module' para añadir tipos a paquetes npm sin tipar. 'declare global' extiende tipos globales como Window. Las declaraciones ambientales le dicen a TypeScript 'esto existe en runtime, confía en mí'. Así es como integras JS legacy, APIs del navegador y variables inyectadas en build-time al sistema de tipos.
// types/my-module.d.ts — describes the types of a JS library
// Declare a module (for libraries without types)
declare module "untyped-lib" {
export function greet(name: string): string;
export const version: string;
export interface Config {
timeout: number;
retries: number;
}
}
// Ambient declarations for global variables
declare global {
interface Window {
myCustomProp: string;
myApp: { init: () => void };
}
}
// Now window.myCustomProp is typed
// window.myCustomProp; // string
// Declaring a global function
declare function myGlobalFn(x: number): string;
// Declaring a global namespace
declare namespace MyLib {
function doSomething(): void;
const version: string;
}
// Use 'declare' for things that exist at runtime but not in TS
// Common for: legacy JS, browser globals, build-time constantsModule Augmentation (Extender Tipos Existentes)
La module augmentation extiende tipos existentes de otros módulos: añade propiedades a interfaces sin modificar la fuente original. Así es como el middleware de Express (como passport) añade req.user, y cómo extiendes tipos de librerías de terceros. La sintaxis 'declare module' reabre el espacio de tipos del módulo. Las augmentations deben estar en un módulo (un archivo con import/export) para tener efecto global.
// Module augmentation: add to an existing module's types
// Extend an interface from another module
import express from "express";
declare module "express" {
interface Request {
user?: {
id: number;
name: string;
};
// Now req.user is typed on all Express requests
}
}
// Augment a third-party module
declare module "axios" {
export interface AxiosRequestConfig {
retryCount?: number; // add a custom config option
}
}
// Augment a global interface
declare global {
interface Array<T> {
last(): T | undefined; // add a custom array method
}
}
// Implementation (in a .ts file, not .d.ts)
Array.prototype.last = function () {
return this[this.length - 1];
};
[1, 2, 3].last(); // 3 — now typed!
// Module augmentation is how middleware adds typed properties
// to req/res objects in Express, Fastify, etc.Triple-Slash Directives
Las triple-slash directives (///) son comentarios especiales del compilador que instruyen a TypeScript a incluir archivos adicionales o paquetes de tipo. La más común es /// <reference types='node' /> para incluir @types/node. Con las opciones 'types' y 'lib' modernas de tsconfig.json, rara vez se necesitan: prefiere configuración basada en config. Se ven principalmente en archivos .d.ts y código legacy. Entenderlas ayuda al leer archivos de declaración.
// Triple-slash directives are special comments processed by TS
/// <reference path="./other-types.d.ts" />
// Includes another declaration file (rarely needed with modules)
/// <reference types="node" />
// Includes types from @types/node (like process, Buffer, __dirname)
/// <reference lib="es2020" />
// Includes a built-in lib (alternative to tsconfig "lib")
// Most common use: referencing @types packages
// In a .d.ts file for a package that needs Node types:
/// <reference types="node" />
declare function readFile(path: string): Buffer; // Buffer from @types/node
// NOTE: With modern TS and tsconfig "types" and "lib" options,
// triple-slash directives are rarely needed. They're mostly
// used in .d.ts files for backward compatibility.
// Prefer tsconfig.json settings:
// {
// "compilerOptions": {
// "types": ["node"], // instead of /// <reference types="node" />
// "lib": ["es2020", "dom"] // instead of /// <reference lib="es2020" />
// }
// }Publicar Tipos con un Paquete
Para publicar tipos TypeScript con tu paquete npm, establece 'types' en package.json para apuntar a tu archivo .d.ts y habilita 'declaration: true' en tsconfig. Los consumidores obtienen tipos automáticamente al instalar tu paquete. declarationMap habilita 'Go to Definition' para saltar al archivo fuente .ts. Para librerías sin tipos empaquetados, el proyecto DefinitelyTyped (@types/package) proporciona declaraciones mantenidas por la comunidad.
// package.json for a library with TypeScript types
{
"name": "my-library",
"version": "1.0.0",
"main": "dist/index.js",
"types": "dist/index.d.ts", // <-- points to the type declarations
"files": ["dist"],
"scripts": {
"build": "tsc",
"prepublishOnly": "npm run build"
}
}
// tsconfig.json for building a library
{
"compilerOptions": {
"declaration": true, // generate .d.ts files
"declarationMap": true, // generate .d.ts.map (source maps for types)
"sourceMap": true, // generate .js.map
"outDir": "./dist",
"rootDir": "./src",
"composite": true // enable project references
}
}
// Consumers get types automatically when they 'npm install my-library'
import { myFunction } from "my-library"; // fully typed!
// For libraries without bundled types, install @types package:
// npm install --save-dev @types/express
// Check if types exist: https://www.typescriptlang.org/dt/searchImports y Exports Solo de Tipo
'import type' importa solo información de tipo: se borra completamente en runtime, reduciendo el tamaño del bundle y evitando problemas de dependencias circulares. Úsalo para interfaces, type aliases y re-exports solo de tipo. La sintaxis inline 'import { x, type Y }' (TS 4.5+) mezcla imports de valor y tipo de forma limpia. verbatimModuleSyntax (TS 5.0+) lo impone estrictamente. Prefiere 'import type' siempre que importes algo usado solo en posiciones de tipo.
// 'import type' imports only types (erased at runtime, no JS emitted)
import type { User, Config } from "./types";
import type { ReactNode } from "react";
// These are erased completely — no runtime import in the output JS
// Useful for: reducing bundle size, avoiding circular dependencies
// Mixed import: value + type
import { createStore, type Store } from "redux";
// createStore is a runtime import; Store is type-only
// 'export type' re-exports types only
export type { User, Config } from "./types";
// Interface vs Type for declarations
interface IUser { name: string; } // can be augmented/merged
type TUser = { name: string }; // cannot be merged, more flexible
// VerbatimModuleSyntax (TS 5.0+): enforces 'import type' for type-only
// imports, ensuring they're always elided
// {
// "compilerOptions": { "verbatimModuleSyntax": true }
// }Opciones de tsconfig.json
Opciones Core del Compilador
El tsconfig.json controla cómo compila TypeScript. 'target' establece la versión JS de salida; 'module' establece el sistema de módulos. 'strict: true' es el ajuste más importante: habilita todas las comprobaciones estrictas de tipo (noImplicitAny, strictNullChecks, etc.). 'lib' determina qué APIs integradas están disponibles (DOM para navegador, ES2022 para características JS modernas). Empieza siempre proyectos nuevos con strict: true.
{
"compilerOptions": {
"target": "ES2022", // JS version to compile to
"module": "ESNext", // module system (ESNext, CommonJS, etc.)
"moduleResolution": "bundler", // how modules are resolved
"lib": ["ES2022", "DOM"], // available type definitions
"outDir": "./dist", // output directory for compiled JS
"rootDir": "./src", // root of source files
"sourceMap": true, // generate .map files for debugging
"declaration": true, // generate .d.ts files (for libraries)
"removeComments": true, // strip comments from output
// Type checking strictness
"strict": true, // enable ALL strict checks (recommended)
"noImplicitAny": true, // error on implicit 'any'
"strictNullChecks": true, // null/undefined are separate types
"noUnusedLocals": true, // error on unused local variables
"noUnusedParameters": true,// error on unused function params
"noImplicitReturns": true, // error if not all paths return
"noFallthroughCasesInSwitch": true,
"esModuleInterop": true, // allow default imports from CommonJS
"skipLibCheck": true, // skip type checking of .d.ts files
"forceConsistentCasingInFileNames": true
}
}Flags de Modo Estricto Explicados
El modo estricto es un bundle de flags de rigidez. strictNullChecks es el más impactante: hace null/undefined tipos distintos, forzándote a manejarlos explícitamente (la fuente #1 de crashes en runtime). noImplicitAny previene la erosión silenciosa de tipos. strictPropertyInitialization captura campos de clase no inicializados (usa ! para asignación definitiva o inicializa en constructor). Habilita siempre el modo estricto en proyectos nuevos: el coste inicial vale la seguridad.
{
"compilerOptions": {
// 'strict: true' enables ALL of these:
"strictNullChecks": true,
// null and undefined are NOT assignable to other types
// without explicit union. Forces null handling.
let x: string = null; // ERROR (without this, it's allowed)
"noImplicitAny": true,
// Parameters/variables can't be implicitly 'any'
function fn(x) { } // ERROR: x is implicitly any
"strictFunctionTypes": true,
// Function parameter types checked contravariantly
// (catches unsafe function assignments)
"strictBindCallApply": true,
// bind/call/apply are type-checked
"strictPropertyInitialization": true,
// Class properties must be initialized or declared with !
class User {
name: string; // ERROR: not initialized
name2!: string; // OK: definite assignment assertion
}
"noImplicitThis": true,
// 'this' must have a known type (no implicit any)
"alwaysStrict": true
// Emit "use strict" in every file
}
}Estrategias de Resolución de Módulos
moduleResolution controla cómo se resuelven las rutas de import. 'node' es la estrategia clásica; 'bundler' (TS 5.0+) coincide con bundlers modernos como Vite y soporta exports de package.json. 'nodenext' es ESM estricto (requiere extensiones). paths permite crear alias de import (@/ → src/), que deben reflejarse en la config de tu bundler (por ejemplo, resolve.alias de Vite). baseUrl + paths es la forma estándar de evitar imports relativos profundos (../../../).
{
"compilerOptions": {
// How TS resolves import paths
"moduleResolution": "node", // classic Node.js resolution
// Looks for: file.ts, file/index.ts, node_modules/file
"moduleResolution": "bundler", // for Vite/webpack/esbuild (TS 5.0+)
// Matches how bundlers resolve: supports import maps,
// conditional exports, no file extension requirement
"moduleResolution": "nodenext", // Node.js ESM resolution (strict)
// Requires file extensions in imports: import "./foo.js"
// Path mapping (aliases)
"baseUrl": ".",
"paths": {
"@/*": ["src/*"], // import "@/components/Button"
"@utils/*": ["src/utils/*"],
"@components": ["src/components/index.ts"]
}
// rootDirs: virtual directories that map to the same location
"rootDirs": ["src", "generated"],
// imports between src/ and generated/ resolve as if same dir
}
}
// In your code:
import { Button } from "@/components/Button";
import { formatDate } from "@utils/date";
// These resolve to src/components/Button.ts and src/utils/date.tsProject References (Monorepos)
Las project references dividen un codebase grande en subproyectos compilados independientemente: esencial para monorepos. Cada proyecto tiene composite: true y emite declaraciones. References declaran dependencias entre proyectos. tsc --build (-b) compila en orden de dependencias, solo recompilando lo que cambió. Esto acelera drásticamente el type-checking para codebases grandes e impone límites arquitectónicos entre paquetes.
// tsconfig.json (root — references sub-projects)
{
"files": [],
"references": [
{ "path": "./packages/shared" },
{ "path": "./packages/api" },
{ "path": "./packages/web" }
]
}
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true, // required for project references
"declaration": true, // must emit declarations
"outDir": "./dist",
"rootDir": "./src"
},
"include": ["src"]
}
// packages/api/tsconfig.json
{
"compilerOptions": { /* ... */ },
"references": [
{ "path": "../shared" } // depends on shared
]
}
// Benefits:
// 1. Faster builds — only rebuild changed projects
// 2. Clear dependency boundaries between packages
// 3. Type-checking is scoped per project
// 4. tsc --build handles the build order automatically
// Build command: tsc --build (or tsc -b)Recetas Comunes de tsconfig
Diferentes tipos de proyecto necesitan diferentes ajustes de tsconfig. React/Vite usa jsx: 'react-jsx' y noEmit (Vite compila). Node.js usa CommonJS (o NodeNext para ESM) y types: ['node']. Las librerías necesitan declaration: true para salida .d.ts y un target más bajo para mayor compatibilidad. isolatedModules es requerido por Vite/esbuild (cada archivo debe ser compilable independientemente). Excluye siempre archivos de test y node_modules del build.
// React + Vite project
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx", // React 17+ JSX transform
"lib": ["ES2022", "DOM", "DOM.Iterable"],
"strict": true,
"noEmit": true, // Vite handles compilation
"isolatedModules": true, // required by Vite/esbuild
"verbatimModuleSyntax": true
},
"include": ["src"]
}
// Node.js backend project
{
"compilerOptions": {
"target": "ES2022",
"module": "CommonJS", // or NodeNext for ESM
"moduleResolution": "node",
"lib": ["ES2022"],
"types": ["node"], // @types/node
"outDir": "./dist",
"strict": true,
"esModuleInterop": true
},
"include": ["src"]
}
// Library project (publishes to npm)
{
"compilerOptions": {
"target": "ES2020", // broader compatibility
"module": "ESNext",
"declaration": true, // emit .d.ts
"declarationMap": true,
"outDir": "./dist",
"strict": true
},
"include": ["src"],
"exclude": ["node_modules", "dist", "**/*.test.ts"]
}Decoradores
Decorador de Clase
Los decoradores de clase reciben el constructor y pueden devolver una clase modificada. Son experimentales (requieren experimentalDecorators: true). Comunes en NestJS y TypeORM.
function logged<T extends new (...args: any[]) => any>(con: T) {
return class extends con {
created = new Date().toISOString();
};
}
@logged
class Service { constructor(public name: string) {} }Decorador de Método
Los decoradores de método reciben (target, key, descriptor). Envolver descriptor.value habilita logging, caché, validación. Así es como funcionan los interceptores de NestJS.
function log(target: any, key: string, desc: PropertyDescriptor) {
const orig = desc.value;
desc.value = function(...args: any[]) {
console.log(`Calling ${key}`, args);
return orig.apply(this, args);
};
}
class Calc { @log add(a: number, b: number) { return a + b; } }Decorador de Propiedad
Los decoradores de propiedad reciben (target, key). Usar Object.defineProperty crea getters/setters para validación. Usado en class-validator para validación de DTO.
function required(target: any, key: string) {
let val: any;
Object.defineProperty(target, key, {
get() { return val; },
set(v: any) { if (v == null) throw new Error(`${key} required`); val = v; }
});
}Decorador de Parámetro
Los decoradores de parámetro reciben (target, methodKey, parameterIndex). Usados con metadata reflection para validación. class-validator y NestJS usan esto.
function Min(min: number) {
return (target: any, key: string, idx: number) => {
console.log(`${key} param ${idx} >= ${min}`);
};
}
class Order { create(@Min(0) qty: number) { return qty; } }Decorator Factory
Las fábricas de decoradores devuelven una función decoradora, permitiendo configuración. La función externa recibe parámetros, la interna es el decorador real.
function Retry(times: number) {
return function(target: any, key: string, desc: PropertyDescriptor) {
const orig = desc.value;
desc.value = async function(...args: any[]) {
for (let i = 0; i < times; i++)
try { return await orig.apply(this, args); }
catch (e) { if (i === times - 1) throw e; }
};
};
}Module Augmentation
Augment Built-in Types
La augmentación de módulos extiende tipos existentes. declare global permite augmentar tipos integrados como Array. La implementación en tiempo de ejecución también debe proporcionarse.
declare global {
interface Array<T> {
last(): T | undefined;
chunk(size: number): T[][];
}
}
Array.prototype.last = function() { return this[this.length - 1]; };Augment Library Types
La augmentación de módulos extiende tipos de bibliotecas de terceros. declare module reabre el tipo del módulo. Esencial para añadir propiedades personalizadas a objetos de frameworks.
declare module 'express' {
interface Request {
user?: { id: string; role: string };
}
}
app.get('/profile', (req, res) => {
const userId = req.user?.id; // Typed!
});Augment Window
Aumentar Window añade propiedades globales personalizadas con seguridad de tipos. Útil para exponer el estado de la aplicación a herramientas de depuración o analítica.
declare global {
interface Window {
myApp: { init: () => void; version: string };
}
}
window.myApp = { init: () => console.log('Ready'), version: '1.0.0' };CSS Modules
CSS Modules necesita declaraciones de tipos. La declaración mapea importaciones .module.css a un registro de nombres de clases. Habilita el autocompletado para referencias de clases CSS.
declare module '*.module.css' {
const classes: { readonly [key: string]: string };
export default classes;
}
import styles from './Button.module.css';
<button className={styles.button} />Vue Plugin
Vue y otros frameworks usan la augmentación de módulos para el tipado de plugins. ComponentCustomProperties añade propiedades de instancia. Habilita plugins con seguridad de tipos.
declare module 'vue' {
interface ComponentCustomProperties {
$auth: { login: () => Promise<void> };
}
}
export default defineComponent({
methods: { async login() { await this.$auth.login(); } }
});Declaration Merging
Merging Interfaces
Las interfaces con el mismo nombre se fusionan automáticamente. Todos los miembros pasan a formar parte de una sola interfaz. Útil para dividir interfaces entre archivos.
interface User { name: string; }
interface User { age: number; }
interface User { email: string; }
const user: User = { name: 'Alice', age: 30, email: '[email protected]' };Merging Namespaces
Los namespaces con el mismo nombre fusionan sus exportaciones. Esto permite dividir el contenido del namespace entre archivos. Se prefieren los módulos ES para código nuevo.
namespace Utils {
export function log(msg: string) { console.log(msg); }
}
namespace Utils {
export function warn(msg: string) { console.warn(msg); }
}
Utils.log('info'); Utils.warn('alert');Namespace with Function
Los namespaces pueden fusionarse con funciones, clases y enums. El namespace añade propiedades estáticas a la función. Usado en Moment.js y bibliotecas similares.
function Counter() { Counter.count++; }
namespace Counter {
export let count = 0;
export function reset() { count = 0; }
}
Counter(); Counter();
console.log(Counter.count); // 2Merging with Classes
Fusionar un namespace con una clase añade miembros estáticos y tipos anidados. El namespace puede exportar interfaces que se convierten en tipos anidados.
class Settings { static defaults = { theme: 'light' }; }
namespace Settings {
export interface Options { theme: string; lang: string; }
}
const opts: Settings.Options = { theme: 'dark', lang: 'en' };Disallowed Merges
Las clases no pueden fusionarse con otras clases. Las variables no pueden fusionarse. Las funciones se fusionan como sobrecargas. Los enums pueden fusionarse con namespaces.
// Cannot merge classes
class A { x = 1; }
class A { y = 2; } // Error
// Function overloads (allowed)
function fn(x: string): string;
function fn(x: number): number;
function fn(x: any): any { return x; }Type Narrowing
typeof & instanceof
typeof estrecha tipos primitivos. instanceof estrecha tipos de clase. TypeScript entiende estas comprobaciones y estrecha el tipo en cada rama.
function process(value: string | number | Date) {
if (typeof value === 'string') return value.toUpperCase();
if (typeof value === 'number') return value.toFixed(2);
if (value instanceof Date) return value.toISOString();
}in Operator
El operador in comprueba si existe una propiedad, estrechando el tipo. Útil para uniones discriminadas con diferentes nombres de propiedades.
interface Cat { meow(): void; }
interface Dog { bark(): void; }
function speak(animal: Cat | Dog) {
if ('meow' in animal) animal.meow();
else animal.bark();
}Discriminated Unions
Las uniones discriminadas usan una propiedad común (discriminante) para estrechar tipos. switch sobre el discriminante para comprobación exhaustiva. El patrón más seguro para tipos variantes.
type Result =
| { status: 'success'; data: string }
| { status: 'error'; message: string };
function handle(r: Result) {
switch (r.status) {
case 'success': console.log(r.data); break;
case 'error': console.log(r.message); break;
}
}Type Predicates
Los predicados de tipo (x is T) habilitan funciones de estrechamiento personalizadas. Retornar true estrecha a T, false estrecha al tipo excluido. TypeScript confía ciegamente en el predicado.
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Fish | Bird) {
if (isFish(pet)) pet.swim();
else pet.fly();
}Assertion Functions
Las funciones de aserción lanzan excepciones si la condición falla, estrechando el tipo para el código subsiguiente. asserts x is T estrecha a T. Elimina comprobaciones de null redundantes.
function assertDefined<T>(value: T | null | undefined): asserts value is T {
if (value == null) throw new Error('Null or undefined');
}
function processUser(user?: User) {
assertDefined(user);
console.log(user.name); // User (not undefined)
}Template Literal Types
Basic Template Literals
Los tipos de template literal crean patrones de cadena. Restringen las cadenas para coincidir con una plantilla. Habilita patrones con seguridad de tipos para endpoints de API y nombres de eventos.
type Greeting = `hello ${string}`;
const g: Greeting = 'hello world'; // OK
type Endpoint = `${'GET' | 'POST'} /api/${string}`;
const ep: Endpoint = 'GET /api/users';Uppercase & Lowercase
Los tipos intrínsecos integrados transforman tipos de literal de cadena. Se combinan con template literals para generar nombres de eventos y constantes con seguridad de tipos.
type Upper = Uppercase<'hello'>; // 'HELLO'
type Lower = Lowercase<'WORLD'>; // 'world'
type Cap = Capitalize<'foo'>; // 'Foo'
type EventName = `on${Capitalize<'click'>}`; // 'onClick'Key Remapping
El remapeo de claves (cláusula as) transforma las claves durante los tipos mapeados. Genera nombres getter/setter a partir de nombres de propiedades. Crea APIs con seguridad de tipos a partir de interfaces.
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}>`]: () => T[K];
};
interface Person { name: string; age: number; }
type PersonGetters = Getters<Person>;
// { getName: () => string; getAge: () => number; }String Pattern Matching
Los tipos de template literal con infer pueden analizar cadenas en tiempo de compilación. Split divide una cadena en una tupla. Habilita manipulación de cadenas con seguridad de tipos.
type Split<S extends string, D extends string> =
S extends `${infer L}${D}${infer R}` ? [L, ...Split<R, D>] : [S];
type Parts = Split<'a,b,c', ','>; // ['a', 'b', 'c']Event System Typing
Los tipos de template literal con genéricos crean sistemas de eventos totalmente con seguridad de tipos. El nombre del evento determina el tipo del payload. on y emit hacen cumplir tipos coincidentes.
type EventMap = { click: { x: number }; submit: { value: string } };
class Emitter {
on<K extends keyof EventMap>(event: K, handler: (e: EventMap[K]) => void) {}
emit<K extends keyof EventMap>(event: K, data: EventMap[K]) {}
}
em.on('click', e => console.log(e.x)); // Typed!infer Keyword
Extract Return Type
infer declara una variable de tipo dentro de un tipo condicional. Captura el tipo en una posición específica. ReturnType es el equivalente integrado.
type ReturnOf<T> = T extends (...args: any[]) => infer R ? R : never;
function getUser() { return { name: 'Alice', age: 30 }; }
type User = ReturnOf<typeof getUser>; // { name: string; age: number; }Extract Promise Type
infer extrae el tipo interno de un Promise. DeepUnwrap desenvuelve recursivamente los Promises anidados. El Awaited<T> integrado hace esto en TypeScript moderno.
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type R = UnwrapPromise<Promise<string>>; // string
type DeepUnwrap<T> = T extends Promise<infer U> ? DeepUnwrap<U> : T;
type D = DeepUnwrap<Promise<Promise<boolean>>>; // booleanExtract Array Element
infer E captura el tipo de elemento de un array. Para tuplas, infer puede capturar posiciones específicas. Útil para trabajar con colecciones genéricas.
type ElementOf<T> = T extends (infer E)[] ? E : never;
type T1 = ElementOf<string[]>; // string
type First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;
type F = First<[string, number]>; // stringExtract Function Parameters
infer P captura la tupla de parámetros de una función. Parameters es el equivalente integrado. Útil para envolver funciones preservando los tipos.
type Params<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number) { return ''; }
type P = Params<typeof greet>; // [string, number]Multiple infer
Múltiples variables infer pueden capturar diferentes partes de un tipo simultáneamente. Habilita transformaciones de tipo complejas en un solo condicional.
type FirstLast<T extends any[]> =
T extends [infer First, ...any[], infer Last]
? { first: First; last: Last } : never;
type R = FirstLast<[1, 2, 3, 4]>; // { first: 1; last: 4 }Variance
Covariance
La covarianza permite que Dog[] se asigne a Animal[]. Los arrays de TypeScript son covariantes pero esto es insound: empujar un Animal en un Dog[] corrompe el array.
class Animal { name: string; }
class Dog extends Animal { breed: string; }
let dogs: Dog[] = [new Dog()];
let animals: Animal[] = dogs; // OK (covariant)
// animals.push(new Animal()); // Unsafe at runtimeContravariance
La contravarianza significa que una función que acepta Dog puede usarse donde se espera una función que acepta Animal. Seguro porque un manejador de Dog maneja cualquier Animal que sea un Dog.
type Handler<T> = (arg: T) => void;
let dogHandler: Handler<Dog> = (d) => console.log(d.breed);
let animalHandler: Handler<Animal> = dogHandler; // OK with strictFunctionTypesBivariance
La sintaxis de método es bivariante. La sintaxis de propiedad de función es contravariante con strictFunctionTypes. Los métodos son bivariantes para compatibilidad OO.
interface IFace {
method(x: Animal): void; // bivariant
fn: (x: Animal) => void; // contravariant
}
class Impl implements IFace {
method(x: Dog) {} // OK (bivariant)
fn = (x: Dog) => {} // Error (contravariant)
}in/out Variance
TypeScript 4.7+ soporta anotaciones de varianza explícitas. in marca contravariante (consumidores), out marca covariante (productores), in out marca invariante (ambos).
interface Producer<out T> { produce(): T; }
interface Consumer<in T> { consume(value: T): void; }
interface Buffer<in out T> { read(): T; write(value: T): void; }Invariant Types
Los tipos invariantes requieren coincidencias exactas de tipo. Un tipo es invariante cuando aparece en posiciones de entrada y salida. Box<Dog> no puede asignarse a Box<Animal>.
interface Box<T> { get(): T; set(value: T): void; }
let dogBox: Box<Dog> = {} as any;
let animalBox: Box<Animal> = dogBox; // Error: invariantBuilder Pattern
Fluent Builder
El patrón builder construye objetos complejos paso a paso. Cada método retorna this para encadenamiento. Útil para consultas SQL, peticiones HTTP y configuración.
class QueryBuilder {
private parts: string[] = [];
select(cols: string): this { this.parts.push(`SELECT ${cols}`); return this; }
from(table: string): this { this.parts.push(`FROM ${table}`); return this; }
where(cond: string): this { this.parts.push(`WHERE ${cond}`); return this; }
build(): string { return this.parts.join(' '); }
}Type-Safe Builder
Los builders con seguridad de tipos usan tipos condicionales para hacer cumplir los campos requeridos. build() solo retorna Person cuando hasName es true. Captura los campos faltantes en tiempo de compilación.
interface State { hasName: boolean; }
class Builder<S extends State> {
name(n: string): Builder<S & { hasName: true }> { return this as any; }
build(): S extends { hasName: true } ? Person : never { return {} as any; }
}Immutable Builder
Los builders inmutables crean una nueva instancia para cada modificación. El sistema de tipos rastrea todas las claves añadidas mediante tipos de intersección. Cada set retorna un nuevo tipo de builder.
class ImmutableBuilder<T extends object> {
constructor(private data: T) {}
set<K extends string, V>(key: K, value: V):
ImmutableBuilder<T & { [P in K]: V }> {
return new ImmutableBuilder({ ...this.data, [key]: value });
}
build(): T { return this.data; }
}Director Pattern
El Director encapsula secuencias de construcción comunes. Usa un builder para crear productos estándar. Diferentes directores producen diferentes variaciones.
class HTMLBuilder {
private html = '';
addTag(tag: string, content: string): this {
this.html += `<${tag}>${content}</${tag}>`; return this;
}
build(): string { return this.html; }
}
class Director {
buildPage(title: string, body: string): string {
return new HTMLBuilder().addTag('title', title).addTag('body', body).build();
}
}Step Builder
El step builder hace cumplir un orden específico de llamadas a métodos a través del sistema de tipos. Cada paso retorna un tipo diferente con solo el siguiente método disponible.
class Step1 { name(n: string): Step2 { return new Step2(n); } }
class Step2 {
constructor(private name: string) {}
age(a: number): Step3 { return new Step3(this.name, a); }
}
class Step3 {
constructor(private name: string, private age: number) {}
build() { return { name: this.name, age: this.age }; }
}Testing with TypeScript
Jest with TypeScript
Usa ts-jest o @swc/jest para pruebas de TypeScript. describe agrupa pruebas relacionadas, it define casos de prueba. expect crea aserciones con matchers como toBe, toEqual.
import { sum } from './sum';
describe('sum', () => {
it('adds two numbers', () => {
expect(sum(1, 2)).toBe(3);
});
it('handles negatives', () => {
expect(sum(-1, -2)).toBe(-3);
});
});Type Testing
expectTypeOf prueba tipos en tiempo de compilación. Verifica tipos de retorno, tipos de parámetros y tipos de promesas resueltas. Falla el build si los tipos son incorrectos.
import { expectTypeOf } from 'vitest';
type GetUser = (id: string) => Promise<{ name: string }>;
test('return type is correct', () => {
const fn = {} as unknown as GetUser;
expectTypeOf(fn).returns.toEqualTypeOf<{ name: string }>();
expectTypeOf(fn).parameters.toEqualTypeOf<[string]>();
});Mocking with Types
jest.Mocked<T> crea un mock tipado a partir de una interfaz. jest.fn() crea funciones mock con valores de retorno tipados. El mock está totalmente tipado.
interface Database { find(id: string): Promise<User>; }
const mockDb: jest.Mocked<Database> = {
find: jest.fn().mockResolvedValue({ id: '1', name: 'Alice' }),
};
mockDb.find.mockResolvedValueOnce({ id: '2', name: 'Bob' });Test Setup & Teardown
beforeAll se ejecuta una vez antes de todas las pruebas, afterAll una vez después. beforeEach se ejecuta antes de cada prueba, afterEach después de cada una. Úsalo para setup y limpieza.
describe('Database', () => {
beforeAll(async () => { db = createDatabase(); await db.connect(); });
afterAll(async () => { await db.disconnect(); });
beforeEach(async () => { await db.clear(); });
afterEach(() => { jest.restoreAllMocks(); });
});Property-Based Testing
Las pruebas basadas en propiedades generan entradas aleatorias para probar invariantes. fc.assert ejecuta la propiedad múltiples veces. Captura casos límite que las pruebas basadas en ejemplos pierden.
import { fc } from 'fast-check';
describe('sort', () => {
it('preserves length', () => {
fc.assert(fc.property(fc.array(fc.integer()), (arr) => {
return [...arr].sort().length === arr.length;
}));
});
});Common Pitfalls
any vs unknown
any desactiva la comprobación de tipos, ocultando bugs. unknown es seguro: debes estrecharlo antes de usarlo. Usa unknown para fuentes no confiables (API, JSON.parse).
// BAD: any disables type checking
function bad(data: any) { return data.foo.bar; }
// GOOD: unknown forces type checking
function good(data: unknown) {
if (typeof data === 'object' && data !== null && 'foo' in data)
return (data as any).foo;
}Excess Property Checks
TypeScript solo comprueba propiedades excesivas en literales de objeto asignados directamente. Mediante variable, la comprobación se omite. Usa zod para validación en tiempo de ejecución más estricta.
interface User { name: string; age: number; }
// Direct literal: checked
const u1: User = { name: 'A', age: 30, extra: true }; // Error
// Via variable: not checked
const data = { name: 'A', age: 30, extra: true };
const u2: User = data; // OKEnum vs Union
Los enums crean objetos en tiempo de ejecución con mapeo inverso. Los tipos de unión son de tiempo de ejecución cero y tree-shakeable. Prefiere tipos de unión para código nuevo.
// Enum: runtime object
enum Color { Red, Green, Blue }
// Union: no runtime
type Color2 = 'red' | 'green' | 'blue';
// Const enum: erased
const enum Dir { Up, Down }Structural Typing
TypeScript usa tipado estructural: los tipos son compatibles si las formas coinciden. Admin es asignable a User. Esto puede causar bugs lógicos. Los tipos branded añaden distinción nominal.
interface User { name: string; age: number; }
interface Admin { name: string; age: number; role: string; }
const admin: Admin = { name: 'Bob', age: 40, role: 'admin' };
const user: User = admin; // OK (structural)Type Assertion Dangers
Las aserciones de tipo (as) sobrescriben TypeScript sin comprobaciones en tiempo de ejecución. Usa validación en tiempo de ejecución (zod, io-ts) para datos externos. safeParse retorna un resultado sin lanzar excepciones.
// BAD: assertion hides errors
const user = JSON.parse(input) as User;
// GOOD: runtime validation
import { z } from 'zod';
const schema = z.object({ name: z.string() });
const result = schema.safeParse(JSON.parse(input));
if (result.success) { const user: User = result.data; }Fragmentos de TypeScript relacionados
Copy-paste ready code for common tasks.
Funciones Genéricas
Definir y usar funciones genéricas.
Tipos Condicionales
Seleccionar tipos basándose en condiciones.
Mapped Types
Construir nuevos tipos a partir de los existentes.
Tipos Utilitarios
Tipos utilitarios integrados de TypeScript.
Type Guards
Funciones personalizadas de type guard.
Sobrecarga de Funciones
Definir firmas de sobrecarga de funciones.
Decoradores
Decoradores de clase y método.
Enum
Enums numéricos, de cadena y const.
Herencia de Interfaces
Herencia e implementación de interfaces.
Clases Abstractas
Definir clases abstractas y métodos abstractos.
Namespaces
Organizar código usando namespaces.
Declaraciones de Módulo
Escribir declaraciones de tipo para librerías JS.
Fusión de Declaraciones
Fusionar múltiples declaraciones con el mismo nombre.
Optional Chaining
Acceder de forma segura a propiedades profundas.
Nullish Coalescing
Usar un valor por defecto solo para null/undefined.
Inferencia de Tipos
TypeScript infiere tipos automáticamente.
Aserciones const
Acotar tipos usando as const.
Operador satisfies
Verificación de tipos preservando el tipo más estrecho.
Palabra clave infer
Extraer tipos dentro de tipos condicionales.
Template Literal Types
Construir tipos basados en cadenas.
Was this helpful?