Kodokon kodokon.com

Patrones de producción: branded types, satisfies y .d.ts

Aplica los patrones de experto que aseguran una base de código real: nominalidad simulada, validación sin ampliación y declaraciones ambientales.

11 min · 3 preguntas

Abrir esta lección en Kodokon

El tipado de TypeScript es estructural: dos tipos con la misma forma son intercambiables, por lo que un string de id de usuario es indistinguible de cualquier otro string. El branding simula el tipado nominal intersecando el tipo base con una propiedad fantasma cuya clave es un unique symbol: ningún valor real la tiene nunca, pero el compilador ahora distingue cada marca.

TYPESCRIPT
declare const brand: unique symbol;

type Brand<T, Name extends string> =
  T & { readonly [brand]: Name };

type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;

function asUserId(raw: string): UserId {
  return raw as UserId;
}

declare function loadUser(id: UserId): void;

loadUser(asUserId("u_42")); // OK
// loadUser("u_42"); // error: bare string rejected
Nominalidad simulada, con coste cero en tiempo de ejecución.

El operador satisfies (TypeScript 4.9) cubre una carencia precisa. Una anotación : T amplía el tipo de la variable a T y pierde los literales inferidos; un as T desactiva parte de las comprobaciones. satisfies T verifica que la expresión se ajusta a T sin tocar el tipo inferido, que se mantiene tan preciso como sea posible. Es la herramienta ideal para objetos de configuración: validación completa, precisión intacta.

TYPESCRIPT
type RouteDef = { path: string; auth?: boolean };

const routes = {
  home: { path: "/" },
  admin: { path: "/admin", auth: true },
} satisfies Record<string, RouteDef>;

routes.admin.auth;
// literal type true: the inferred precision is
// preserved, and an unknown key would be rejected
satisfies: comprobación estricta sin ampliación.

Un archivo .d.ts contiene declaraciones ambientales exclusivamente: describe valores que existirán en tiempo de ejecución, sin emitir una sola línea de JavaScript. Para extender el ámbito global desde un módulo, el bloque declare global es obligatorio - y la especificación solo lo permite en un archivo que realmente sea un módulo, de ahí el idioma export {} al principio del archivo.

TYPESCRIPT
export {};

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}
env.d.ts: el export {} convierte el archivo en un módulo.

Para una dependencia de JavaScript sin tipos publicados, declare module crea un módulo ambiental: el compilador usará tu declaración para cada importación del paquete. Cuidado con el alcance de skipLibCheck: cuando está activado, ignora los errores de todos los archivos .d.ts, incluidos los tuyos; así que verifica tus declaraciones mediante tests de tipos colocados en archivos .ts ordinarios.

TYPESCRIPT
declare module "legacy-lib" {
  export interface InitOptions {
    debug?: boolean;
  }
  export function init(options?: InitOptions): void;
}
legacy-lib.d.ts: tipar un paquete sin tipos publicados.

Prueba de conocimientos

Comprueba que has retenido los puntos clave de esta lección.

  1. ¿Cuál es la diferencia entre satisfies T y una anotación : T?
    • satisfies amplía el tipo de la expresión a T
    • satisfies comprueba la conformidad con T conservando el tipo inferido más preciso
    • satisfies desactiva las comprobaciones, como as
    • Ninguna: las dos formas son equivalentes
  2. En tiempo de ejecución, ¿qué contiene un valor con marca construido con Brand<string, ...>?
    • Un string junto con una propiedad de tipo symbol
    • Un string simple: la marca solo existe en tiempo de compilación
    • Un objeto que envuelve el string original
  3. ¿Por qué añadir export {} al principio de un archivo .d.ts que usa declare global?
    • Para exportar los tipos globales a otros archivos
    • Para convertir el archivo en un módulo, condición requerida por declare global
    • Para activar automáticamente el modo estricto
    • Es puramente decorativo