Kodokon kodokon.com

Patterns de production : branded types, satisfies et .d.ts

Appliquez les patterns experts qui sécurisent une base de code réelle : nominalité simulée, validation sans élargissement et déclarations ambiantes.

11 min · 3 questions

Ouvrir cette leçon dans Kodokon

Le typage de TypeScript est structurel : deux types de même forme sont interchangeables, si bien qu'un string d'identifiant utilisateur se confond avec n'importe quel autre string. Le branding simule un typage nominal en intersectant le type de base avec une propriété fantôme dont la clé est un unique symbol : aucune valeur réelle ne la possède, mais le compilateur, lui, distingue désormais chaque marque.

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"); // erreur : string nu refuse
Une nominalité simulée, sans aucun coût à l'exécution.

L'opérateur satisfies (TypeScript 4.9) comble un vide précis. Une annotation : T élargit le type de la variable vers T et fait perdre les littéraux inférés ; un as T désactive une partie des vérifications. satisfies T vérifie la conformité de l'expression à T sans toucher au type inféré, qui reste le plus précis possible. C'est l'outil idéal pour les objets de configuration : validation complète, précision intacte.

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

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

routes.admin.auth;
// type litteral true : la precision inferee est
// conservee, et une cle inconnue serait refusee
satisfies : vérification stricte sans élargissement.

Un fichier .d.ts contient exclusivement des déclarations ambiantes : il décrit des valeurs qui existeront à l'exécution, sans émettre le moindre JavaScript. Pour étendre le scope global depuis un module, le bloc declare global est obligatoire - et la spécification ne l'autorise que dans un fichier qui est effectivement un module, d'où l'idiome export {} en tête de fichier.

TYPESCRIPT
export {};

declare global {
  interface Window {
    analytics: { track(event: string): void };
  }
}
env.d.ts : le export {} rend le fichier module.

Pour une dépendance JavaScript sans types publiés, declare module crée un module ambiant : le compilateur utilisera votre déclaration à chaque import du paquet. Attention à la portée de skipLibCheck : activé, il ignore les erreurs de tous les fichiers .d.ts, y compris les vôtres ; vérifiez donc vos déclarations via des tests de types placés dans des fichiers .ts ordinaires.

TYPESCRIPT
declare module "legacy-lib" {
  export interface InitOptions {
    debug?: boolean;
  }
  export function init(options?: InitOptions): void;
}
legacy-lib.d.ts : typer un paquet sans types publiés.

Quiz de validation

Vérifiez que vous avez bien retenu les points clés de cette leçon.

  1. Quelle est la différence entre satisfies T et une annotation : T ?
    • satisfies élargit le type de l'expression vers T
    • satisfies vérifie la conformité à T tout en conservant le type inféré, plus précis
    • satisfies désactive les vérifications, comme as
    • Aucune : les deux écritures sont équivalentes
  2. À l'exécution, que contient une valeur brandée construite avec Brand<string, ...> ?
    • Une chaîne accompagnée d'une propriété symbole
    • Une simple chaîne : la marque n'existe qu'à la compilation
    • Un objet qui enveloppe la chaîne d'origine
  3. Pourquoi ajouter export {} en tête d'un fichier .d.ts qui utilise declare global ?
    • Pour exporter les types globaux vers les autres fichiers
    • Pour transformer le fichier en module, condition exigée par declare global
    • Pour activer automatiquement le mode strict
    • C'est purement décoratif