Kodokon kodokon.com

Génériques avancés : contraintes et valeurs par défaut

Vous contraignez vos paramètres de type avec extends et fournissez des valeurs par défaut pour des API génériques ergonomiques.

10 min · 3 questions

Ouvrir cette leçon dans Kodokon

Un générique sans contrainte est opaque : dans le corps de la fonction, T ne vous autorise rien. La clause extends établit un contrat minimal sur le paramètre de type, ce qui débloque l'accès aux propriétés garanties tout en préservant le type précis de l'appelant. C'est la différence fondamentale avec un paramètre typé par une interface : la contrainte conserve l'information, l'interface l'efface.

TYPESCRIPT
function longest<T extends { length: number }>(
  a: T,
  b: T
): T {
  return a.length >= b.length ? a : b;
}

const word = longest("hello", "worldwide");
const list = longest([1, 2], [3, 4, 5]);
word reste de type string, list de type number[].

Le duo K extends keyof T est le pattern le plus rentable du quotidien : il lie deux paramètres de type entre eux et garantit statiquement qu'une clé appartient bien à l'objet. Le type de retour T[K] (un type indexé) suit précisément la propriété demandée. C'est exactement ainsi que sont typées les fonctions comme pick ou pluck des bibliothèques utilitaires.

TYPESCRIPT
function getProp<T extends object, K extends keyof T>(
  obj: T,
  key: K
): T[K] {
  return obj[key];
}

const config = { port: 8080, host: "localhost" };
const port = getProp(config, "port");
const host = getProp(config, "host");
port est inféré number, host string ; toute autre clé refuse.

Les valeurs par défaut (T = string) rendent un générique optionnel à l'appel : l'utilisateur ne fournit l'argument de type que lorsqu'il veut dévier du cas courant. Combinées à une contrainte (T extends X = Y), elles définissent à la fois la borne et le comportement implicite. Le défaut doit satisfaire la contrainte, sinon la déclaration elle-même ne compile pas. C'est le pattern standard des clients HTTP, des stores et des event emitters typés.

TYPESCRIPT
interface ApiResponse<T = unknown> {
  status: number;
  data: T;
}

async function fetchJson<T = unknown>(
  url: string
): Promise<ApiResponse<T>> {
  const res = await fetch(url);
  return { status: res.status, data: await res.json() };
}

interface Post { id: number; title: string }
const typed = await fetchJson<Post>("/api/posts/1");
const loose = await fetchJson("/api/health");
Sans argument de type, data retombe sur unknown, pas any.

Quiz de validation

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

  1. Que garantit la contrainte K extends keyof T dans une signature générique ?
    • Que K est une chaîne de caractères quelconque
    • Que K est une clé existante de T, vérifiée à la compilation
    • Que T contient au moins une clé optionnelle
    • Que K et T sont du même type
  2. Quel avantage longest<T extends { length: number }> a-t-il sur longest(a: { length: number }, b: ...) sans générique ?
    • Il s'exécute plus vite
    • Il préserve le type précis de l'appelant dans le retour
    • Il accepte plus de types en entrée
  3. Pourquoi préférer <T = unknown> à <T = any> comme valeur par défaut ?
    • unknown est plus rapide à compiler que any
    • any est interdit en mode strict
    • unknown oblige l'appelant à rétrécir le type avant usage, any désactive toute vérification
    • unknown accepte moins de valeurs que any