04 - Template literal types : manipuler des strings dans les types
Ce que tu vas apprendre
- La syntaxe des template literal types et comment ils construisent des unions
- Les utility types de strings : Uppercase, Lowercase, Capitalize, Uncapitalize
- Comment typer des routes, des event names, des clés CSS
- Les combinaisons avec
inferpour parser des strings au niveau des types
Prerequisites
Avoir lu les articles sur les conditional types et les mapped types.
Des strings typees
En JavaScript, les template literals construisent des strings :
typescriptconst route = `/api/users/${userId}`
En TypeScript, les template literal types construisent des types string :
typescripttype Route = `/api/users/${string}`
const a: Route = "/api/users/123" // ✅
const b: Route = "/api/users/abc" // ✅
const c: Route = "/api/products/123" // ❌
Le type Route accepte toute string qui commence par /api/users/ suivi de n'importe quoi. C'est plus precis que string et le compilateur vérifié le prefixe.
Combinaisons et produit cartesien
Quand tu utilises une union dans un template literal, TypeScript généré toutes les combinaisons :
typescripttype Color = "red" | "green" | "blue"
type Size = "sm" | "md" | "lg"
type ClassName = `${Color}-${Size}`
// "red-sm" | "red-md" | "red-lg" | "green-sm" | "green-md" | "green-lg" | "blue-sm" | "blue-md" | "blue-lg"
9 valeurs générées automatiquement à partir de 3 x 3. C'est un produit cartesien au niveau des types.
Ca fonctionne avec plus de deux unions :
typescripttype Variant = "primary" | "secondary"
type State = "default" | "hover" | "active"
type Token = `${Variant}-${State}`
// 6 combinaisons
Utility types de strings
TypeScript fournit quatre utility types pour transformer les strings :
typescripttype A = Uppercase<"hello"> // "HELLO"
type B = Lowercase<"HELLO"> // "hello"
type C = Capitalize<"hello"> // "Hello"
type D = Uncapitalize<"Hello"> // "hello"
Combines avec les template literals et les mapped types, ils permettent des transformations puissantes. On a deja vu le pattern Getters dans l'article sur les mapped types :
typescripttype Getters<T> = {
[K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
}
Patterns concrets
Typer les événements DOM
typescripttype DOMEventName = `on${Capitalize<keyof HTMLElementEventMap>}`
// "onClick" | "onScroll" | "onKeydown" | "onFocus" | ...
type EventHandler<E extends keyof HTMLElementEventMap> = {
[K in E as `on${Capitalize<K>}`]: (event: HTMLElementEventMap[K]) => void
}
Typer les routes d'une API
typescripttype Method = "GET" | "POST" | "PUT" | "DELETE"
type Resource = "users" | "orders" | "products"
type Endpoint = `${Method} /api/${Resource}`
// "GET /api/users" | "GET /api/orders" | ... | "DELETE /api/products"
// 12 combinaisons
type EndpointWithId = `${Method} /api/${Resource}/${number}`
// "GET /api/users/123" etc.
Prefixes CSS
typescripttype CSSPrefix = "webkit" | "moz" | "ms"
type CSSProperty = "transform" | "animation" | "transition"
type PrefixedCSS = `-${CSSPrefix}-${CSSProperty}`
// "-webkit-transform" | "-webkit-animation" | ... | "-ms-transition"
Cles de traduction typees
Un pattern que j'utilise sur paltemps.fr pour les messages d'erreur :
typescripttype Entity = "user" | "order" | "product"
type Action = "create" | "update" | "delete" | "notFound"
type ErrorKey = `error.${Entity}.${Action}`
// "error.user.create" | "error.user.update" | ... | "error.product.notFound"
const messages: Record<ErrorKey, string> = {
"error.user.create": "Impossible de creer l'utilisateur",
"error.user.update": "Impossible de mettre a jour l'utilisateur",
// ... le compilateur reclame les 12 entrees
}
Le Record garantit que chaque combinaison a un message. Si tu ajoutes une entité "invoice", le compilateur te montre les 4 messages manquants.
Parser des strings avec infer
Les template literal types combines avec infer permettent de décomposer des strings au niveau des types :
typescript// Extraire le path d'une URL
type ExtractPath<T> = T extends `${string}://${string}/${infer Path}` ? Path : never
type P = ExtractPath<"https://paltemps.fr/api/users"> // "api/users"
// Decomposer une route en segments
type Split<S extends string, D extends string> =
S extends `${infer Head}${D}${infer Tail}`
? [Head, ...Split<Tail, D>]
: [S]
type Segments = Split<"api/users/123", "/"> // ["api", "users", "123"]
Extraire les paramètres d'une route
typescripttype ExtractParams<T extends string> =
T extends `${string}:${infer Param}/${infer Rest}`
? Param | ExtractParams<`/${Rest}`>
: T extends `${string}:${infer Param}`
? Param
: never
type Params = ExtractParams<"/api/users/:userId/posts/:postId">
// "userId" | "postId"
Ce pattern est utilise par des libs comme Express typees ou tRPC pour inferer les paramètres de route à partir de la définition de l'URL.
CamelCase a kebab-case
typescripttype CamelToKebab<S extends string> =
S extends `${infer Head}${infer Tail}`
? Head extends Uppercase<Head>
? `-${Lowercase<Head>}${CamelToKebab<Tail>}`
: `${Head}${CamelToKebab<Tail>}`
: S
type A = CamelToKebab<"backgroundColor"> // "background-color"
type B = CamelToKebab<"fontSize"> // "font-size"
type C = CamelToKebab<"color"> // "color"
TypeScript itéré sur chaque caractère. Si c'est une majuscule, il ajoute un - et la met en minuscule. C'est de la manipulation de strings au moment de la compilation.
Limites
Les template literal types ont des limites de complexité. Si tu générés trop de combinaisons, TypeScript refuse :
typescripttype Hex = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "a" | "b" | "c" | "d" | "e" | "f"
type HexColor = `#${Hex}${Hex}${Hex}${Hex}${Hex}${Hex}`
// ❌ Expression produces a union type that is too complex to represent
// 16^6 = 16 millions de combinaisons
La limite pratique est autour de 100 000 combinaisons. Au-dela, utilise string avec un pattern plus simple ou une validation runtime (Zod).
Les types recursifs sur les strings ont aussi une profondeur maximale. TypeScript coupe la recursion autour de 50 niveaux.
Résumé
- Les template literal types construisent des types string avec la syntaxe backtick
- Une union dans un template literal généré le produit cartesien de toutes les combinaisons
Uppercase,Lowercase,Capitalize,Uncapitalizetransforment les types string- Combines avec
infer, ils permettent de parser des strings (routes, URLs, clés) - Attention a l'explosion combinatoire : trop de combinaisons fait crasher le compilateur
Article précédent : 03 - Distributive conditional types
Article suivant : 05 - Branded types : typage nominal en TypeScript