Julien Bardin

Julien BARDIN

Lead Developer | Artisan du Code et Résolveur de Problèmes Numériques

🧩 TypeScript : astuces, bonnes pratiques et types avancés

1. Type vs Interface

En TypeScript, type et interface permettent tous deux de décrire la forme d’un objet. Il est tout à fait cohérent d’utiliser les deux dans un même projet, chacun a ses avantages selon le contexte, mais pour garder une certaine cohérence et lisibilité, il est souvent recommandé de choisir l’un ou l’autre en fonction du projet (ou des préférences de l'équipe).

  • Interface : idéale pour décrire la forme des objets, extensible par héritage ou déclaration multiple (⚠️ à éviter dans le code métier, voir note ci-dessous).
  • Type : plus flexible (unions, intersections, types primitifs, alias, etc.).
⚠️ Merging (déclaration multiple) modifie une interface existante : ce mécanisme est à éviter car il peut introduire des incohérences/incompréhensions et potentiellement des bugs.
// Interface : extensible
interface User {
  id: number;
  name: string;
}
interface User {
  email: string; // Ajout possible par déclaration multiple (⚠️ à éviter)
}

interface UserRole extends User {
  role: string;
} // UserRole est une interface qui hérite de User et ajoute un champ role.

// Type : unions, intersections
type ID = number | string;
type UserWithRole = User & { role: string }; // alternative à extends pour les types
InterfaceType
Extensible, héritage, objetsUnions, intersections, alias, primitives
Déclaration multiple possibleNon extensible par déclaration multiple

2. Unions et intersections de types

Les unions permettent de définir un type qui peut prendre plusieurs formes : type Role = "admin" | "editor" | "viewer";.
Les intersections permettent de combiner plusieurs types en un seul : type UserWithRole = User & { role: string };.
Il est aussi possible d'utiliser une intersection sur des unions pour restreindre davantage les possibilités : type AdminOrEditor = (User & { role: "admin" }) | (User & { role: "editor" });.
Ici, AdminOrEditor accepte uniquement les objets User dont le rôle est "admin" ou "editor" (et pas "viewer"), ce qui permet de combiner la puissance des deux outils. Ces deux outils sont essentiels pour composer des types flexibles et sécuriser vos données.

// Union de types
 type Status = "success" | "error" | "loading";
 let s: Status = "success";
 s = "loading"; // OK
 s = "fail";    // Erreur TS

// Intersection de types
 type Address = { city: string };
 type User = { id: number; name: string };
 type UserWithAddress = User & Address;
 const u: UserWithAddress = { id: 1, name: "Alice", city: "Paris" };

// Intersection sur une union
// Variante 1 : discriminée
// type AdminOrEditor = (User & { role: "admin" }) | (User & { role: "editor" });
// Variante 2 : plus simple et souvent suffisante
 type AdminOrEditor = User & { role: "admin" | "editor" };
 const admin: AdminOrEditor = { id: 1, name: "Alice", role: "admin" }; // OK
 const editor: AdminOrEditor = { id: 2, name: "Bob", role: "editor" }; // OK
 const viewer: AdminOrEditor = { id: 3, name: "Eve", role: "viewer" }; // Erreur TS
// La première forme permet de distinguer chaque rôle si d'autres propriétés varient selon le rôle.
// La seconde est plus concise si seule la valeur du rôle change.
💡 Astuce : Utilisez les unions pour restreindre les valeurs possibles et les intersections pour enrichir vos objets avec plusieurs sources de données.
Attention ! Toutes les erreurs présentées ici sont détectées uniquement par TypeScript lors du développement : elles ne sécurisent pas les données réelles à l’exécution. TypeScript ne vérifie pas les valeurs reçues depuis une API, un formulaire ou une base de données. Pour garantir la sécurité des données en production, il faut utiliser une validation runtime : soit manuellement (condition sur des paramètres ou autre), soit avec des outils dédiés comme Zod, Yup, etc...

3. Utiliser les types utilitaires intégrés

TypeScript fournit des types prêts à l’emploi pour transformer vos définitions et manipuler les objets de façon typée.

type User = {
  id: number;
  name: string;
  email: string;
}

// Partial rend tout optionnel
// Résultat : { id?: number; name?: string; email?: string }
type UserUpdate = Partial<User>;

// Pick extrait seulement certaines propriétés
// Résultat : { id: number; name: string }
type UserPublic = Pick<User, 'id' | 'name'>;

// Définition du type ID utilisé comme clé pour Record
// ID peut être un number ou une string
type ID = number | string;

// Record pour créer des objets typés dynamiquement
// Résultat : { [clé: ID]: User }
type UserMap = Record<ID, User>;

// Exemple d'utilisation de Record :
const users: UserMap = {
  1: { id: 1, name: "Alice", email: "alice@mail.com" },
  "2": { id: 2, name: "Bob", email: "bob@mail.com" }
};
// Ici, la clé peut être un nombre ou une chaîne, et la valeur est toujours un User
// users[1] => { id: 1, name: "Alice", email: "alice@mail.com" }
// users["2"] => { id: 2, name: "Bob", email: "bob@mail.com" }

4. Utiliser les génériques

Les génériques permettent de rendre vos fonctions, types et interfaces réutilisables et typées de façon flexible. Ils sont très utilisés pour créer des helpers, des API ou des structures de données qui s'adaptent à différents types.

// Fonction générique
type ID = number | string;
function getById<T>(items: T[], id: ID, key: keyof T): T | undefined {
  return items.find(item => item[key] === id);
}

// Type générique
interface ApiResponse<T> {
  data: T;
  error?: string;
}

// Utilisation
const users: ApiResponse<User[]> = {
  data: [{ id: 1, name: "Alice" }],
};

// Générique avec contrainte
function first<T extends { id: ID }>(items: T[]): T | undefined {
  return items[0];
}
💡 Astuce : Les génériques sont la base des helpers avancés et des types utilitaires comme Partial, Pick, Record, etc.

5. Préférer unknown à any

any désactive totalement la vérification de type : TypeScript ne vous aide plus à détecter les erreurs. unknown est plus sûr : il oblige à vérifier le type avant d’utiliser la valeur.

function handleInput(input: unknown) {
  if (typeof input === "string") {
    // Ici, TypeScript sait que input est une string
    console.log(input.toUpperCase());
  }
}

function handleAny(input: any) {
  // Pas de vérification, risque d'erreur
  console.log(input.toUpperCase()); // Peut planter si input n'est pas une string
}
⚠️ Bonnes pratiques : Utilisez unknown pour les entrées dynamiques et évitez any autant que possible.

6. Typer explicitement le retour des fonctions

Il est recommandé de toujours spécifier le type de retour de vos fonctions, qu'elles soient asynchrones ou non. Même si TypeScript peut souvent l'inférer, le noter explicitement permet d'éviter les erreurs lors de modifications futures et améliore la lisibilité du code.

// Fonction asynchrone typée
async function fetchUser(id: number): Promise<User> {
  const res = await fetch(`/api/users/${id}`);
  return res.json();
}

// Fonction synchrone typée
function sum(a: number, b: number): number {
  return a + b;
}

// Mauvais exemple :
function multiply(a: number, b: number) {
  // Type de retour implicite : number
  return a * b;
}
// Si la fonction change et retourne autre chose, TypeScript ne le signalera pas sans typage explicite,
// ce qui peut entrainer des erreurs dans le code appelant et des allé/retours
// inutiles lors de la maintenance.
⚠️ Bonnes pratiques : Toujours typer explicitement le retour des fonctions pour éviter les surprises et faciliter la maintenance.

7. Les types conditionnels

Les types conditionnels permettent d'introduire de la logique dans vos définitions de types. Ils servent à créer des types dynamiques, capables de s'adapter ou de transformer d'autres types selon des règles précises. Ils sont souvent utilisés en complément des génériques pour concevoir des helpers avancés et des transformations typées.

// Exemple 1 : Extraire le type de retour d'une fonction
// Utilisez unknown au lieu de any pour plus de sécurité
type ReturnTypeOf<T> = T extends (...args: unknown[]) => infer R ? R : never;

function sum(a: number, b: number): number {
  return a + b;
}
type Result = ReturnTypeOf<typeof sum>; // number

// Exemple 2 : Filtrer les types
// Si T est un tableau, retourne le type des éléments, sinon never
type ElementType<T> = T extends Array<infer U> ? U : never;
type A = ElementType<string[]>; // string
type B = ElementType<number>;   // never

// Exemple 3 : Retirer les propriétés optionnelles d'un objet
// Ce type utilitaire permet d'obtenir un type avec uniquement les propriétés obligatoires
type RequiredKeys<T> = {
  [K in keyof T as {} extends Pick<T, K> ? never : K]: T[K]
};

type User = {
  id: number;
  name?: string;
  email?: string;
};
type UserRequired = RequiredKeys<User>; // { id: number }
⚠️ Astuce avancée : Les types conditionnels sont puissants pour créer des helpers typés, des API génériques et des transformations complexes sur les types.

Conclusion

TypeScript offre une richesse de concepts et d’outils pour écrire du code plus sûr, lisible et maintenable. Ce guide n’est qu’une porte d’entrée : chaque projet, chaque équipe et chaque problématique peut amener à explorer des usages plus avancés ou à remettre en question certaines pratiques.

À vous de jouer ! Expérimentez, testez, lisez la documentation officielle et n’hésitez pas à vous questionner sur vos choix de typage. C’est en explorant et en confrontant vos besoins que vous progresserez vraiment avec TypeScript.