Introduction : la magie cachée dans les types
\n\nImaginez que vous n'écrivez pas simplement du code, mais un ensemble d'instructions pour le compilateur lui-même, qui, en fonction de vos règles, génère des constructions sûres et auto-documentées. C'est cela, la métaprogrammation en TypeScript — la capacité d'écrire du code qui fonctionne au niveau des types, analyse et transforme d'autres types lors de la compilation.
\n\nDe nombreux développeurs s'arrêtent aux génériques et interfaces de base, sans même soupçonner l'arsenal puissant qui se cache derrière les mots-clés infer, extends et les constructions conditionnelles. Dans cet article, nous allons explorer les techniques avancées de métaprogrammation TypeScript qui vous feront passer d'un simple utilisateur du langage à un véritable architecte.
1. Types conditionnels : le if au niveau des types
\n\nLes types conditionnels sont la base de toute programmation au niveau des types. Ils fonctionnent comme un opérateur ternaire, mais au niveau du système de types :
\n\ntype IsString<T> = T extends string ? "yes" : "no";\n\ntype Test1 = IsString<"hello">; // "yes"\ntype Test2 = IsString<42>; // "no"\n\n\nMais la véritable magie commence lorsque nous les combinons avec des génériques. Par exemple, créons un type qui extrait uniquement les clés de type chaîne d'un objet :
\n\ntype StringKeys<T> = {\n [K in keyof T]: T[K] extends string ? K : never\n}[keyof T];\n\ninterface User {\n name: string;\n age: number;\n email: string;\n}\n\ntype UserStringKeys = StringKeys<User>; // "name" | "email"\n\n\nCet exemple montre comment les types avancés TypeScript permettent de filtrer et transformer des structures de données à l'étape de compilation. Vous programmez littéralement une logique qui s'exécute avant que le code n'atteigne l'exécution.
\n\n2. Infer : extraire des types à partir d'autres types
\n\nLe mot-clé infer est sans doute l'outil le plus puissant de l'arsenal de la métaprogrammation TypeScript. Il permet de "regarder à l'intérieur" d'un type et d'en extraire ses parties constitutives. Prenons un exemple classique : obtenir le type de retour d'une fonction :
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;\n\ntype Fn = (x: number) => string;\ntype FnReturn = ReturnType<Fn>; // string\n\n\nMais infer peut faire bien plus. Extrayons le type du premier argument d'une fonction :
type FirstArg<T> = T extends (first: infer F, ...rest: any[]) => any ? F : never;\n\ntype Handler = (id: number, name: string) => void;\ntype HandlerFirstArg = FirstArg<Handler>; // number\n\n\nEt que diriez-vous d'une analyse récursive des promesses ? Créons un type qui "déroule" toutes les promesses imbriquées :
\n\ntype UnwrapPromise<T> = T extends Promise<infer U> ? UnwrapPromise<U> : T;\n\ntype DeepPromise = Promise<Promise<Promise<number>>>;\ntype Unwrapped = UnwrapPromise<DeepPromise>; // number\n\n\nRemarquez la récursion — c'est l'une des techniques avancées TypeScript les plus élégantes, permettant de traiter des structures infiniment imbriquées.
\n\n3. Mapped types : transformation d'objets
\n\nLes Mapped types permettent de créer de nouveaux types à partir de types existants en parcourant leurs clés. C'est un outil indispensable pour créer des utilitaires génériques :
\n\ntype Readonly<T> = {\n readonly [K in keyof T]: T[K];\n};\n\ntype Partial<T> = {\n [K in keyof T]?: T[K];\n};\n\n\nMais nous pouvons aller plus loin. Créons un type qui rend toutes les propriétés optionnelles, mais uniquement si ce sont des chaînes :
\n\ntype PartialStringProps<T> = {\n [K in keyof T]: T[K] extends string ? T[K] | undefined : T[K];\n};\n\ninterface Config {\n nam