TypeScript possède un riche système de types, mais vous n'avez pas besoin de tout comprendre pour être productif. La réalité est que 90% du travail est accompli par une poignée de fonctionnalités que vous utilisez chaque jour. Voici la référence pratique — primitives, unions, génériques, types utilitaires et les patrons de rétrécissement qui font tout cliquer. Pas de théorie abstraite ; chaque exemple est le genre de code que vous écrivez dans un vrai projet. Si vous connaissez bien JavaScript et avez fait un peu de TypeScript mais souhaitez consolider votre modèle mental, c'est pour vous. L'histoire complète se trouve dans le Manuel TypeScript — cet article est la version 80/20.

Primitives et littéraux

Les types primitifs de TypeScript correspondent directement aux types d'exécution de JavaScript : string, number, boolean, null, undefined, bigint et symbol. Vous utiliserez les trois premiers constamment ; les autres apparaissent dans des contextes spécifiques.

ts
// Primitives — the foundation
let userId: number = 42;
let username: string = 'alice';
let isActive: boolean = true;
let deletedAt: Date | null = null;     // nullable pattern
let refreshToken: string | undefined;  // optional pattern

Là où TypeScript devient intéressant, ce sont les types littéraux. Au lieu de simplement dire qu'une valeur est une string, vous pouvez dire qu'elle doit être l'une d'un ensemble spécifique de chaînes. C'est bien plus utile qu'une annotation string ordinaire car TypeScript peut détecter les valeurs invalides au moment de la compilation :

ts
type Direction = 'north' | 'south' | 'east' | 'west';
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';
type StatusCode = 200 | 201 | 400 | 401 | 403 | 404 | 500;

function navigate(dir: Direction) {
  console.log(`Moving ${dir}`);
}

navigate('north');  // ✅
navigate('up');     // ❌ Argument of type '"up"' is not assignable to type 'Direction'

// Combine with string for "known values plus free-form"
type EventName = 'click' | 'focus' | 'blur' | (string & {});
L'astuce (string & {}) à la fin garde l'autocomplétion fonctionnelle pour les valeurs connues tout en acceptant n'importe quelle chaîne. C'est un patron courant dans les bibliothèques de systèmes de design.

interface vs type — quand utiliser lequel

C'est la question que tout débutant TypeScript pose. La réponse pratique : utilisez interface pour les formes d'objets — en particulier celles qui représentent des contrats d'API publics ou que d'autres types étendront. Utilisez type pour les unions, intersections, types mappés, et tout ce qui n'est pas purement une forme d'objet. En pratique, ils sont largement interchangeables pour les formes d'objets — choisissez une convention et soyez cohérent au sein d'une base de code.

ts
// interface — object shapes, extensible via extends
interface User {
  id: number;
  email: string;
  createdAt: Date;
}

interface AdminUser extends User {
  role: 'admin';
  permissions: string[];
}

// type — unions, intersections, aliases for anything
type ID = string | number;
type Nullable<T> = T | null;

// Intersection — combine two shapes (common for mixins / HOC props)
type WithTimestamps = {
  createdAt: Date;
  updatedAt: Date;
};

type UserRecord = User & WithTimestamps;

// type is required here — interface can't express a union of shapes
type ApiResponse<T> =
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; code: number };

Une différence significative : les interfaces supportent la fusion de déclarations — vous pouvez déclarer la même interface deux fois et TypeScript fusionne les définitions. C'est ainsi que les bibliothèques augmentent les types globaux (par exemple, en ajoutant des propriétés à Window). Les types ne fusionnent pas ; redéclarer un type est une erreur.

Types union et intersection

Les types union (A | B) disent « cette valeur est soit A soit B ». Les types intersection (A & B) disent « cette valeur est à la fois A et B en même temps ». Les unions sont partout dans le code réel — le patron le plus puissant qu'elles permettent est l' union discriminée, qui est la façon de modéliser des données qui peuvent être dans différents états sans recourir à des champs optionnels partout.

ts
// Discriminated union — model API response states cleanly
type FetchState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; error: string; retryable: boolean };

// TypeScript narrows the type inside each branch
function render<T>(state: FetchState<T>) {
  switch (state.status) {
    case 'idle':    return '<p>Not started</p>';
    case 'loading': return '<p>Loading...</p>';
    case 'success': return `<pre>${JSON.stringify(state.data)}</pre>`;
    case 'error':   return `<p>Error: ${state.error}</p>`;
  }
}

// Intersection — common in React/Angular for composing prop types
type ButtonBaseProps = {
  label: string;
  disabled?: boolean;
};

type IconButtonProps = ButtonBaseProps & {
  icon: string;
  iconPosition: 'left' | 'right';
};

Le patron d'union discriminée s'appuie sur un discriminant — une propriété littérale (ici status) qui est unique à chaque variante. TypeScript utilise cette propriété pour rétrécir le type complet dans les branches conditionnelles, vous donnant un accès sûr aux types pour les champs spécifiques à la variante comme data ou error.

