| name | typescript-mastery |
| description | Maîtrise de TypeScript avec types avancés et patterns. Se déclenche avec "TypeScript", "TS", "types", "generics", "interface", "type guard", "utility types", "strict mode", "tsconfig". Also triggers on "TypeScript types", "fix this type error". |
TypeScript Mastery
1. Configuration — point de départ obligatoire
tsconfig.json minimal recommandé (2026) :
{
"compilerOptions": {
"strict": true,
"target": "ES2022",
"moduleResolution": "bundler",
"verbatimModuleSyntax": true,
"exactOptionalPropertyTypes": true,
"noUncheckedIndexedAccess": true,
"paths": { "@/*": ["./src/*"] }
}
}
Critères de choix moduleResolution :
| Contexte | Valeur |
|---|
| Vite, esbuild, webpack 5 | bundler |
| Node 18+ natif (ESM) | node16 ou nodenext |
| Lib publiée NPM | node16 |
| Legacy CJS | node |
Monorepo : utiliser references + composite: true par package pour éviter que tsc recompile tout.
2. Types fondamentaux — choisir la bonne construction
type HttpMethod = "GET" | "POST" | "PUT" | "DELETE";
type UserId = string & { readonly _brand: "UserId" };
type OrderId = string & { readonly _brand: "OrderId" };
const toUserId = (s: string): UserId => s as UserId;
type Pair<A, B> = [A, B];
const pair: Pair<string, number> = ["age", 30];
type Shape =
| { kind: "circle"; radius: number }
| { kind: "rect"; w: number; h: number };
function area(): {
(s.) {
: . * s. ** ;
: s. * s.;
}
}
type vs interface :
interface → shape d'objet qu'on va étendre ou merger (lib publique, OOP).
type → union, intersection, mapped type, alias de primitive. Par défaut utiliser type.
3. Generics avancés
type UnwrapPromise<T> = T extends Promise<infer R> ? R : T;
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends object ? DeepReadonly<T[K]> : T[K];
};
type EventName<T extends string> = `on${Capitalize<T>}`;
type RequiredFields<T, K extends keyof T> = Omit<T, K> & Required<Pick<T, K>>;
4. Utility types — référence rapide
| Built-in | Usage concret |
|---|
Partial<T> | Formulaire de mise à jour partielle |
Required<T> | Forcer tous les champs après validation |
Pick<T, K> | Projeter une sous-forme |
Omit<T, K> | Exclure password d'un DTO user |
Record<K, V> | Map statique clé-valeur |
Readonly<T> | Config immuable |
ReturnType<F> | Typer le retour d'une fonction inconnue |
Awaited<T> | Déballer le type d'une Promise |
Parameters<F> | Forwarding d'arguments |
NonNullable<T> | Après un guard null |
type DeepPartial<T> = { [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K] };
5. Type guards et narrowing
function isUser(x: unknown): x is User {
return typeof x === "object" && x !== null && "id" in x && "email" in x;
}
function assertDefined<T>(val: T | undefined, msg: string): asserts val is T {
if (val === undefined) throw new Error(msg);
}
function assertNever(x: never): never {
throw new Error(`Unhandled case: ${JSON.stringify(x)}`);
}
6. Patterns métier
Result type (gestion d'erreurs sans exception) :
type Result<T, E = Error> =
| { ok: true; value: T }
| { ok: false; error: E };
async function fetchUser(id: UserId): Promise<Result<User>> {
try {
const data = await api.get(`/users/${id}`);
return { ok: true, value: data };
} catch (e) {
return { ok: false, error: e as Error };
}
}
Validation runtime → types TS avec Zod :
import { z } from "zod";
const UserSchema = z.object({
id: z.string().uuid(),
email: z.string().email(),
role: z.enum(["admin", "user"]),
});
type User = z.infer<typeof UserSchema>;
const user = UserSchema.parse(rawJson);
const safe = UserSchema.safeParse(rawJson);
Repository générique typé :
interface Repository<T, Id> {
findById(id: Id): Promise<T | null>;
save(entity: T): Promise<T>;
delete(id: Id): Promise<void>;
}
class UserRepo implements Repository<User, UserId> { ... }
7. APIs type-safe
import type { paths } from "./api";
import createClient from "openapi-fetch";
const client = createClient<paths>({ baseUrl: "/api" });
const { data, error } = await client.GET("/users/{id}", {
params: { path: { id: "123" } },
});
8. Debugging des types
type Prettify<T> = { [K in keyof T]: T[K] } & {};
const config = {
port: 3000,
host: "localhost",
} satisfies Record<string, string | number>;
import { expectType, expectError } from "tsd";
expectType<User>(parseUser(raw));
expectError(parseUser(123));
legacyCall();
9. Anti-patterns et pièges
| Anti-pattern | Problème | Remède |
|---|
any partout | Désactive le type checker | unknown + narrowing |
as Type sans validation | Cast dangereux silencieux | Zod parse ou type guard |
! non-null assertion | Exception runtime non détectée | assertDefined() ou ?. |
enum standard | Génère du JS runtime, tree-shaking difficile | const enum ou union de literals |
| Types écrits manuellement dupliquant le schéma DB/API | Désynchronisation garantie | Générer depuis OpenAPI / Prisma / Zod |
noUncheckedIndexedAccess: false | arr[0] peut être undefined sans avertissement | Activer et gérer le T | undefined |
Ignorer exactOptionalPropertyTypes | { a?: string } accepte { a: undefined } | Activer pour distinguer absent vs undefined |
Object ou {} comme type générique | Trop permissif, accepte tout | object, Record<string, unknown> |
10. Bonnes pratiques 2026
- Générer, ne pas écrire les types depuis la source de vérité : Prisma → types DB, OpenAPI → types API, Zod schema → types métier.
satisfies plutôt que : Type quand on veut garder le type inféré précis tout en le validant.
verbatimModuleSyntax obligatoire : sépare clairement import type (zéro runtime) de import (runtime).
- Commenter avec JSDoc (
/** */) les types publics : les IDE affichent le commentaire au hover, indispensable pour les libs internes.
tsd ou @vitest/expect-type pour les tests de types dans la CI : évite les régressions silencieuses.
ts-reset (Matt Pocock) pour corriger les types stdlib surprenants (JSON.parse → unknown, Array.isArray correct, etc.).