Aller au contenu principal
Retour au blog
Web

TypeScript : 10 patterns qui sauvent le code en agence

Au-delà des types basiques, ce sont les patterns avancés (template literal types, branded types, exhaustive checks, etc.) qui transforment vraiment la qualité du code. 10 patterns avec exemples concrets.

16 min969 mots

Tous les devs TypeScript connaissent les bases : interfaces, types union, génériques. Mais ce sont les patterns avancés (souvent peu connus) qui font la différence entre un code TypeScript verbeux et frustrant, et un code qui rend les bugs impossibles à compiler. Cet article compile 10 patterns qu'on utilise quotidiennement chez Krealabs sur nos projets Next.js et React Native, avec exemples concrets. Long mais utile à garder en bookmark.

011. Branded types : empêcher les confusions d'IDs

Le problème : `function deleteUser(userId: string)` accepte n'importe quel string, même un `projectId` ou un `email`. Branded types rendent les IDs typiquement incompatibles entre eux. Plus jamais de bug "j'ai passé un postId à une fonction qui attend un userId".

type Brand<K, T> = K & { __brand: T };
type UserId = Brand<string, 'UserId'>;
type ProjectId = Brand<string, 'ProjectId'>;

function deleteUser(id: UserId) { /* ... */ }
const projectId = 'proj_123' as ProjectId;
deleteUser(projectId); // ❌ Error: ProjectId not assignable to UserId
const userId = 'user_456' as UserId;
deleteUser(userId); // ✅ OK

022. Exhaustive switch avec never

Quand on a un type union (`'pending' | 'paid' | 'failed'`) et qu'on switch dessus, TS peut nous forcer à gérer TOUS les cas. Si on ajoute `'cancelled'` au type plus tard, le compilateur nous le signale partout.

type Status = 'pending' | 'paid' | 'failed';
function statusLabel(s: Status): string {
  switch (s) {
    case 'pending': return 'En attente';
    case 'paid': return 'Payé';
    case 'failed': return 'Échec';
    default:
      const _exhaustive: never = s;
      throw new Error(`Status non géré: ${_exhaustive}`);
  }
}
// Si on ajoute 'cancelled' au type Status :
// → erreur de compilation sur la ligne never car 'cancelled' échappe au switch

033. Type guards custom pour narrowing fiable

Pour distinguer des types union au runtime, écrire des type guards user-defined avec `is`. Plus sûr que `typeof` ou `instanceof` qui ont des edge cases.

interface User { type: 'user'; id: string; email: string }
interface Org { type: 'org'; id: string; name: string }
type Entity = User | Org;

function isUser(e: Entity): e is User {
  return e.type === 'user';
}

function sendEmail(e: Entity) {
  if (isUser(e)) {
    // TS sait que e est User ici, accès à e.email autorisé
    sendTo(e.email);
  } else {
    // e est Org ici
    sendToOrgAdmin(e.id);
  }
}

044. Template literal types pour URLs typées

Les template literal types (TS 4.1+) permettent de typer les patterns de strings. Cas d'usage idéal : URLs API typées, slugs, événements analytics.

type ApiPath = `/api/${'users' | 'projects' | 'invoices'}/${string}`;

function apiCall(path: ApiPath) { /* ... */ }
apiCall('/api/users/123'); // ✅
apiCall('/api/foo/bar'); // ❌ 'foo' not in union
apiCall('/users/123'); // ❌ doesn't start with /api/

055. const assertions pour figer les literals

Sans `as const`, TS infère le type le plus large (`string`, `number`). Avec, il infère le type le plus étroit (le literal exact). Utile pour les configs, les arrays de constantes, les action types Redux.

const ROLES = ['admin', 'editor', 'viewer']; // type string[]
const ROLES_STRICT = ['admin', 'editor', 'viewer'] as const;
// type readonly ['admin', 'editor', 'viewer']
type Role = (typeof ROLES_STRICT)[number];
// 'admin' | 'editor' | 'viewer'
// On peut maintenant utiliser Role partout