Les génériques — la partie qui fait trébucher tout le monde

Le problème motivant : vous voulez une fonction qui fonctionne sur plusieurs types, mais utiliser any jette toutes les informations de type. Les génériques résolvent cela — ce sont des paramètres de type, écrits entre chevrons, qui permettent à une fonction ou interface de fonctionner sur une famille de types tout en conservant une sécurité de type complète.

ts
// Without generics — any loses all information
function first(arr: any[]): any {
  return arr[0];
}
const x = first([1, 2, 3]); // x is 'any' — TypeScript can't help you here

// With generics — T is inferred from the argument
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}
const n = first([1, 2, 3]);       // n: number | undefined ✅
const s = first(['a', 'b', 'c']); // s: string | undefined ✅

La syntaxe <T> déclare un paramètre de type. Vous pouvez le nommer comme vous voulez — T est juste la convention pour un seul type générique. Quand vous appelez la fonction, TypeScript infère T à partir des arguments, donc vous avez rarement besoin de l'écrire explicitement. Les interfaces génériques sont tout aussi utiles pour modéliser les formes d'API :

ts
// Generic interface — the API wrapper pattern every codebase has
interface ApiResponse<T> {
  data: T;
  meta: {
    page: number;
    totalPages: number;
    totalItems: number;
  };
}

interface User {
  id: number;
  name: string;
  email: string;
}

// The response type is fully typed — no casting needed
async function fetchUsers(): Promise<ApiResponse<User[]>> {
  const res = await fetch('/api/users');
  return res.json();
}

const response = await fetchUsers();
response.data[0].email; // ✅ TypeScript knows this is a string
response.meta.totalPages; // ✅ TypeScript knows this is a number

// Constrained generics — T must have an 'id' field
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
  return items.find(item => item.id === id);
}

const user = findById(users, 42); // T inferred as User
Quand utiliser les génériques : quand vous vous retrouvez à écrire la même signature de fonction deux fois avec des types différents, ou à caster avec as pour obtenir le type de retour souhaité. Ce sont les deux signes qu'un générique serait plus propre.

Types utilitaires que vous utiliserez vraiment

TypeScript est livré avec un ensemble de types utilitaires intégrés qui transforment les types existants en nouveaux types. Ils éliminent le besoin de dupliquer ou d'ajuster manuellement les définitions de types. Voici ceux qui apparaissent constamment dans les vraies bases de code :

ts
interface User {
  id: number;
  name: string;
  email: string;
  role: 'admin' | 'user';
  createdAt: Date;
}

// Partial<T> — all properties become optional
// Perfect for PATCH/update payloads
type UpdateUserPayload = Partial<User>;
// { id?: number; name?: string; email?: string; ... }

async function updateUser(id: number, payload: Partial<User>) {
  return fetch(`/api/users/${id}`, {
    method: 'PATCH',
    body: JSON.stringify(payload)
  });
}

// Required<T> — all properties become required (opposite of Partial)
interface Config {
  apiUrl?: string;
  timeout?: number;
  retries?: number;
}
type ResolvedConfig = Required<Config>;
// { apiUrl: string; timeout: number; retries: number }

// Pick<T, K> — keep only specified properties
type UserSummary = Pick<User, 'id' | 'name' | 'email'>;
// Use in list views where you only need the display fields

// Omit<T, K> — exclude specified properties
type PublicUser = Omit<User, 'role' | 'createdAt'>;
// Safe to expose in API responses

// Readonly<T> — prevents mutation (great for config and frozen state)
const config: Readonly<ResolvedConfig> = {
  apiUrl: 'https://api.example.com',
  timeout: 5000,
  retries: 3
};
// config.timeout = 10000; // ❌ Cannot assign to 'timeout' because it is a read-only property

// Record<K, V> — typed dictionary
type UserCache = Record<number, User>;
const cache: UserCache = {};
cache[42] = { id: 42, name: 'Alice', email: '[email protected]', role: 'user', createdAt: new Date() };

// Common pattern: mapping string keys to a known value shape
type FeatureFlags = Record<string, { enabled: boolean; rolloutPct: number }>;

// ReturnType<T> — infer the return type of a function
function createSession(userId: number) {
  return {
    token: crypto.randomUUID(),
    userId,
    expiresAt: new Date(Date.now() + 86_400_000)
  };
}

type Session = ReturnType<typeof createSession>;
// { token: string; userId: number; expiresAt: Date }
// — no need to define the type separately and keep it in sync

unknown vs any vs never

Ces trois types confondent la plupart des développeurs jusqu'à ce qu'ils comprennent quel problème chacun résout. any est l'échappatoire — il désactive complètement la vérification de type pour cette valeur. Il est utile lors de la migration de JavaScript vers TypeScript, mais une utilisation excessive annule l'intérêt de TypeScript. unknown est l'alternative sûre en termes de types : la valeur pourrait être n'importe quoi, mais vous devez la rétrécir avant de pouvoir faire quoi que ce soit avec elle. never est un type qui ne peut jamais se produire — le bas de la hiérarchie des types.

