Aller au contenu principal
~/GiwiSoft
Ts01

TypeScript avancé : types conditionnels, templates littéraux et inference

Giwi 5 min de lecture TypeScript

TypeScript est bien plus qu’un simple système de types basique. Une fois qu’on maîtrise les types conditionnels, les templates littéraux, et l’inférence, on peut construire des types qui s’adaptent dynamiquement à vos données.

Je vais vous montrer des cas concrets issus de mon code pour illustrer ces concepts.

Types conditionnels

Un type conditionnel agit comme un if au niveau du type :

type IsString<T> = T extends string ? true: false;

type A = IsString<"hello">; // true
type B = IsString<42>; // false

Cas concret : extraire les promesses

Dans mon agent autonome, j’avais besoin d’unifier des retours synchrones et asynchrones :

type AwaitResult<T> = T extends Promise<infer U> ? U : T;

type R1 = AwaitResult<Promise<string>>; // string
type R2 = AwaitResult<number>; // number

Union distribution

Quand on passe une union à un type conditionnel, il distribue automatiquement :

type ToArray<T> = T extends unknown ? T[] : never;
type Result = ToArray<string | number>;
// string[] | number[] (pas (string | number)[])

Pour éviter la distribution, on encapsule :

type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
type Result = ToArrayNonDist<string | number>;
// (string | number)[]

Inférence avec infer

Le mot-clé infer permet de capturer un type dans une condition pour le réutiliser.

Extraire le type d’un tableau

type ArrayItem<T> = T extends Array<infer U> ? U : never;

type Items = ArrayItem<string[]>; // string

Cas concret : typage d’une API

J’ai utilisé infer pour typer automatiquement les retours d’une API Express :

type ApiResponse<T> = {
data: T;
status: number;
message: string;
};

// Fonction générique qui infère le type de retour
async function get<T>(url: string): Promise<ApiResponse<T>> {
const res = await fetch(url);
return res.json();
}

// Usage : le type est inféré automatiquement
const user = await.get<User>("/api/users/1");
// user.data est de type User

Inférer les paramètres d’une fonction

type Parameters<T> = T extends (...args: infer P) => unknown ? P : never;
type ReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;

function greet(name: string, age: number) {
return `Bonjour ${name}, tu as ${age} ans`;
}

type Params = Parameters<typeof greet>; // [string, number]
type Result = ReturnType<typeof greet>; // string

Templates littéraux

Les template literal types permettent de construire des types à partir de chaînes dynamiques.

Routes typées

C’est la fonctionnalité que j’utilise le plus au quotidien :

type Route = `/api/${string}`;

const valid: Route = "/api/users"; // OK
const invalid: Route = "/index.html"; // Error

Cas concret : API REST typée

J’ai typé toutes les routes de mon API REST avec des templates littéraux :

type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type Entity = "users" | "posts" | "comments";

type ApiRoute<M extends HttpMethod, E extends Entity, Id extends string = ""> =
M extends "GET" | "DELETE"
? `/api/${E}/${Id}`
: `/api/${E}`;

// Usage
type GetUser = ApiRoute<"GET", "users", "42">; // "/api/users/42"
type CreatePost = ApiRoute<"POST", "posts">; // "/api/posts"

Manipulation de chaînes de types

TypeScript fournit des utilitaires pour transformer les chaînes :

type EventName = "user:created" | "user:updated" | "post:deleted";

// Capitalize, Uncapitalize, Uppercase, Lowercase
type Capitalized = Capitalize<EventName>;
// "User:created" | "User:updated" | "Post:deleted"

// Extraction avec infer + template
type ExtractEntity<T> = T extends `${infer Entity}:${string}`
? Entity
: never;

type Entity = ExtractEntity<EventName>; // "user" | "post"

type ExtractAction<T> = T extends `${string}:${infer Action}`
? Action
: never;

type Action = ExtractAction<EventName>; // "created" | "updated" | "deleted"

Combiner le tout : un vrai cas d’usage

Voici comment j’ai combiné ces trois concepts pour typer mon système d’événements :

// Définition des événements
interface Events {
"user:created": { id: string; name: string };
"user:updated": { id: string; changes: Partial<User> };
"post:published": { postId: string; title: string };
}

// Types conditionnels + infer
type EventPayload<E extends keyof Events> =
Events[E] extends infer P ? P : never;

// Template literal pour extraire l'entité
type EventEntity<E extends keyof Events> =
E extends `${infer Entity}:${string}` ? Entity : never;

// Le système d'écoute typé
function on<E extends keyof Events>(
event: E,
handler: (payload: EventPayload<E>) => void
): void;

Validation au runtime via les templates

On peut même faire de la validation d’inputs avec les template types :

type HexColor = `#${string}`;

function setColor(color: HexColor) {
document.documentElement.style.setProperty("--color", color);
}

setColor("#312e81"); // OK
setColor("red"); // Error

Prédicats de type avancés

Avec is, on crée des guards qui affinent le type :

function isStringArray(value: unknown): value is string[] {
return (
Array.isArray(value) &&
value.every((item) => typeof item === "string")
);
}

const data: unknown = ["a", "b", "c"];
if (isStringArray(data)) {
// data est string[] ici
console.log(data.map((s) => s.toUpperCase()));
}

Bonus : mapped types avec template keys

On peut créer des types à partir de clés transformées :

type Getters<T> = {
[K in keyof T & string as `get${Capitalize<K>}`]: () => T[K];
};

type User = { name: string; age: number };
type UserGetters = Getters<User>;
// { getName: () => string; getAge: () => number }
type Setters<T> = {
[K in keyof T & string as `set${Capitalize<K>}`]: (value: T[K]) => void;
};

type UserSetters = Setters<User>;
// { setName: (value: string) => void; setAge: (value: number) => void }

Conclusion

Les types conditionnels, l’inférence avec infer, et les templates littéraux sont les piliers du typage avancé en TypeScript. Ils permettent de construire des types dynamiques, auto-adaptatifs, qui suivent la logique de votre métier.

J’utilise ces patterns quotidiennement pour typer mes APIs, mes événements, et mes systèmes de routing. Le compilateur détecte les incohérences à la compilation plutôt qu’à l’exécution. C’est un gain de temps considérable.

Et vous, quel pattern TypeScript vous semble le plus utile au quotidien ?