Que se passe-t-il quand ce code est exécuté ?
let x: number = 5;
let y: string = x;TypeScript n'autorise pas l'assignation d'une valeur de type number à une variable de type string. C'est une erreur de compilation détectée par le système de types.
Toutes les questions avec les bonnes réponses et leurs explications.
let x: number = 5;
let y: string = x;TypeScript n'autorise pas l'assignation d'une valeur de type number à une variable de type string. C'est une erreur de compilation détectée par le système de types.
? à côté de age ?interface User {
name: string;
age?: number;
}
const user: User = { name: "Alice" };? marque une propriété optionnelleLe ? après une propriété d'interface la marque comme optionnelle. L'objet n'est pas obligé de la contenir.
type Status = "active" | "inactive" | "pending";
const s: Status = "deleted";Un union type restreint les valeurs autorisées. 'deleted' ne fait pas partie du type Status, donc le compilateur signale une erreur.
identity<string>(42) ?function identity<T>(arg: T): T {
return arg;
}
const result = identity<string>(42);Le générique T a été explicitement fixé à string, mais l'argument 42 est de type number. TypeScript signalera une erreur de compilation.
console.log(Direction.Up) ?enum Direction {
Up,
Down,
Left,
Right
}
console.log(Direction.Up);Dans un enum numérique sans valeurs explicites, le premier membre vaut 0 et les suivants s'incrémentent de 1.
push ?const arr: readonly number[] = [1, 2, 3];
arr.push(4);Un readonly array n'a pas de méthodes mutantes comme push, pop, splice. C'est une protection au niveau des types.
& dans le contexte des types ?type Point = {
x: number;
y: number;
};
type Point3D = Point & { z: number };
const p: Point3D = { x: 1, y: 2, z: 3 };L'opérateur & crée un intersection type qui combine toutes les propriétés des deux types en un seul.
void ?function greet(name: string): void {
console.log(`Hello, ${name}`);
}void veut dire que la fonction ne renvoie pas de valeur. À ne pas confondre avec never, qui veut dire que la fonction ne se termine jamais.
as const dans ce contexte ?const obj = { name: "Alice", age: 30 } as const;as const crée une const assertion — toutes les propriétés deviennent readonly avec des types littéraux (par ex. 'Alice' au lieu de string).
type StringOrNumber = string | number;
function process(val: StringOrNumber) {
if (typeof val === "string") {
return val.toUpperCase();
}
return val.toFixed(2);
}Type narrowing, c'est la technique qui consiste à restreindre le type à l'intérieur d'un bloc conditionnel. TypeScript reconnaît automatiquement le type après un check typeof.
Config.interface Config {
debug: boolean;
}
interface Config {
verbose: boolean;
}
const cfg: Config = { debug: true, verbose: false };Le Declaration Merging est une particularité unique des interfaces — plusieurs déclarations du même nom fusionnent en un seul type. Les type aliases (type) n'ont pas cette possibilité.
function handleInput(val: unknown) {
return val.toUpperCase();
}Le type unknown est l'alternative sûre à any. Avant d'utiliser la valeur, tu dois la narrower, par ex. via typeof val === 'string'. Différence clé : any désactive le check, unknown le force.
val is string dans le type de retour ?function isString(val: unknown): val is string {
return typeof val === "string";
}
function process(input: unknown) {
if (isString(input)) {
console.log(input.toUpperCase());
}
}Un type predicate (val is string) est un custom type guard. Quand la fonction renvoie true, TypeScript narrow automatiquement le type dans le bloc conditionnel. Plus puissant qu'un retour boolean classique.
let a: Object = "hello";
let b: object = "hello";
let c: {} = "hello";Object (avec un grand O) et {} acceptent les primitives (string, number). object (en minuscule) n'accepte que les types non-primitifs (objets, tableaux, fonctions). String est une primitive, donc b échoue.
new Animal() ?abstract class Animal {
abstract speak(): string;
greet(): string {
return `I say: ${this.speak()}`;
}
}
class Dog extends Animal {
speak() { return "Woof!"; }
}
const a = new Animal();Une classe abstract est une classe de base qui ne peut pas être instanciée. Elle force les sous-classes à implémenter les méthodes abstraites. new Dog() fonctionne, new Animal() non.
class User {
constructor(
public name: string,
private password: string,
protected role: string
) {}
}
const u = new User("Alice", "secret", "admin");
console.log(u.name, u.password, u.role);public — accessible partout. private — uniquement à l'intérieur de la classe. protected — à l'intérieur de la classe et de ses sous-classes. Depuis l'extérieur, seul name est visible.
const user = {
name: "Alice",
address: {
city: null as string | null,
},
};
const city = user.address?.city ?? "Unknown";
console.log(city);?. est l'optional chaining, ?? est le nullish coalescing?. (optional chaining) accède de manière sûre aux champs imbriqués. ?? (nullish coalescing) renvoie le côté droit uniquement quand le gauche vaut null/undefined. city est null, donc le résultat est 'Unknown'.
! après input et est-il sûr ?function getLength(input: string | null): number {
return input!.length;
}Le !. (non-null assertion) dit au compilateur : 'je sais que ce n'est pas null'. Il ne génère aucun code à l'exécution — si la valeur est réellement null, tu auras un runtime error. Préfère un type guard.
Pair et pourquoi wrong ne compile pas ?type Pair = [string, number];
const p: Pair = ["age", 30];
const [label, value] = p;
const wrong: Pair = [30, "age"];Un tuple est un tableau de longueur fixe avec des types précis à chaque position. [string, number] exige un string en index 0 et un number en index 1. L'ordre inversé est une erreur.
type et interface quand on étend des types ?interface User {
name: string;
}
type Admin = User & { role: string };
// vs
interface Manager extends User {
department: string;
}Interface s'étend avec extends et supporte le declaration merging (plusieurs déclarations). Type utilise & (intersection) et gère unions, tuples et primitives. Pour les API publiques, interface est recommandé.
type Person = {
name: string
age: number
}
type Employee = {
name: string
email: string
}
type A = Person | Employee;
type B = Person & Employee;
const a: A = {
name: 'John',
age: 20,
email: 'john@test-email.co.uk',
}
const b: B = {
name: 'John',
age: 20,
email: 'john@test-email.co.uk',
}
console.log('A:', a.name, a.age, a.email)
console.log('B:', b.name, b.age, b.email)C'est un piège ! Même si l'objet a contient tous les champs, le type A = Person | Employee veut dire 'soit Person, soit Employee'. TypeScript ne sait pas quelle variante tu as — ça pourrait être Person (sans email) ou Employee (sans age). Donc a.age et a.email lèvent une erreur sans narrowing. En revanche, B = Person & Employee exige TOUS les champs des deux types, donc b.name, b.age et b.email sont garantis.
val dans le dernier return ?function formatValue(val: string | number | boolean) {
if (typeof val === "string") {
return val.toUpperCase();
}
if (typeof val === "number") {
return val.toFixed(2);
}
// Co wie TypeScript o val tutaj?
return val;
}TypeScript narrow progressivement le type après chaque type guard. Après typeof === 'string', il élimine string ; après typeof === 'number', il élimine number. Il ne reste que boolean. C'est du type narrowing automatique — le compilateur le déduit tout seul.
res.data dans le bloc success et res.message dans le bloc error ?type ApiResponse<T> =
| { status: "success"; data: T; timestamp: number }
| { status: "error"; message: string; code: number }
| { status: "loading" };
function handleResponse(res: ApiResponse<string[]>) {
if (res.status === "success") {
console.log(res.data.join(", "));
} else if (res.status === "error") {
console.log(res.message);
}
}status joue le rôle de tag et permet à TypeScript de narrower le type dans chaque brancheUne discriminated union utilise un champ commun (status) pour permettre à TypeScript de narrower automatiquement le type. Dans le bloc status === 'success', le compilateur sait que tu as la variante avec data et timestamp. Dans error — message et code. Pas besoin de cast.
ConfigKey et ConfigValue ?const config = {
apiUrl: "https://api.example.com",
timeout: 5000,
debug: false,
} as const;
type Config = typeof config;
type ConfigKey = keyof Config;
type ConfigValue = Config[ConfigKey];Grâce à as const, les valeurs de l'objet conservent leurs types littéraux (pas string/number/boolean). typeof config extrait le type de l'objet. keyof donne une union des clés. Config[ConfigKey] est un indexed access type — l'union de TOUS les types de valeurs. Combinaison puissante pour une config type-safe.
in comme type guard ?interface Bird {
fly(): void;
layEggs(): void;
}
interface Fish {
swim(): void;
layEggs(): void;
}
function move(animal: Bird | Fish) {
if ("fly" in animal) {
animal.fly();
} else {
animal.swim();
}
}if, TypeScript sait qu'on a un Bird'fly' in animal vérifie à l'exécution si l'objet possède la méthode fly. TypeScript reconnaît ça comme un type guard : dans le bloc if, il sait qu'on a un Bird (seul Bird a fly). Dans else, il sait qu'on a un Fish. L'opérateur in est une alternative à instanceof quand tu travailles avec des interfaces.
type IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // ?
type B = IsString<number>; // ?Les conditional types fonctionnent comme un ternaire : T extends string vérifie si T étend string. Pour string → 'yes', pour number → 'no'.
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object
? DeepReadonly<T[K]>
: T[K];
};DeepReadonly traverse récursivement toutes les propriétés de l'objet. Si une propriété est un objet, le type s'applique à nouveau. Résultat : tout l'objet devient immuable.
Result ?type ExtractReturn<T> = T extends (...args: any[]) => infer R
? R
: never;
type Result = ExtractReturn<() => Promise<string>>;Le mot-clé infer R extrait le type de retour de la fonction. La fonction renvoie Promise<string>, donc R = Promise<string>.
ClickEvent ?type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">;Les template literal types permettent de créer de nouveaux types string. Capitalize met la première lettre en majuscule : 'click' → 'Click', donc 'onClick'.
type Flatten<T> = T extends Array<infer U> ? U : T;
type A = Flatten<string[]>;
type B = Flatten<number>;Flatten extrait avec infer le type d'élément d'un tableau. string[] → string. Pour number (qui n'est pas un tableau), il renvoie le type tel quel.
Result ?type Exclude<T, U> = T extends U ? never : T;
type Result = Exclude<"a" | "b" | "c", "a" | "c">;Exclude fonctionne de manière distributive sur les union types. Pour chaque membre : s'il étend U → never (supprimé), sinon il reste. Il ne reste que 'b'.
UserGetters ?type Getters<T> = {
[K in keyof T as `get${Capitalize<K & string>}`]: () => T[K];
};
type UserGetters = Getters<{ name: string; age: number }>;Le key remapping (as) dans les mapped types permet de renommer les clés. Ici chaque clé K est transformée en get${Capitalize<K>}, et la valeur devient une fonction getter.
assertNever ?function assertNever(value: never): never {
throw new Error(`Unexpected value: ${value}`);
}
type Shape = "circle" | "square";
function area(shape: Shape) {
switch (shape) {
case "circle": return Math.PI;
case "square": return 1;
default: return assertNever(shape);
}
}assertNever attend un never — un type qui ne devrait jamais exister. Si tu ajoutes une nouvelle variante à Shape sans la traiter dans le switch, le compilateur signalera une erreur.
Result ?type UnionToIntersection<U> =
(U extends any ? (x: U) => void : never) extends
(x: infer I) => void ? I : never;
type Result = UnionToIntersection<{ a: 1 } | { b: 2 }>;UnionToIntersection convertit une union en intersection en exploitant la contravariance de la position des paramètres de fonction. Résultat : { a: 1 } & { b: 2 }.
payUSD(euros) ?declare const brand: unique symbol;
type Branded<T, B> = T & { [brand]: B };
type USD = Branded<number, "USD">;
type EUR = Branded<number, "EUR">;
function payUSD(amount: USD) { /* ... */ }
const euros = 100 as EUR;
payUSD(euros);Les branded types (types nominaux) ajoutent un 'phantom brand' à un type structurel. USD et EUR sont des types distincts même s'ils reposent tous les deux sur number.
-? et -readonly dans les mapped types ?type MakeRequired<T> = {
[P in keyof T]-?: T[P];
};
type MakeMutable<T> = {
-readonly [P in keyof T]: T[P];
};
interface Config {
readonly host?: string;
readonly port?: number;
}
type WritableConfig = MakeMutable<MakeRequired<Config>>;Le préfixe - dans les mapped types retire un modificateur : -? rend un champ obligatoire (comme Required<T>), -readonly rend un champ mutable. WritableConfig devient { host: string; port: number }.
UserPreview et UserWithoutEmail ?interface User {
id: number;
name: string;
email: string;
age: number;
}
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;Pick<T, K> sélectionne les clés indiquées d'un type. Omit<T, K> retire les clés indiquées. Les deux créent un nouveau type avec un sous-ensemble des propriétés d'origine.
Record<Roles, boolean> et que se passerait-il si on omettait la clé viewer ?type Roles = "admin" | "editor" | "viewer";
type Permissions = Record<Roles, boolean>;
const perms: Permissions = {
admin: true,
editor: true,
viewer: false,
};Record<K, V> crée un type avec les clés K et les valeurs V. Toutes les clés sont obligatoires — en omettre une est une erreur de compilation.
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; width: number; height: number }
| { kind: "triangle"; base: number; height: number };
function area(shape: Shape): number {
switch (shape.kind) {
case "circle":
return Math.PI * shape.radius ** 2;
case "rect":
return shape.width * shape.height;
case "triangle":
return (shape.base * shape.height) / 2;
}
}kind partagé permet à TypeScript de narrower le type dans chaque brancheUne Discriminated Union utilise un champ tag (discriminant) comme kind. TypeScript narrow automatiquement le type après vérification du tag dans switch/if — ce qui te donne accès aux champs propres à chaque variante.
function logLength<T extends { length: number }>(obj: T): void {
console.log(obj.length);
}
logLength("hello");
logLength([1, 2, 3]);
logLength(42);extends { length: number } l'exigeLe generic constraint T extends { length: number } exige que T ait une propriété length. String et tableau ont length, donc ça passe. Number n'en a pas — erreur de compilation.
Result ?function getParams<T extends (...args: any[]) => any>(
fn: T
): Parameters<T> {
return [] as any;
}
type Result = Parameters<(a: string, b: number) => void>;Parameters<T> extrait les types des paramètres d'une fonction sous forme de tuple. Pour (a: string, b: number) => void, ça renvoie [string, number].
Result ?type NonStringKeys<T> = {
[K in keyof T]: T[K] extends string ? never : K;
}[keyof T];
interface User {
id: number;
name: string;
age: number;
email: string;
}
type Result = NonStringKeys<User>;Ce type filtre les clés : si la valeur est string → never (supprimée), sinon il garde le nom de la clé. id et age sont en number, donc ils restent. name et email (string) sont retirés.
Numbers ?type Extract<T, U> = T extends U ? T : never;
type Numbers = Extract<string | number | boolean, number | boolean>;Extract<T, U> est l'inverse d'Exclude — il garde de l'union T uniquement les types assignables à U. string n'est pas dans U, donc il est retiré. Il reste number | boolean.
// tsconfig.json
{
"compilerOptions": {
"strict": true,
"noImplicitAny": true
}
}
function add(a, b) {
return a + b;
}strict: true active entre autres noImplicitAny. Les paramètres a et b n'ont pas de types et ne peuvent pas être inférés du contexte — le compilateur leur attribuerait any par défaut, mais le flag l'empêche.
type ReadOnly<T> = { readonly [P in keyof T]: T[P] };
type MyPartial<T> = { [P in keyof T]?: T[P] };
interface Todo {
title: string;
done: boolean;
}
type A = ReadOnly<Todo>;
type B = MyPartial<Todo>;Les mapped types itèrent sur les clés d'un type et permettent d'ajouter des modificateurs. ReadOnly ajoute readonly à chaque champ, MyPartial ajoute ? (optionnalité). C'est ainsi que fonctionnent les Readonly<T> et Partial<T> intégrés.
CSSSpacing et comment sont-ils générés ?type CSSProperty = "margin" | "padding";
type Direction = "top" | "right" | "bottom" | "left";
type CSSSpacing = {
[K in `${CSSProperty}-${Direction}`]: string;
};
const spacing: CSSSpacing = {
"margin-top": "10px",
"margin-right": "20px",
// ... wymaga wszystkich 8 kombinacji
};Un template literal type dans un mapped type crée TOUTES les combinaisons : margin-top, margin-right, margin-bottom, margin-left, padding-top, padding-right, padding-bottom, padding-left = 8 champs. TypeScript génère le produit cartésien de strings. Outil puissant pour générer des types CSS-in-JS.
type Awaited<T> =
T extends Promise<infer U>
? Awaited<U>
: T;
type A = Awaited<Promise<string>>;
type B = Awaited<Promise<Promise<number>>>;
type C = Awaited<boolean>;Awaited 'unwrap' récursivement les Promises : Promise<string> → string, Promise<Promise<number>> → Promise<number> → number. Si T n'est pas une Promise, il renvoie T tel quel (boolean). C'est une version simplifiée du Awaited<T> intégré depuis TypeScript 4.5.
type ToArray<T> = T extends any ? T[] : never;
type A = ToArray<string | number>;
type B = ToArray<string | number | boolean>;Les conditional types avec un paramètre T sont distributifs sur les unions ! TypeScript applique la condition à CHAQUE membre de l'union séparément : string → string[], number → number[]. Le résultat est une union de tableaux, pas un tableau d'union. Pour désactiver la distribution, enveloppe T dans [T].
r et g, et en quoi satisfies diffère de : Colors ?type Colors = Record<string, [number, number, number] | string>;
const palette = {
red: [255, 0, 0],
green: "#00ff00",
blue: [0, 0, 255],
} satisfies Colors;
const r = palette.red; // jaki typ?
const g = palette.green; // jaki typ?L'opérateur satisfies (TS 4.9+) vérifie qu'une valeur est conforme à un type, mais n'élargit PAS le type de la variable. Avec : Colors, les deux champs auraient le type [number,number,number] | string. Avec satisfies, TypeScript conserve les types étroits — red est un tuple, green est un string.
StringFields et NumberFields ?type PickByType<T, U> = {
[K in keyof T as T[K] extends U ? K : never]: T[K];
};
interface Model {
id: number;
name: string;
active: boolean;
email: string;
age: number;
}
type StringFields = PickByType<Model, string>;
type NumberFields = PickByType<Model, number>;Le key remapping avec as + un conditional type permet de filtrer les clés. Quand T[K] extends U est false, la clé est mappée sur never — ce qui la retire du résultat. StringFields contient les champs dont les valeurs sont des string, NumberFields ceux en number.
console.log et comment s'appelle cette technique ?const add = (a: number) => (b: number) => a + b;
const add5 = add(5);
console.log(add5(3));Le currying transforme une fonction à plusieurs arguments en une suite de fonctions à un seul argument. add(5) renvoie (b) => 5 + b, donc add5(3) = 8.
transform(5) ?const pipe = <T>(...fns: Array<(arg: T) => T>) =>
(value: T): T =>
fns.reduce((acc, fn) => fn(acc), value);
const transform = pipe(
(x: number) => x * 2,
(x: number) => x + 1,
(x: number) => x * 3
);
console.log(transform(5));Pipe exécute les fonctions de gauche à droite : 5 → *2 = 10 → +1 = 11 → *3 = 33.
type Option<T> = { tag: "some"; value: T } | { tag: "none" };
const map = <A, B>(opt: Option<A>, fn: (a: A) => B): Option<B> =>
opt.tag === "some"
? { tag: "some", value: fn(opt.value) }
: { tag: "none" };
const result = map({ tag: "some", value: 5 }, x => x * 2);Le monad Option (Maybe) enveloppe une valeur qui peut ne pas exister. La fonction map est une opération de functor — elle transforme la valeur à l'intérieur du conteneur sans changer la structure.
const compose = <A, B, C>(
f: (b: B) => C,
g: (a: A) => B
) => (a: A): C => f(g(a));
const double = (x: number) => x * 2;
const toString = (x: number) => `Value: ${x}`;
const doubleThenString = compose(toString, double);
console.log(doubleThenString(5));Compose exécute les fonctions de droite à gauche (composition mathématique f∘g). Pipe fait l'inverse — de gauche à droite.
flatMap dans le contexte des monades ?type Result<T, E> =
| { ok: true; value: T }
| { ok: false; error: E };
const flatMap = <T, E, U>(
result: Result<T, E>,
fn: (value: T) => Result<U, E>
): Result<U, E> =>
result.ok ? fn(result.value) : result;
const parse = (s: string): Result<number, string> => {
const n = Number(s);
return isNaN(n)
? { ok: false, error: "Not a number" }
: { ok: true, value: n };
};flatMap (bind/chain) est l'opération monadique fondamentale. Elle diffère de map en ce que la fonction renvoie un nouveau conteneur (Result), et flatMap 'aplatit' le résultat.
const memoize = <T extends (...args: any[]) => any>(fn: T): T => {
const cache = new Map<string, ReturnType<T>>();
return ((...args: Parameters<T>): ReturnType<T> => {
const key = JSON.stringify(args);
if (cache.has(key)) return cache.get(key)!;
const result = fn(...args);
cache.set(key, result);
return result;
}) as T;
};
const factorial = memoize((n: number): number =>
n <= 1 ? 1 : n * factorial(n - 1)
);La mémoïsation est une technique qui retient les résultats d'une fonction pour des arguments donnés. Les appels suivants avec les mêmes arguments renvoient le résultat caché au lieu de recalculer.
updated ?type Lens<S, A> = {
get: (s: S) => A;
set: (a: A, s: S) => S;
};
const nameLens: Lens<{ name: string; age: number }, string> = {
get: (s) => s.name,
set: (a, s) => ({ ...s, name: a }),
};
const user = { name: "Alice", age: 30 };
const updated = nameLens.set("Bob", user);Lens est un pattern de programmation fonctionnelle pour les mises à jour immuables de structures imbriquées. Set crée un nouvel objet avec le champ mis à jour, en conservant le reste.
type Predicate<T> = (value: T) => boolean;
const and = <T>(...preds: Predicate<T>[]): Predicate<T> =>
(value) => preds.every(p => p(value));
const or = <T>(...preds: Predicate<T>[]): Predicate<T> =>
(value) => preds.some(p => p(value));
const not = <T>(pred: Predicate<T>): Predicate<T> =>
(value) => !pred(value);
const isEven: Predicate<number> = n => n % 2 === 0;
const isPositive: Predicate<number> = n => n > 0;
const isOddAndPositive = and(not(isEven), isPositive);
console.log(isOddAndPositive(3), isOddAndPositive(-3));Composition de prédicats : 3 est impair (not even) et positif → true. -3 est impair mais pas positif → false.
Task ?type Task<T> = {
fork: (reject: (e: Error) => void, resolve: (t: T) => void) => void;
};
const taskOf = <T>(value: T): Task<T> => ({
fork: (_, resolve) => resolve(value),
});
const taskMap = <A, B>(task: Task<A>, fn: (a: A) => B): Task<B> => ({
fork: (reject, resolve) =>
task.fork(reject, (a) => resolve(fn(a))),
});Task est un monad asynchrone lazy. Contrairement à Promise, il ne s'exécute pas tout de suite — le calcul démarre seulement à l'appel de fork().
const trampoline = <T>(fn: () => T | (() => T)): T => {
let result = fn();
while (typeof result === "function") {
result = (result as () => T)();
}
return result;
};
const sumTo = (n: number, acc: number = 0): any =>
n === 0 ? acc : () => sumTo(n - 1, acc + n);
console.log(trampoline(() => sumTo(100000)));Trampoline transforme la récursion terminale en boucle while, évitant le stack overflow. Au lieu de s'appeler récursivement, il renvoie un thunk (fonction) exécuté dans la boucle.
const double = (x: number): number => x * 2;
const result1 = double(5);
const result2 = double(5);
let counter = 0;
const impureDouble = (x: number): number => {
counter++;
return x * 2;
};Une pure function remplit deux conditions : (1) même input → même output, (2) zéro side-effects. impureDouble mute la variable externe counter, donc elle est impure. La referential transparency veut dire qu'on peut toujours remplacer double(5) par la valeur 10.
withLogging et comment s'appelle ce type de fonction ?const withLogging = <T extends (...args: any[]) => any>(fn: T) =>
(...args: Parameters<T>): ReturnType<T> => {
console.log("Calling with:", args);
const result = fn(...args);
console.log("Result:", result);
return result;
};
const add = (a: number, b: number) => a + b;
const loggedAdd = withLogging(add);
loggedAdd(2, 3);Une Higher-Order Function (HOF) est une fonction qui prend ou renvoie une autre fonction. withLogging prend la fonction fn et renvoie une nouvelle fonction avec du logging en plus — comme un decorator. En FP, les fonctions sont des first-class citizens.
function createCounter() {
let count = 0;
return {
increment: () => ++count,
getCount: () => count,
};
}
const counter = createCounter();
counter.increment();
counter.increment();
console.log(counter.getCount());Une closure est un mécanisme par lequel une fonction conserve l'accès aux variables du scope dans lequel elle a été définie — même après que ce scope est terminé. count vit dans la closure de createCounter.
result ?type Either<L, R> =
| { tag: "left"; value: L }
| { tag: "right"; value: R };
const left = <L>(value: L): Either<L, never> =>
({ tag: "left", value });
const right = <R>(value: R): Either<never, R> =>
({ tag: "right", value });
const map = <L, A, B>(
either: Either<L, A>,
fn: (a: A) => B
): Either<L, B> =>
either.tag === "right"
? right(fn(either.value))
: either;
const result = map(right(10), x => x * 2);Either est un monad avec deux variantes : Left (par convention l'erreur) et Right (le succès). La fonction map transforme la valeur uniquement dans Right, et fait passer Left tel quel — alternative à try/catch.
type Thunk<T> = () => T;
const lazyValue: Thunk<number> = () => {
console.log("Computing...");
return 42;
};
// Wartość nie jest jeszcze obliczona
console.log("Before");
const result = lazyValue();
console.log("After:", result);Un Thunk enveloppe un effet/calcul dans une fonction pour une exécution différée (lazy evaluation). Le calcul ne démarre pas tant que tu n'appelles pas le thunk — ça te donne le contrôle sur le moment d'exécution.
const numbers = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
// Podejście imperatywne
let sum = 0;
for (let i = 0; i < numbers.length; i++) {
if (numbers[i] % 2 === 0) {
sum += numbers[i] * 2;
}
}
// Podejście deklaratywne
const result = numbers
.filter(n => n % 2 === 0)
.map(n => n * 2)
.reduce((acc, n) => acc + n, 0);Le style déclaratif (FP) dit 'quoi' faire : filtre les pairs, double, somme. L'impératif dit 'comment' : itère, vérifie, additionne. Le déclaratif est plus lisible et moins sujet aux bugs.
safeDivide (partial) et totalDivide (total) ?const safeDivide = (a: number, b: number): number | never => {
if (b === 0) throw new Error("Division by zero");
return a / b;
};
const totalDivide = (a: number, b: number): number =>
b === 0 ? 0 : a / b;Une total function est définie pour chaque input de son domaine — elle renvoie toujours un résultat. Une partial function peut lever une exception (comme safeDivide pour b=0). En FP, on préfère les total functions.
const idempotent = (arr: number[]): number[] =>
[...new Set(arr)].sort((a, b) => a - b);
console.log(idempotent([3, 1, 2, 1, 3]));
console.log(idempotent(idempotent([3, 1, 2, 1, 3])));Idempotence : f(f(x)) === f(x). Le premier appel déduplique et trie le tableau. Un second appel sur le résultat déjà traité donnera un résultat identique. Exemples : Math.abs, Array.sort sur un tableau déjà trié.
result ? Quelles méthodes de tableau sont essentielles en FP et pourquoi ?const users = [
{ name: "Alice", age: 30 },
{ name: "Bob", age: 25 },
{ name: "Charlie", age: 35 },
];
const result = users
.filter(u => u.age >= 30)
.map(u => u.name)
.reduce((acc, name) => acc + ", " + name);filter (sélectionner), map (transformer) et reduce (combiner) sont les opérations FP fondamentales sur les collections. Elles sont déclaratives, ne mutent pas la source et peuvent être chaînées (pipeline). Résultat : Alice et Charlie ont >= 30 ans.
result et fallback ?const Maybe = <T>(value: T | null | undefined) => ({
map: <U>(fn: (val: T) => U) =>
value != null ? Maybe(fn(value)) : Maybe<U>(null),
getOrElse: (defaultVal: T) =>
value != null ? value : defaultVal,
});
const result = Maybe("hello")
.map(s => s.toUpperCase())
.map(s => s + "!")
.getOrElse("default");
const fallback = Maybe<string>(null)
.map(s => s.toUpperCase())
.getOrElse("default");Maybe gère null/undefined : si la valeur existe, map continue à la transformer. Si elle est null — toute la chaîne est sautée et getOrElse renvoie la valeur par défaut. Alternative aux longues chaînes de checks if-null.
result et pourquoi ce style de code est-il préféré en FP ?const users = [
{ name: "Alice", age: 30, role: "admin" },
{ name: "Bob", age: 25, role: "user" },
{ name: "Charlie", age: 35, role: "admin" },
{ name: "Diana", age: 28, role: "user" },
] as const;
const result = users
.filter(u => u.role === "admin")
.map(u => ({ ...u, name: u.name.toUpperCase() }))
.sort((a, b) => a.age - b.age);Le pipeline filter→map→sort, c'est l'essence du FP : chaque opération crée une nouvelle structure, l'original reste intact. Le style déclaratif dit CE QUE tu fais (filtre les admins, met les noms en uppercase, trie par âge), pas COMMENT. Facile à lire, à tester et à débugger.
type Fn<A, B> = (a: A) => B;
const pipe2 = <A, B, C>(
f: Fn<A, B>,
g: Fn<B, C>
): Fn<A, C> => (a) => g(f(a));
const parse = (s: string): number => parseInt(s, 10);
const double = (n: number): number => n * 2;
const format = (n: number): string => `Result: ${n}`;
const transform = pipe2(pipe2(parse, double), format);
console.log(transform("21"));pipe2 compose deux fonctions : f: A→B et g: B→C, ce qui donne A→C. Les generics garantissent que les types correspondent — tu ne peux pas combiner string→number avec boolean→string parce que B doit matcher. parse('21')=21, double(21)=42, format(42)='Result: 42'.
Tree<T> ?type Tree<T> =
| { type: "leaf"; value: T }
| { type: "node"; left: Tree<T>; right: Tree<T> };
const sumTree = (tree: Tree<number>): number =>
tree.type === "leaf"
? tree.value
: sumTree(tree.left) + sumTree(tree.right);
const myTree: Tree<number> = {
type: "node",
left: { type: "leaf", value: 1 },
right: {
type: "node",
left: { type: "leaf", value: 2 },
right: { type: "leaf", value: 3 },
},
};
console.log(sumTree(myTree));Tree<T> est un algebraic data type (ADT) — une discriminated union avec une définition récursive. sumTree somme récursivement : node(leaf(1), node(leaf(2), leaf(3))) = 1 + (2 + 3) = 6. Les ADT sont la base de la FP — ils modélisent des structures de données sans classes, purement par les types.
const map = <A, B>(arr: A[], fn: (a: A) => B): B[] =>
arr.map(fn);
const identity = <T>(x: T): T => x;
const double = (x: number) => x * 2;
const addOne = (x: number) => x + 1;
// Law 1: Identity
map([1, 2, 3], identity) // === [1, 2, 3]
// Law 2: Composition
map(map([1, 2, 3], double), addOne)
// ===
map([1, 2, 3], x => addOne(double(x)))Un functor est un conteneur (Array, Maybe, Promise) avec une opération map qui satisfait deux lois : (1) mapper l'identité ne change pas le conteneur, (2) mapper deux fonctions à la suite donne le même résultat que mapper leur composition. Ça garantit un comportement prévisible.
result et qu'est-ce qu'un transducer ?type Reducer<A, B> = (acc: B, val: A) => B;
type Transducer<A, B> = <C>(rf: Reducer<B, C>) => Reducer<A, C>;
const mapT = <A, B>(fn: (a: A) => B): Transducer<A, B> =>
(rf) => (acc, val) => rf(acc, fn(val));
const filterT = <A>(pred: (a: A) => boolean): Transducer<A, A> =>
(rf) => (acc, val) => pred(val) ? rf(acc, val) : acc;
const nums = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10];
const isEven = (n: number) => n % 2 === 0;
const triple = (n: number) => n * 3;
const xform = (rf: Reducer<number, number[]>) =>
filterT(isEven)(mapT(triple)(rf));
const result = nums.reduce(
xform((acc, val) => [...acc, val]),
[] as number[]
);Un transducer combine filter et map en un seul passage — pas de tableaux intermédiaires comme avec un chain .filter().map(). Il filtre les pairs (2,4,6,8,10) et multiplie ×3 dans une seule réduction. Plus efficace que les chains sur de gros datasets.
interface ButtonProps {
label: string;
onClick: () => void;
variant?: "primary" | "secondary";
}
const Button = ({ label, onClick, variant = "primary" }: ButtonProps) => (
<button onClick={onClick} className={variant}>
{label}
</button>
);La best practice est de définir une interface/type pour les props et de l'utiliser comme type du paramètre. Les props optionnels sont marqués avec ?, et les valeurs par défaut sont fournies dans la déstructuration.
setCount('hello') va-t-il provoquer une erreur ?const [count, setCount] = useState(0);
setCount("hello");TypeScript infère le type du state à partir de la valeur initiale. useState(0) donne le type number, donc setCount n'accepte que number. Pour une union, utilise useState<string | number>(0).
useState<User | null>(null) ?const [user, setUser] = useState<User | null>(null);
if (user) {
console.log(user.name);
}null — il ne saurait rien de UserQuand la valeur initiale est null, TypeScript infère le type comme null. Le générique explicite <User | null> dit : le state peut être User ou null. Après if (user), TypeScript narrow le type à User.
const handleClick = (e: React.MouseEvent<HTMLButtonElement>) => {
console.log(e.currentTarget.value);
};
const handleChange = (e: React.ChangeEvent<HTMLInputElement>) => {
console.log(e.target.value);
};React fournit ses propres types d'events (React.MouseEvent, React.ChangeEvent, React.FormEvent, etc.) avec un générique pour l'élément HTML. Ça donne accès complet aux propriétés typées de l'élément via currentTarget.
useRef<HTMLInputElement>(null) a-t-il besoin d'un générique ?const inputRef = useRef<HTMLInputElement>(null);
useEffect(() => {
inputRef.current?.focus();
}, []);
return <input ref={inputRef} />;useRef<T>(null) crée un ref de type T | null. Le générique dit à TypeScript que .current sera un HTMLInputElement (ou null avant le mount). Tu obtiens ainsi l'autocomplétion complète sur le DOM API.
interface ListProps<T> {
items: T[];
renderItem: (item: T) => React.ReactNode;
}
function List<T>({ items, renderItem }: ListProps<T>) {
return <ul>{items.map((item, i) => <li key={i}>{renderItem(item)}</li>)}</ul>;
}
<List items={["a", "b"]} renderItem={(item) => <span>{item}</span>} />Un generic component utilise un paramètre de type <T> à la fois dans l'interface des props et dans le composant. TypeScript infère T à partir des données passées — ici string[] → T = string. Le rendu est entièrement typé.
ThemeContextType | undefined ?interface ThemeContextType {
theme: "light" | "dark";
toggle: () => void;
}
const ThemeContext = createContext<ThemeContextType | undefined>(undefined);
function useTheme() {
const ctx = useContext(ThemeContext);
if (!ctx) throw new Error("useTheme must be used within ThemeProvider");
return ctx;
}createContext a besoin d'une valeur par défaut. Undefined + custom hook avec guard est un pattern sûr : si un composant n'est pas dans un provider, le hook throw une erreur lisible au lieu de renvoyer undefined.
children et en quoi diffère-t-il de React.ReactElement ?interface CardProps {
title: string;
children: React.ReactNode;
}
const Card = ({ title, children }: CardProps) => (
<div>
<h2>{title}</h2>
<div>{children}</div>
</div>
);React.ReactNode est le type le plus large pour children : JSX, string, number, boolean, null, undefined, tableaux. React.ReactElement est strictement un élément JSX (<Component /> ou <div />). Pour children, presque toujours utilise ReactNode.
React.ComponentPropsWithoutRef<'button'> ?type ButtonProps = React.ComponentPropsWithoutRef<"button"> & {
variant?: "primary" | "danger";
};
const Button = ({ variant = "primary", children, ...rest }: ButtonProps) => (
<button className={variant} {...rest}>
{children}
</button>
);ComponentPropsWithoutRef<'element'> extrait l'ensemble complet des props d'un élément HTML natif. La combiner avec & permet d'ajouter tes propres props (variant). Pattern idéal pour construire des composants UI réutilisables.
React.memo fonctionne avec TypeScript ?const MemoizedList = React.memo(function UserList({
users,
onSelect,
}: {
users: User[];
onSelect: (user: User) => void;
}) {
return (
<ul>
{users.map((u) => (
<li key={u.id} onClick={() => onSelect(u)}>{u.name}</li>
))}
</ul>
);
});React.memo() est générique et infère automatiquement les types de props depuis le composant wrappé. TypeScript garantit que la comparaison de props (shallow comparison) est type-safe.
dispatch de useReducer ?type Action =
| { type: "increment" }
| { type: "decrement" }
| { type: "set"; payload: number };
function reducer(state: number, action: Action): number {
switch (action.type) {
case "increment": return state + 1;
case "decrement": return state - 1;
case "set": return action.payload;
}
}
const [count, dispatch] = useReducer(reducer, 0);useReducer infère le type de dispatch depuis le type Action. La discriminated union (champ type) garantit que dispatch n'accepte que des actions valides. action.payload n'est accessible que dans le case 'set'.
useFetch utilise-t-il le générique <T,> ?const useFetch = <T,>(url: string) => {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
useEffect(() => {
fetch(url)
.then((res) => res.json())
.then((json: T) => setData(json))
.catch((err) => setError(err))
.finally(() => setLoading(false));
}, [url]);
return { data, loading, error };
};
const { data } = useFetch<User[]>("/api/users");Dans les fichiers .tsx, <T> serait interprété comme un tag JSX. <T,> est le workaround — la virgule finale distingue un générique d'un tag. Le caller écrit useFetch<User[]>(url) et data a le type User[] | null.
React.forwardRef ?const ForwardedInput = React.forwardRef<
HTMLInputElement,
{ label: string }
>(({ label, ...props }, ref) => (
<div>
<label>{label}</label>
<input ref={ref} {...props} />
</div>
));React.forwardRef<RefType, PropsType> — le premier générique est le type de l'élément DOM (ref), le second le type des props. Ça permet à un composant enfant de transmettre une ref à un élément interne.
React.ReactNode et React.ReactElement dans les props ?type PropsWithChildren<P = {}> = P & {
children?: React.ReactNode;
};
interface LayoutProps {
sidebar: React.ReactElement;
}
const Layout = ({ sidebar, children }: PropsWithChildren<LayoutProps>) => (
<div>
<aside>{sidebar}</aside>
<main>{children}</main>
</div>
);Utilise ReactNode pour children (accepte tout). Utilise ReactElement quand tu exiges un composant JSX concret (par ex. sidebar doit être un élément, pas du texte). C'est une distinction importante pour typer les layouts.
AsyncState<T> est-il meilleur que State<T> pour gérer un state asynchrone ?type Status = "idle" | "loading" | "success" | "error";
interface State<T> {
status: Status;
data: T | null;
error: string | null;
}
type AsyncState<T> =
| { status: "idle"; data: null; error: null }
| { status: "loading"; data: null; error: null }
| { status: "success"; data: T; error: null }
| { status: "error"; data: null; error: string };Une discriminated union sur le status élimine les états impossibles. TypeScript sait : success → data: T (pas null), error → error: string (pas null). State<T> autorise error + data en même temps — un bug en attente d'arriver.
useCallback et pourquoi as string est-il nécessaire pour formData.get ?const handleSubmit = useCallback(
async (e: React.FormEvent<HTMLFormElement>) => {
e.preventDefault();
const formData = new FormData(e.currentTarget);
const email = formData.get("email") as string;
await submitForm({ email });
},
[]
);FormData.get() renvoie string | File | null parce que les formulaires peuvent contenir des fichiers. Quand tu sais que le champ est du texte, as string narrow le type. Alternative : utilise un type guard typeof val === 'string'.
| undefined contrairement à l'exemple précédent ?type Theme = "light" | "dark";
const ThemeContext = createContext<Theme>("light");
const useTheme = () => useContext(ThemeContext);
const ThemeProvider = ({ children }: { children: React.ReactNode }) => {
const [theme, setTheme] = useState<Theme>("light");
return (
<ThemeContext.Provider value={theme}>
{children}
</ThemeContext.Provider>
);
};Quand createContext reçoit une valeur par défaut sensée ("light"), le composant l'obtient même sans provider. Pas besoin d'undefined ni de guard. Le pattern avec undefined est nécessaire quand il N'Y A PAS de valeur par défaut sensée.
type ButtonVariant = "primary" | "secondary" | "danger";
interface ButtonProps extends React.ButtonHTMLAttributes<HTMLButtonElement> {
variant?: ButtonVariant;
isLoading?: boolean;
}
const Button = React.forwardRef<HTMLButtonElement, ButtonProps>(
({ variant = "primary", isLoading, children, disabled, ...props }, ref) => (
<button
ref={ref}
disabled={disabled || isLoading}
className={variant}
{...props}
>
{isLoading ? "Loading..." : children}
</button>
)
);Le pattern extends HTMLAttributes est un standard dans les design systems. Le composant accepte tous les props natifs de <button> (onClick, disabled, type, etc.) + les siens (variant, isLoading). forwardRef permet de transmettre la ref.
const useLocalStorage = <T,>(
key: string,
initialValue: T
): [T, (value: T | ((prev: T) => T)) => void] => {
const [storedValue, setStoredValue] = useState<T>(() => {
try {
const item = window.localStorage.getItem(key);
return item ? (JSON.parse(item) as T) : initialValue;
} catch {
return initialValue;
}
});
const setValue = (value: T | ((prev: T) => T)) => {
const valueToStore = value instanceof Function ? value(storedValue) : value;
setStoredValue(valueToStore);
window.localStorage.setItem(key, JSON.stringify(valueToStore));
};
return [storedValue, setValue];
};Le hook imite l'API de useState — il renvoie un tuple [value, setter]. L'union T | ((prev: T) => T) permet d'utiliser le setter avec une valeur ou un callback. C'est le même pattern que le setState intégré.
keyof T dans le type Column<T> ?type Column<T> = {
key: keyof T;
header: string;
render?: (value: T[keyof T], row: T) => React.ReactNode;
};
interface TableProps<T> {
data: T[];
columns: Column<T>[];
}
function Table<T extends Record<string, unknown>>({ data, columns }: TableProps<T>) {
return (
<table>
<thead>
<tr>{columns.map(col => <th key={String(col.key)}>{col.header}</th>)}</tr>
</thead>
<tbody>
{data.map((row, i) => (
<tr key={i}>
{columns.map(col => (
<td key={String(col.key)}>
{col.render ? col.render(row[col.key], row) : String(row[col.key])}
</td>
))}
</tr>
))}
</tbody>
</table>
);
}key dans une colonne est une vraie clé du type T — tu ne peux pas indiquer un champ inexistant. TypeScript autocomplète les clés disponibleskeyof T dans un composant Table générique garantit la type-safety : les colonnes ne peuvent référencer que des champs existants des données. Ajouter un nouveau champ aux données le rend automatiquement disponible dans les colonnes.
type PolymorphicProps<E extends React.ElementType> = {
as?: E;
children: React.ReactNode;
} & Omit<React.ComponentPropsWithoutRef<E>, "as" | "children">;
function Box<E extends React.ElementType = "div">({
as,
children,
...props
}: PolymorphicProps<E>) {
const Component = as || "div";
return <Component {...props}>{children}</Component>;
}
<Box as="a" href="/about">Link</Box>
<Box as="button" onClick={() => {}}>Click</Box>
<Box>Default div</Box>as pour changer le tag HTML — TypeScript type automatiquement les props selon l'élément choisiUn polymorphic component (pattern populaire dans Chakra UI, Radix) permet de changer l'élément rendu via le prop as. Grâce aux generics, TypeScript sait : si as='a', les props incluent href. Si as='button' — les props incluent onClick. Type-safety complète.
{ data: T | null; loading: boolean; error: Error | null } ?type QueryResult<T> =
| { status: "idle"; data: undefined; error: undefined }
| { status: "loading"; data: undefined; error: undefined }
| { status: "success"; data: T; error: undefined }
| { status: "error"; data: undefined; error: Error };
function useQuery<T>(url: string): QueryResult<T> {
// implementation
}
const result = useQuery<User[]>("/api/users");
if (result.status === "success") {
result.data.map(u => u.name); // TypeScript wie!
}Avec un simple { data, loading, error }, rien n'empêche les états impossibles comme loading: true, data: someData, error: someError. Une discriminated union les exclut au niveau des types. Après status === 'success', TypeScript garantit que data est T (pas undefined).
interface DataListProps<T> {
items: T[];
filterFn?: (item: T) => boolean;
sortFn?: (a: T, b: T) => number;
children: (item: T, index: number) => React.ReactNode;
}
function DataList<T>({
items,
filterFn,
sortFn,
children,
}: DataListProps<T>) {
let processed = [...items];
if (filterFn) processed = processed.filter(filterFn);
if (sortFn) processed = processed.sort(sortFn);
return <>{processed.map((item, i) => children(item, i))}</>;
}
<DataList
items={users}
filterFn={u => u.active}
sortFn={(a, b) => a.name.localeCompare(b.name)}
>
{(user) => <UserCard key={user.id} user={user} />}
</DataList>DataList est un composant générique avec le pattern render prop. TypeScript infère T depuis items et le propage à filterFn, sortFn et children. Sépare la logique (filtrage, tri) de la présentation (render prop). Du pur FP en React.
handleChange('name', 42) et handleChange('unknown', 'value') provoquent une erreur ?type FormFields = {
name: string;
email: string;
age: number;
};
const useForm = <T extends Record<string, unknown>>(initial: T) => {
const [values, setValues] = useState<T>(initial);
const handleChange = <K extends keyof T>(
field: K,
value: T[K]
) => {
setValues(prev => ({ ...prev, [field]: value }));
};
return { values, handleChange };
};
const { values, handleChange } = useForm<FormFields>({
name: "",
email: "",
age: 0,
});
handleChange("name", "Alice"); // OK
handleChange("age", 25); // OK
handleChange("name", 42); // Error!
handleChange("unknown", "value"); // Error!Le générique K extends keyof T dans handleChange crée une dépendance : field: K restreint aux clés existantes, value: T[K] exige le type correspondant à cette clé. TypeScript vérifie LES DEUX en même temps — impossible de passer un mauvais champ ou un mauvais type de valeur.
interface DialogProps {
open: boolean;
onClose: () => void;
children: React.ReactNode;
}
interface DialogComposition {
Header: React.FC<{ children: React.ReactNode }>;
Body: React.FC<{ children: React.ReactNode }>;
Footer: React.FC<{ children: React.ReactNode }>;
}
const Dialog: React.FC<DialogProps> & DialogComposition = ({
open,
onClose,
children,
}) => {
if (!open) return null;
return <div className="dialog">{children}</div>;
};
Dialog.Header = ({ children }) => <header>{children}</header>;
Dialog.Body = ({ children }) => <main>{children}</main>;
Dialog.Footer = ({ children }) => <footer>{children}</footer>;Le pattern Compound Component (populaire dans Radix, Headless UI) utilise la dot notation pour la composition. Le type FC<DialogProps> & DialogComposition est une intersection — TypeScript sait que Dialog est à la fois un composant et possède des sous-composants. Donne l'autocomplétion sur Dialog.