Que sont les types avancés de TypeScript ?
Les types avancés sont des outils qui vous permettent de transformer, composer et manipuler des types au niveau du système de types. Au lieu de définir manuellement des types parallèles pour chaque variation, vous les dérivez automatiquement à partir de types existants.
Si les bases vous apportent les types et que les génériques vous permettent d’écrire du code réutilisable, les types avancés vous permettent de construire un système de types qui décrit vos données avec précision — capturant ainsi des erreurs que des approches plus simples laisseraient passer.
Les trois piliers sont les utility types (transformations intégrées), les conditional types (logique au niveau du type) et les mapped types (transformation de propriétés). Ensemble, ils forment un véritable petit langage pour travailler avec les types.
Types utilitaires
TypeScript propose des types intégrés permettant de modifier d’autres types :
Partial et Required
interface User {
id: number;
name: string;
email: string;
}
// All properties optional
type UserUpdate = Partial<User>;
// { id?: number; name?: string; email?: string; }
// All properties required
type StrictUser = Required<Partial<User>>;
// { id: number; name: string; email: string; }
Pick et Omit
// Extract specific properties
type UserPreview = Pick<User, "id" | "name">;
// { id: number; name: string; }
// Remove specific properties
type UserWithoutEmail = Omit<User, "email">;
// { id: number; name: string; }
Record
// Create object type with specific keys and values
type Scores = Record<string, number>;
// { [key: string]: number }
const scores: Scores = { math: 95, science: 87 };
// With union keys
type StatusMap = Record<"idle" | "loading" | "done", string>;
Extract et Exclude
type Status = "idle" | "loading" | "success" | "error";
// Keep only matching members
type ActiveStatus = Extract<Status, "loading" | "success">;
// "loading" | "success"
// Remove matching members
type InactiveStatus = Exclude<Status, "loading" | "success">;
// "idle" | "error"
ReturnType et Parameters
function createUser(name: string, age: number) {
return { name, age, id: Date.now() };
}
type User = ReturnType<typeof createUser>;
// { name: string; age: number; id: number }
type Args = Parameters<typeof createUser>;
// [string, number]
NonNullable
type Maybe = string | null | undefined;
type Definite = NonNullable<Maybe>;
// string
Types mappés (Mapped types)
Les types mappés itèrent sur les clés d’un type existant pour transformer chaque propriété :
type ReadOnly<T> = {
readonly [K in keyof T]: T[K];
};
type MutableUser = ReadOnly<User>;
// All properties are readonly
Ajout de modificateurs
// Add optional
type Optional<T> = {
[K in keyof T]?: T[K];
};
// Remove optional
type Concrete<T> = {
[K in keyof T]-?: T[K];
};
// Remove readonly
type Mutable<T> = {
-readonly [K in keyof T]: T[K];
};
Les opérateurs - et + permettent de supprimer ou d’ajouter des modificateurs lors du mappage.
Remappage de clés avec as
TypeScript 4.1+ vous permet de remapper les clés pendant le mappage :
type Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
type UserGetters = Getters<User>;
// { getId: () => number; getName: () => string; getEmail: () => string; }
Types conditionnels
Les types conditionnels permettent d’exprimer une logique au niveau des types :
type IsString<T> = T extends string ? true : false;
type A = IsString<"hello">; // true
type B = IsString<42>; // false
Le mot-clé infer
infer permet d’extraire un type à partir d’un autre type :
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type Fn = () => string;
type Result = ReturnType<Fn>; // string
Exemples pratiques
// Extract array element type
type ElementOf<T> = T extends (infer E)[] ? E : never;
type Num = ElementOf<number[]>; // number
// Unwrap promises
type Awaited<T> = T extends Promise<infer U> ? Awaited<U> : T;
type Deep = Awaited<Promise<Promise<string>>>; // string
// Extract object property types
type ValuesOf<T> = T[keyof T];
type UserValues = ValuesOf<User>; // number | string
Types conditionnels distributifs
Lorsqu’un type conditionnel est appliqué à une union, chaque membre de celle-ci est vérifié séparément :
type ToArray<T> = T extends any ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] — not (string | number)[]
C’est particulièrement utile pour des types comme Extract et Exclude qui servent à filtrer les membres d’une union.
Les types Template Literal
Les types Template Literal permettent de manipuler des chaînes de caractères au niveau du typage :
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type FocusEvent = EventName<"focus">; // "onFocus"
Avec les mapped types
type EventHandlers<T extends string> = {
[K in T as `on${Capitalize<K>}`]: () => void;
};
type MouseEvents = EventHandlers<"click" | "hover" | "focus">;
// { onClick: () => void; onHover: () => void; onFocus: () => void; }
Pattern matching de chaînes de caractères
type CSSProperty = `${string}-${string}`;
type ValidCSS = CSSProperty; // any string with a hyphen
Unions discriminées
Les unions discriminées sont le pattern avancé le plus important pour modéliser un état :
type RequestState<T> =
| { status: "idle" }
| { status: "loading" }
| { status: "success"; data: T }
| { status: "error"; error: string };
function handleState(state: RequestState<User>) {
switch (state.status) {
case "idle":
return "Ready";
case "loading":
return "Loading...";
case "success":
return state.data.name; // TypeScript knows data exists
case "error":
return state.error; // TypeScript knows error exists
}
}
La propriété status est le discriminant. TypeScript affine le type automatiquement dans chaque branche.
Types brandés (Branded types)
Les types brandés permettent d’éviter de mélanger des types qui possèdent la même structure :
type UserId = string & { readonly __brand: unique symbol };
type OrderId = string & { readonly __brand: unique symbol };
function branded<T extends string>(value: T): T & { readonly __brand: unique symbol } {
return value as any;
}
const userId = branded<UserId>("user-123");
const orderId = branded<OrderId>("order-456");
function getUser(id: UserId) { /* ... */ }
getUser(userId); // OK
getUser(orderId); // Error: OrderId is not assignable to UserId
Bonnes pratiques
- Utilisez des utility types plutôt que de dupliquer manuellement les définitions de types.
- Privilégiez les discriminated unions aux type assertions pour la modélisation d’états.
- Construisez vos types à partir de petits éléments composables plutôt que de définitions monolithiques volumineuses.
- Utilisez les conditional types avec parcimonie — si un type est difficile à lire, simplifiez l’API.
- Utilisez
inferpour extraire les types des bibliothèques au lieu de typer manuellement les valeurs de retour. - Maintenez une source de vérité unique et dérivez tout le reste.
- Utilisez des branded types lorsque vous avez besoin d’un typage nominal pour des IDs ou des valeurs similaires.
- Considérez les types complexes comme un dernier recours — les types simples sont plus faciles à maintenir.
Erreurs courantes
- Sur-concevoir les types alors que des solutions plus simples suffiraient.
- Créer des types conditionnels profondément imbriqués et impossibles à déboguer.
- Utiliser des assertions
asau lieu d’un affinement de type (type narrowing) approprié. - Ne pas utiliser les types utilitaires, ce qui entraîne des duplications de définitions de types.
- Rendre les types trop complexes par rapport au bénéfice qu’ils apportent.
- Oublier que les types sont supprimés à l’exécution — il n’y a pas de vérification de type au runtime.
- Ne pas documenter les types complexes avec des commentaires expliquant l’intention.
Quelle est la suite ?
Vous maîtrisez désormais les types avancés : les utility types, les types conditionnels, les mapped types et les template literals. La prochaine étape logique est de se plonger dans les fondamentaux de TypeScript — les annotations, l’inférence, les interfaces et les génériques. Choisissez un projet, refactorisez quelques types et laissez la pratique porter ses fruits.