ts
// any — type checking disabled, use sparingly
function dangerousTransform(input: any) {
  return input.toUpperCase(); // TypeScript won't warn even if this crashes at runtime
}

// unknown — safe alternative, forces you to check before using
function safeTransform(input: unknown): string {
  if (typeof input === 'string') {
    return input.toUpperCase(); // ✅ narrowed to string inside this block
  }
  if (typeof input === 'number') {
    return input.toFixed(2);    // ✅ narrowed to number
  }
  throw new Error(`Cannot transform value of type ${typeof input}`);
}

// Common use: parsing untrusted data (API responses, localStorage, user input)
function parseConfig(raw: unknown): ResolvedConfig {
  if (
    typeof raw === 'object' &&
    raw !== null &&
    'apiUrl' in raw &&
    typeof (raw as any).apiUrl === 'string'
  ) {
    return raw as ResolvedConfig;
  }
  throw new Error('Invalid config format');
}

// never — the exhaustive switch pattern
type Shape = { kind: 'circle'; radius: number } | { kind: 'square'; side: number };

function area(shape: Shape): number {
  switch (shape.kind) {
    case 'circle': return Math.PI * shape.radius ** 2;
    case 'square': return shape.side ** 2;
    default:
      // If you add a new Shape variant and forget to handle it here,
      // TypeScript will error: Type 'NewShape' is not assignable to type 'never'
      const _exhaustive: never = shape;
      throw new Error(`Unhandled shape: ${_exhaustive}`);
  }
}

Le rétrécissement de type en pratique

Le rétrécissement de type est la façon dont TypeScript affine un type large (string | number, unknown, une union discriminée) vers un type spécifique à l'intérieur d'un bloc conditionnel. La documentation de rétrécissement TypeScript couvre chaque garde en profondeur — voici les patrons que vous écrirez quotidiennement.

ts
// typeof — primitive narrowing
function formatValue(val: string | number | boolean): string {
  if (typeof val === 'string')  return val.trim();
  if (typeof val === 'number')  return val.toLocaleString();
  return val ? 'Yes' : 'No';
  // TypeScript knows val must be boolean here
}

// instanceof — class/object narrowing
function handleError(err: unknown): string {
  if (err instanceof Error)   return err.message;
  if (typeof err === 'string') return err;
  return 'An unknown error occurred';
}

// in operator — property existence check
type Cat = { meow(): void };
type Dog = { bark(): void };

function makeNoise(animal: Cat | Dog) {
  if ('meow' in animal) {
    animal.meow(); // TypeScript narrows to Cat
  } else {
    animal.bark(); // TypeScript narrows to Dog
  }
}

// Discriminated union narrowing (most common in component state)
type LoadState<T> =
  | { status: 'loading' }
  | { status: 'success'; data: T }
  | { status: 'error'; message: string };

function renderState<T>(state: LoadState<T>) {
  if (state.status === 'success') {
    console.log(state.data);    // ✅ data is accessible
  } else if (state.status === 'error') {
    console.error(state.message); // ✅ message is accessible
  }
}

// Type predicates — custom type guards
function isUser(obj: unknown): obj is User {
  return (
    typeof obj === 'object' &&
    obj !== null &&
    'id' in obj &&
    'email' in obj
  );
}

// After isUser() returns true, TypeScript knows obj is User
function processPayload(payload: unknown) {
  if (isUser(payload)) {
    console.log(payload.email); // ✅ fully typed
  }
}

L' opérateur typeof est l'outil de rétrécissement le plus fondamental. Combiné avec instanceof, l'opérateur in et les vérifications de propriété discriminante, vous pouvez gérer pratiquement n'importe quel scénario de rétrécissement sans utiliser any ni des casts dangereux.

Conclusion

Le système de types de TypeScript récompense bien apprendre les fondamentaux plutôt que mémoriser des fonctionnalités obscures. Les types littéraux et les unions discriminées éliminent des catégories entières de bogues d'exécution. Les génériques vous permettent d'écrire des abstractions réutilisables et sûres en termes de types. Les types utilitaires comme Partial, Pick, Omit et ReturnType maintiennent vos types DRY. Et unknown avec le rétrécissement vous donne la sécurité des types même aux bords où les données arrivent de l'extérieur. La meilleure façon de consolider tout cela est d'expérimenter interactivement — le Playground TypeScript vous permet de coller n'importe quel extrait et de voir les types inférés instantanément, sans configuration nécessaire. Quand vous travaillez avec des données JSON dans des projets TypeScript, les outils Formateur JSON et JSON vers TypeScript sur ce site peuvent générer des définitions d'interface à partir de vraies charges utiles d'API automatiquement.