066. Utility types : Pick, Omit, Partial, Required

Le quartet de base à maîtriser. Ils évitent de redéfinir des types à la main.

interface User {
  id: string;
  email: string;
  name: string;
  password: string;
  createdAt: Date;
}

// API publique : sans password
type PublicUser = Omit<User, 'password'>;

// Formulaire de signup : tout sauf id/createdAt (auto-générés)
type SignupForm = Omit<User, 'id' | 'createdAt'>;

// Update partiel : tous les champs optionnels
type UserUpdate = Partial<Omit<User, 'id'>>;

// Filtre minimal : juste id et email
type UserRef = Pick<User, 'id' | 'email'>;

077. ReturnType + Parameters pour typer indirectement

Plutôt que dupliquer un type, dériver depuis la signature de fonction. Pratique pour rester DRY quand la source de vérité est la fonction (ex: tRPC, Prisma).

async function getUser(id: string) {
  return { id, name: 'Alice', email: 'a@b.com' };
}
type User = Awaited<ReturnType<typeof getUser>>;
// User = { id: string; name: string; email: string }

type GetUserArgs = Parameters<typeof getUser>;
// [id: string]

088. Discriminated unions pour les états React

Pour modéliser des états mutuellement exclusifs (loading / success / error), utiliser un champ discriminant qui rend le type-narrowing automatique.

type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: Error };

function render<T>(state: FetchState<T>) {
  if (state.status === 'success') {
    // TS sait que state.data existe ici
    return <Display data={state.data} />;
  }
  if (state.status === 'error') {
    // state.error existe
    return <Error msg={state.error.message} />;
  }
  return <Spinner />;
}

099. satisfies pour valider sans élargir le type

L'opérateur `satisfies` (TS 4.9+) vérifie qu'une expression matche un type, MAIS garde l'inférence la plus étroite. Très utile pour les config objects.

type Color = 'red' | 'green' | 'blue';
type Config = Record<string, { color: Color; weight: number }>;

const config = {
  primary: { color: 'red', weight: 1 },
  secondary: { color: 'green', weight: 0.5 },
} satisfies Config;
// TS sait que config.primary.color est exactement 'red' (pas 'Color')
// Et que les clés sont exactement 'primary' | 'secondary' (pas string)
// Tout en garantissant que la structure matche Config

1010. Strict null checks : la base non négociable

Activer `strict: true` dans tsconfig.json. Ça active strictNullChecks (les `null`/`undefined` ne sont plus assignables à n'importe quoi), noImplicitAny, strictFunctionTypes, etc. C'est la fondation de la sécurité TS. Sans ça, TS perd 70% de sa valeur. Sur tous les projets Krealabs : `strict: true` non négociable. Pour les vieux projets, migration progressive : activer strictNullChecks d'abord, corriger les erreurs, puis monter le reste un par un. Voir notre article sur TypeScript 5 strict mode.

En résumé

Maîtriser ces 10 patterns transforme votre code TypeScript : moins de bugs en prod, refactoring plus serein, autocomplétion plus utile. Sur un projet de 30-50k lignes, le gain de productivité (et la baisse du nombre de bugs) est mesurable. Sur tous nos projets Krealabs Next.js et React Native, on utilise ces patterns quotidiennement. Pour discuter de TypeScript ou cadrer un projet avec une stack moderne, contactez-nous. Voir aussi notre lexique TypeScript, notre stack TypeScript, et nos services développement web.

TypeScript
Patterns
Code quality
Agence
Best practices
Maxime Dubois

Écrit par

Maxime Dubois

Fondateur · Krealabs

Découvrir l'équipe

À propos de cet article

Rédigé par

Maxime Dubois

Fondateur · Krealabs

Méthodologie

Rédigé à partir de notre travail d'agence et de la documentation officielle des outils cités. Pas d'IA générative pour le fond éditorial.

Publié le
Parlons projet

Un sujet à creuser ensemble ?

Si cet article t'a parlé et que tu as un projet en cours (ou naissant), écris-nous - premier échange offert.