TypeScript types avances - 04 - Template literal types : manipuler des strings dans les types

Comment construire et transformer des types string avec les template literal types. Autocompletion de routes, event names, CSS properties et plus.

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 infer pour 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, Uncapitalize transforment 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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.