Cours JavaScript et TypeScript · Module 10: TypeScript en pratique
Template Literal Types - manipuler des chaînes dans les types
Dans cette leçon3
Imagine un système d'étiquettes pour échantillons d'ADN, dans lequel chaque nom suit le format dna-espèce-numéro. Un simple string laisserait passer toutes les fautes de frappe, et écrire à la main toutes les variantes prendrait des semaines. Les template literal types sont une fonctionnalité de TypeScript 4.1+ qui permet de créer des types chaîne avec la syntaxe des template literals de JavaScript. Le compilateur vérifie alors le format de la chaîne avant que le code n'arrive au laboratoire.
Les bases des template literal types
La syntaxe de base
L'écriture ressemble à un template literal ordinaire, mais à la place d'une valeur se trouve un type : ${string} désigne n'importe quelle chaîne, et ${number} n'importe quel nombre écrit sous forme de texte :
1// Template literal types de base
2type Greeting = `Hello ${string}`;
3type Welcome = `Welcome to ${string}!`;
4
5// Exemples d'utilisation
6const greeting1: Greeting = "Hello World"; // OK
7const greeting2: Greeting = "Hello TypeScript"; // OK
8// const greeting3: Greeting = "Hi World"; // Erreur ! Doit commencer par "Hello "
9
10// Combinaison de literal types
11type EventName = "click" | "focus" | "blur";
12type EventHandler = `on${Capitalize<EventName>}`;
13// type EventHandler = "onClick" | "onFocus" | "onBlur"
14
15// Exemple pratique - les unités CSS
16type CSSUnit = "px" | "%" | "em" | "rem" | "vh" | "vw";
17type CSSValue = `${number}${CSSUnit}`;
18
19const width: CSSValue = "100px"; // OK
20const height: CSSValue = "50vh"; // OK
21const margin: CSSValue = "2rem"; // OK
22// const padding: CSSValue = "10"; // Erreur ! Il manque l'unité
23
24// Template literals imbriqués
25type BreakpointSize = "sm" | "md" | "lg" | "xl";
26type Breakpoint = `@${BreakpointSize}`;
27type ResponsiveProp<T extends string> = T | `${T}${Breakpoint}`;
28
29type Display = ResponsiveProp<"block" | "none" | "flex">;
30// "block" | "none" | "flex" | "block@sm" | "none@sm" | "flex@sm" |Greeting accepte toute chaîne qui commence par Hello , et non littéralement un texte contenant un signe dollar. Quand tu y insères une union, TypeScript génère l'union de toutes les combinaisons, donc EventHandler représente trois noms, et Display pas moins de quinze variantes.
Le pattern matching avec les template literal types
Combinés aux types conditionnels et au mot-clé infer, ils permettent de décomposer des chaînes en morceaux, un peu comme une expression régulière, mais au moment de la compilation :
1// Extraction de parties de chaînes
2type ExtractId<T> = T extends `id-${infer Id}` ? Id : never;
3
4type UserId = ExtractId<"id-123">; // "123"
5type PostId = ExtractId<"id-abc-def">; // "abc-def"
6type Invalid = ExtractId<"user-123">; // never
7
8// Un pattern matching plus complexe
9type ParseRoute<T> = T extends `/${infer Module}/${infer Action}`
10 ? { module: Module; action: Action }
11 : never;
12
13type Route1 = ParseRoute<"/users/create">; // { module: "users"; action: "create" }
14type Route2 = ParseRoute<"/posts/edit">; // { module: "posts"; action: "edit" }
15
16// Parsing récursif
17type Split<S extends string, D extends string> =
18 S extends `${infer T}${D}${infer U}`
19 ? [T, ...Split<U, D>]
20 : [S];
21
22type PathSegments = Split<"users/123/posts/456", "/">;
23// ["users", "123", "posts", "456"]
24
25// Extraction des paramètres d'une URL
26type ExtractParams<T extends string> = T extends `${infer Start}:${infer Param}/${infer Rest}`
27 ? { [K in Param]: string } & ExtractParams<Rest>
28 : T extends `${infer Start}:${infer Param}`
29 ? { [K in Param]: string }
30 : {};
31
32type RouteParams = ExtractParams<"/users/:userId/posts/:postId">;
33// { userId: string; postId: string }infer Id capture le reste de la chaîne après le préfixe id-, et Split s'appelle lui-même jusqu'à épuisement des séparateurs. La récursion dans les types a cependant une limite de profondeur, donc des chaînes très longues peuvent dépasser les capacités du compilateur.
Les types intégrés de manipulation de chaînes
TypeScript propose quatre types intégrés pour changer la casse : Uppercase, Lowercase, Capitalize et Uncapitalize. Il n'existe pas, par exemple, de type Reverse : celui-là, tu devrais l'écrire toi-même :
1// TypeScript fournit des types intégrés pour manipuler les chaînes
2// Voici comment ils sont déclarés dans lib.es5.d.ts (ne les redéclare pas, sinon tu obtiendras Duplicate identifier) :
3// type Uppercase<T extends string> = intrinsic;
4// type Lowercase<T extends string> = intrinsic;
5// type Capitalize<T extends string> = intrinsic;
6// type Uncapitalize<T extends string> = intrinsic;
7
8// Exemples d'utilisation
9type Shout = Uppercase<"hello world">; // "HELLO WORLD"
10type Whisper = Lowercase<"HELLO WORLD">; // "hello world"
11type Title = Capitalize<"typescript">; // "Typescript"
12type CamelCase = Uncapitalize<"TypeScript">; // "typeScript"
13
14// Application pratique - générer des event handlers
15type DOMEventName = "click" | "focus" | "blur" | "change" | "submit";
16type EventHandlerName<T extends DOMEventName> = `on${Capitalize<T>}`;
17
18type ClickHandler = EventHandlerName<"click">; // "onClick"
19type FocusHandler = EventHandlerName<"focus">; // "onFocus"
20
21// Mapping automatique des event handlers
22type EventHandlers = {
23 [E in DOMEventName as EventHandlerName<E>]: (event: Event) => void;
24};
25// {
26// onClick: (event: Event) => void;
27// onFocus: (event: Event) => void;
28// onBlur: (event: Event) => void;
29// onChange: (event: Event) => void;
30// onSubmit: (event: Event) => void;
31// }Le mot intrinsic signifie que l'implémentation est fournie par le compilateur, c'est pourquoi les déclarations sont en commentaire, et toi, tu te contentes de les utiliser. La clause as dans un type mappé renomme les clés, donc la liste d'événements devient un objet avec les champs onClick, onFocus et les suivants.
Patterns avancés
Transformer les noms de propriétés
Les API renvoient souvent des champs en snake_case, alors que dans ton code tu préfères le camelCase. Les types peuvent convertir les noms pour toi :
1// Snake case to camel case
2type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
3 ? `${T}${Capitalize<SnakeToCamelCase<U>>}`
4 : S;
5
6type CamelCased = SnakeToCamelCase<"hello_world_typescript">; // "helloWorldTypescript"
7
8// Camel case to snake case
9type CamelToSnakeCase<S extends string> = S extends `${infer C}${infer Rest}`
10 ? C extends Lowercase<C>
11 ? `${C}${CamelToSnakeCase<Rest>}`
12 : `_${Lowercase<C>}${CamelToSnakeCase<Rest>}`
13 : S;
14
15type SnakeCased = CamelToSnakeCase<"helloWorldTypeScript">; // "hello_world_type_script"
16
17// Application pratique - conversion d'une réponse d'API
18type SnakeToCamelCaseNested<T> = T extends object
19 ? {
20 [K in keyof T as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<T[K]>
21 }
22 : T;
23
24interface APIResponse {
25 user_id: number;
26 first_name: string;
27 last_name: string;
28 created_at: string;
29 user_settings: {
30 email_notifications: boolean;
31 dark_mode: boolean;
32 };
33}
34
35type CamelCasedResponse = SnakeToCamelCaseNested<APIResponse>;
36// {
37// userId: number;
38// firstName: string;
39// lastName: string;
40// createdAt: string;
41// userSettings: {
42// emailNotifications: boolean;
43// darkMode: boolean;
44// };
45// }CamelToSnakeCase vérifie caractère par caractère et remplace chaque majuscule par un tiret bas suivi de la minuscule. Ce ne sont que des types : au runtime, tu transformes les données avec une fonction séparée, et le type se contente de décrire le résultat.
Validation et contraintes
Les types savent aussi vérifier le format des chaînes. Voici à quoi ressemble la validation d'une adresse e-mail, d'un numéro de téléphone et d'une couleur hexadécimale :
1// Validation du format d'un e-mail
2type ValidateEmail<T extends string> = T extends `${infer Local}@${infer Domain}`
3 ? Domain extends `${infer Name}.${infer Extension}`
4 ? Extension extends "com" | "org" | "net" | "pl" | "edu"
5 ? T
6 : never
7 : never
8 : never;
9
10type ValidEmail = ValidateEmail<"user@example.com">; // "user@example.com"
11type InvalidEmail = ValidateEmail<"invalid-email">; // never
12
13// Validation des numéros de téléphone
14type ValidatePhoneNumber<T extends string> =
15 T extends `+${infer CountryCode}-${infer Rest}`
16 ? CountryCode extends `${number}`
17 ? Rest extends `${number}-${number}-${number}`
18 ? T
19 : never
20 : never
21 : never;
22
23type ValidPhone = ValidatePhoneNumber<"+48-123-456-789">; // "+48-123-456-789"
24type InvalidPhone = ValidatePhoneNumber<"123-456-789">; // never
25
26// Validation d'une couleur hexadécimale
27type HexDigit = "0" | "1" | "2" | "3" | "4" | "5" | "6" | "7" | "8" | "9" | "A" | "B" | "C" | "D" | "E" | "F" | "a" | "b" | "c" | "d" | "e" | "f";
28type HexColor<T extends string> = T extends `#${HexDigit}${HexDigit}${HexDigit}${infer Rest}`
29 ? Rest extends `${HexDigit}${HexDigit}${HexDigit}`
30 ? T
31 : Rest extends ""
32 ? T
33 : never
34 : never;
35
36type ValidHex1 = HexColor<"#FF5733">; // "#FF5733"
37type ValidHex2 = HexColor<"#F53">; // "#F53"
38type InvalidHex = HexColor<"#GG5733">; // neverUne chaîne invalide donne never, donc une fonction qui accepte ce type la rejettera. Cela fonctionne pour les littéraux connus à la compilation, mais le texte saisi par l'utilisateur doit toujours être vérifié au runtime.
Générer des types dynamiquement
Une carte des routes décrite par un type donne au routeur le contrôle des paramètres, et des unions de tables et de colonnes génèrent des requêtes SQL correctes :
1// Système de routing avec type safety
2type RouteDefinition = {
3 "/": {};
4 "/users": {};
5 "/users/:id": { id: string };
6 "/users/:id/posts": { id: string };
7 "/posts/:postId/comments/:commentId": { postId: string; commentId: string };
8};
9
10type ExtractRouteParams<T extends string> =
11 T extends keyof RouteDefinition
12 ? RouteDefinition[T]
13 : T extends `${infer Start}/:${infer Param}/${infer Rest}`
14 ? { [K in Param]: string } & ExtractRouteParams<`${Start}/${Rest}`>
15 : T extends `${infer Start}/:${infer Param}`
16 ? { [K in Param]: string }
17 : {};
18
19// Routeur avec type safety
20class TypedRouter<Routes extends Record<string, any>> {
21 navigate<T extends keyof Routes & string>(
22 path: T,
23 params: Routes[T]
24 ): void {
25 console.log(`Navigating to ${path} with params:`, params);
26 }
27}
28
29const router = new TypedRouter<RouteDefinition>();
30
31router.navigate("/", {}); // OK
32router.navigate("/users/:id", { id: "123" }); // OK
33router.navigate("/posts/:postId/comments/:commentId", { // OK
34 postId: "456",
35 commentId: "789"
36});
37// router.navigate("/users/:id", {}); // Erreur ! Il manque id
38// router.navigate("/users/:id", { id: "123", extra: "field" }); // Erreur ! Champ en trop
39
40// Génération de requêtes SQL
41type Table = "users" | "posts" | "comments";
42type Column<T extends Table> =
43 T extends "users" ? "id" | "name" | "email" | "created_at" :
44 T extends "posts" ? "id" | "title" | "content" | "author_id" :
45 T extends "comments" ? "id" | "text" | "post_id" | "user_id" :
46 never;
47
48type SelectQuery<T extends Table> = `SELECT ${Column<T> | "*"} FROM ${T}`;
49type WhereClause<T extends Table> = `WHERE ${Column<T>} = ?`;
50type FullQuery<T extends Table> = `${SelectQuery<T>} ${WhereClause<T>}`;
51
52type UserQuery = SelectQuery<"users">;
53// "SELECT id FROM users" | "SELECT name FROM users" | ... | "SELECT * FROM users"
54
55type PostQueryWithWhere = FullQuery<"posts">;
56// "SELECT id FROM posts WHERE id = ?" | "SELECT title FROM posts WHERE title = ?" |navigate exige exactement les paramètres que décrit la route, donc un champ manquant ou superflu provoque une erreur de compilation. Attention à la taille des unions : le produit de plusieurs grandes unions croît de façon explosive, et TypeScript refuse de travailler avec des unions de plus de 100 000 éléments.
Applications pratiques
CSS-in-JS avec type safety
Un système de design tokens est une application classique : les noms naissent de la combinaison d'une couleur et d'une nuance, donc une faute de frappe ne passera pas :
1// Système de design tokens
2type Color = "primary" | "secondary" | "success" | "danger" | "warning";
3type Shade = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
4type ColorToken = `color-${Color}-${Shade}`;
5
6type Spacing = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12 | 16 | 20 | 24 | 32;
7type SpacingToken = `spacing-${Spacing}`;
8
9type DesignToken = ColorToken | SpacingToken;
10
11// Fonction auxiliaire avec type safety
12function token(tokenName: DesignToken): string {
13 return `var(--${tokenName})`;
14}
15
16const primaryColor = token("color-primary-500"); // OK
17const spacing = token("spacing-4"); // OK
18// const invalid = token("color-invalid-999"); // Erreur !
19
20// Responsive props system
21type Breakpoint = "sm" | "md" | "lg" | "xl";
22type ResponsiveValue<T> = T | { [K in Breakpoint]?: T };
23
24type FlexDirection = "row" | "column" | "row-reverse" | "column-reverse";
25type JustifyContent = "start" | "end" | "center" | "between" | "around" | "evenly";
26
27interface FlexProps {
28 direction?: ResponsiveValue<FlexDirection>;
29 justify?: ResponsiveValue<JustifyContent>;
30 gap?: ResponsiveValue<Spacing>;
31}
32
33// Génération de classes CSS
34function generateClasses<T extends string>(
35 prefix: string,
36 value: ResponsiveValue<T>
37): string[] {
38 if (typeof value === "object") {
39 return Object.entries(value).map(([breakpoint, val]) =>
40 `${prefix}-${val}@${breakpoint}`
41 );
42 }
43 return [`${prefix}-${value}`];
44}Cinq couleurs et neuf nuances donnent 45 noms que tu n'as pas à écrire à la main.
Un client d'API avec type safety
Des clés de la forme "MÉTHODE /chemin" décrivent les endpoints, et le client en déduit le type de la réponse et du corps de la requête :
1// Définition des endpoints de l'API
2type APIEndpoints = {
3 "GET /users": { response: User[]; params: {} };
4 "GET /users/:id": { response: User; params: { id: string } };
5 "POST /users": { response: User; params: {}; body: CreateUserDto };
6 "PUT /users/:id": { response: User; params: { id: string }; body: UpdateUserDto };
7 "DELETE /users/:id": { response: void; params: { id: string } };
8};
9
10// Extraction de la méthode et du chemin
11type ExtractMethod<T> = T extends `${infer Method} ${infer Path}` ? Method : never;
12type ExtractPath<T> = T extends `${infer Method} ${infer Path}` ? Path : never;
13
14// Type-safe API client
15class APIClient {
16 async request<T extends keyof APIEndpoints>(
17 endpoint: T,
18 options: {
19 params?: APIEndpoints[T]["params"];
20 body?: "body" extends keyof APIEndpoints[T] ? APIEndpoints[T]["body"] : never;
21 } = {}
22 ): Promise<APIEndpoints[T]["response"]> {
23 const method = endpoint.split(" ")[0];
24 const path = endpoint.split(" ")[1];
25
26 // Remplacer les paramètres dans le chemin
27 let url = path;
28 if (options.params) {
29 Object.entries(options.params).forEach(([key, value]) => {
30 url = url.replace(`:${key}`, value as string);
31 });
32 }
33
34 console.log(`${method} ${url}`, options.body);
35
36 // Simulation d'un appel API
37 return {} as APIEndpoints[T]["response"];
38 }
39}
40
41// Utilisation
42const api = new APIClient();
43
44api.request("GET /users"); // OK
45api.request("GET /users/:id", { params: { id: "123" } }); // OK
46api.request("POST /users", { body: { /* ... */ } }); // OK
47// api.request("GET /users/:id"); // Ça compile ! params est optionnel
48// api.request("GET /users", { body: {} }); // Erreur ! GET n'a pas de bodyAppeler GET /users avec un corps est une erreur, car body a alors le type never. Les paramètres restent toutefois optionnels, donc GET /users/:id sans params compilera, et pour les rendre obligatoires, il faudrait des surcharges ou un type conditionnel portant sur l'ensemble des options.
Internationalisation avec type safety
Pour finir, les clés de traduction : un objet imbriqué produit une union de chemins du style user.profile.title :
1// Système de traductions avec des clés imbriquées
2type TranslationKeys = {
3 common: {
4 yes: string;
5 no: string;
6 cancel: string;
7 };
8 user: {
9 profile: {
10 title: string;
11 edit: string;
12 };
13 settings: {
14 title: string;
15 notifications: {
16 email: string;
17 push: string;
18 };
19 };
20 };
21};
22
23// Génération des chemins vers les clés
24type PathsToStringProps<T> = T extends string
25 ? []
26 : {
27 [K in keyof T]: [K, ...PathsToStringProps<T[K]>];
28 }[keyof T];
29
30type TranslationPath = PathsToStringProps<TranslationKeys>;
31// ["common", "yes"] | ["common", "no"] | ["user", "profile", "title"] |
32// Conversion d'un tableau en chaîne séparée par des points
33type JoinPath<T extends string[]> = T extends []
34 ? ""
35 : T extends [infer First]
36 ? First
37 : T extends [infer First, ...infer Rest]
38 ? First extends string
39 ? Rest extends string[]
40 ? `${First}.${JoinPath<Rest> & string}`
41 : never
42 : never
43 : never;
44
45type TranslationKey = JoinPath<TranslationPath>;
46// "common.yes" | "common.no" | "user.profile.title" |
47// Fonction de traduction avec type safety
48function t(key: TranslationKey): string {
49 // Implémentation de la récupération d'une traduction
50 return key;
51}
52
53const title = t("user.profile.title"); // OK
54const email = t("user.settings.notifications.email"); // OK
55// const invalid = t("user.invalid.key"); // Erreur !
56
57// Traductions avec paramètres
58type TranslationWithParams = {
59 "welcome": { name: string };
60 "items_count": { count: number };
61 "date_format": { date: Date };
62};
63
64function tWithParams<K extends keyof TranslationWithParams>(
65 key: K,
66 params: TranslationWithParams[K]
67): string {
68 // Implémentation avec substitution des paramètres
69 return `${key} with ${JSON.stringify(params)}`;
70}
71
72tWithParams("welcome", { name: "Jan" }); // OK
73tWithParams("items_count", { count: 5 }); // OK
74// tWithParams("welcome", { count: 5 }); // Erreur ! Paramètres invalidesL'écriture JoinPath<Rest> & string indique au compilateur que le résultat de la récursion est une chaîne. Sans elle, l'insérer dans un template provoque une erreur de type.
Les template literal types sont un outil puissant qui fait passer la manipulation de chaînes au niveau du système de types. Cela permet de créer des API plus expressives et plus sûres, en particulier pour le routing, l'internationalisation et les systèmes de styles. Mon conseil : utilise-les là où le format de la chaîne est un contrat, par exemple dans les noms d'événements et de tokens, et pas pour valider les données saisies par les utilisateurs. Dans l'éditeur ci-dessous, tu pourras expérimenter avec les exemples, et dans la leçon suivante, tu mettras ces connaissances à profit dans un système d'événements typé.
Retiens : un template literal type, c'est le modèle d'étiquette d'une éprouvette, qui ne laisse entrer au laboratoire aucun échantillon décrit dans le mauvais format.
Code de cette leçon : index.ts
1// Exemples de template literal types en TypeScript
2
3// 1. Template literal types de base
4type Greeting = `Hello ${string}`;
5type ErrorMessage = `Error: ${string}`;
6
7const greeting: Greeting = "Hello TypeScript!"; // OK
8const error: ErrorMessage = "Error: File not found"; // OK
9// const invalid: Greeting = "Hi there!"; // Erreur !
10
11// 2. Valeurs CSS avec sécurité de typage
12type CSSUnit = "px" | "%" | "em" | "rem" | "vh" | "vw";
13type CSSValue = `${number}${CSSUnit}`;
14
15const width: CSSValue = "100px";
16const height: CSSValue = "50vh";
17const margin: CSSValue = "2.5rem";
18
19// 3. Event handlers - génération automatique des noms
20type EventName = "click" | "focus" | "blur" | "change";
21type EventHandlerName<T extends EventName> = `on${Capitalize<T>}`;
22
23type ClickHandler = EventHandlerName<"click">; // "onClick"
24type FocusHandler = EventHandlerName<"focus">; // "onFocus"
25
26// Mappage des event handlers
27type EventHandlers = {
28 [E in EventName as EventHandlerName<E>]: (event: Event) => void;
29};
30
31// 4. Snake case vers camel case
32type SnakeToCamelCase<S extends string> = S extends `${infer T}_${infer U}`
33 ? `${T}${Capitalize<SnakeToCamelCase<U>>}`
34 : S;
35
36// Conversion des propriétés d'un objet
37type SnakeToCamelCaseNested<T> = T extends object
38 ? {
39 [K in keyof T as SnakeToCamelCase<K & string>]: SnakeToCamelCaseNested<T[K]>
40 }
41 : T;
42
43// Transformation de la réponse d'API
44interface APIUserResponse {
45 user_id: number;
46 first_name: string;
47 last_name: string;
48 email_address: string;
49 created_at: string;
50}
51
52type CamelCasedUser = SnakeToCamelCaseNested<APIUserResponse>;
53// Résultat : { userId, firstName, lastName, emailAddress, createdAt }
54
55// 5. Routing avec sécurité de typage
56type Route<T extends string> = T extends `/users/${infer UserId}/posts/${infer PostId}`
57 ? { userId: UserId; postId: PostId }
58 : T extends `/users/${infer UserId}`
59 ? { userId: UserId }
60 : T extends `/posts/${infer PostId}`
61 ? { postId: PostId }
62 : never;
63
64// Test du routing
65type UserPostRoute = Route<"/users/123/posts/456">; // { userId: "123"; postId: "456" }
66type UserRoute = Route<"/users/789">; // { userId: "789" }
67type PostRoute = Route<"/posts/101">; // { postId: "101" }
68
69// 6. Système de traduction concret
70type TranslationKey =
71 | "common.yes"
72 | "common.no"
73 | "common.cancel"
74 | "user.profile.title"
75 | "user.profile.edit"
76 | "errors.notFound"
77 | "errors.unauthorized";
78
79// Fonction de traduction avec sécurité de typage
80function t(key: TranslationKey): string {
81 const translations: Record<TranslationKey, string> = {
82 "common.yes": "Oui",
83 "common.no": "Non",
84 "common.cancel": "Cancel",
85 "user.profile.title": "Profil de l'utilisateur",
86 "user.profile.edit": "Modifier le profil",
87 "errors.notFound": "Introuvable",
88 "errors.unauthorized": "Non autorisé"
89 };
90
91 return translations[key];
92}
93
94// Utilisation avec une sécurité de typage complète
95console.log(t("common.yes")); // "Oui"
96console.log(t("user.profile.title")); // "Profil de l'utilisateur"
97// console.log(t("invalid.key")); // Erreur de compilation !
98
99// 7. Design tokens CSS-in-JS
100type Color = "primary" | "secondary" | "success" | "danger";
101type Shade = 100 | 200 | 300 | 400 | 500 | 600 | 700 | 800 | 900;
102type ColorToken = `color-${Color}-${Shade}`;
103
104type Spacing = 0 | 1 | 2 | 3 | 4 | 5 | 6 | 8 | 10 | 12;
105type SpacingToken = `spacing-${Spacing}`;
106
107type DesignToken = ColorToken | SpacingToken;
108
109function token(name: DesignToken): string {
110 return `var(--${name})`;
111}
112
113// Utilisation des design tokens
114const primaryColor = token("color-primary-500");
115const dangerColor = token("color-danger-700");
116const smallSpace = token("spacing-2");
117const largeSpace = token("spacing-8");
118
119console.log("Design tokens:", {
120 primaryColor,
121 dangerColor,
122 smallSpace,
123 largeSpace
124});
125
126// 8. Validation de format
127type Email<T extends string> = T extends `${string}@${string}.${string}` ? T : never;
128type ValidEmail = Email<"user@example.com">; // "user@example.com"
129type InvalidEmail = Email<"not-an-email">; // never
130
131// Fonction avec validation d'email
132function sendEmail<T extends string>(email: Email<T>, subject: string): void {
133 console.log(`J'envoie un email à ${email} avec pour sujet : ${subject}`);
134}
135
136sendEmail("user@example.com" as Email<"user@example.com">, "Bonjour !");
137// sendEmail("invalid", "Test"); // Erreur de compilation !Tu as repéré une erreur dans cette leçon ?
Teste tes connaissances
Réponds aux questions de cette leçon. Choisis une réponse pour voir tout de suite si elle est correcte.
1. Que crée le type type Greeting =
Hello ${string}en TypeScript ?2. Lequel des éléments suivants n'est PAS un type intégré de manipulation de chaînes en TypeScript ?
Tâches pratiques dans le jeu
- Éditeur de code
Crée des types basés sur les template literal types.
- Clique dans l'ordre
Ordonne les éléments d'un template literal type :
- Tri vertical
Ordonne les étapes de création d'un template literal type :
- Tri horizontal
Ordonne dans le bon ordre les éléments d'un template literal type :
- Éditeur de code
Crée un type pour parser les chemins d'URL.