Jour 12 Day 12 · jeudi 13 août 2026 Thursday 13 August 2026 Web Fondamental

TypeScript : le typage qui change tout TypeScript: typing that changes everything

Inférence, narrowing, génériques, unknown vs any : ce que le système de types de TypeScript fait vraiment pour toi — et les questions qui reviennent dans quasiment tous les entretiens front et Node. Inference, narrowing, generics, unknown vs any: what TypeScript's type system really does for you — and the questions that come up in almost every front-end and Node interview.

L’essentiel

TypeScript est un sur-ensemble typé de JavaScript : tout code JS valide est du TS valide, auquel on ajoute des annotations de types vérifiées à la compilation. Le compilateur (tsc) analyse le code, signale les incohérences, puis produit du JavaScript ordinaire — les types disparaissent entièrement au runtime.

app.ts ──tsc──▶ app.js ──node / navigateur──▶ exécution
   │               │
 types vérifiés   types effacés : aucune
 à la compilation  vérification au runtime

Pourquoi typer ? Trois bénéfices concrets : les bugs de classe entière (undefined is not a function, faute de frappe dans un nom de propriété, mauvais argument passé à une fonction) sont attrapés avant d’exécuter quoi que ce soit ; le refactoring devient sûr — renomme une propriété et le compilateur liste tous les endroits à corriger ; et l’autocomplétion devient une documentation vivante — l’IDE sait exactement ce qu’un objet contient, ce qui vaut mieux qu’un README obsolète.

Sur un projet à plusieurs, c’est surtout un contrat entre développeurs : la signature d’une fonction dit ce qu’elle attend et ce qu’elle rend, et le compilateur fait respecter le contrat.

Comment ça marche

Le système de types de TypeScript est structurel (duck typing vérifié statiquement), pas nominal comme en Java ou C#. Deux types sont compatibles si leurs structures le sont, peu importe leur nom : un objet { name: string, age: number } est assignable à une interface Person { name: string } — il a tout ce qu’il faut, et plus. C’est ce qui rend TS naturel pour typer du JS existant, mais ça surprend les habitués de Java : deux interfaces identiques mais de noms différents sont interchangeables.

Deuxième pilier : l’inférence. On n’annote pas tout — const x = 42 infère number, [1, 2].map(n => n * 2) infère number[]. La bonne pratique : annoter les frontières (paramètres de fonctions, valeurs de retour publiques, API) et laisser l’inférence faire le reste à l’intérieur.

Troisième pilier : les unions et le narrowing. Un type string | null force à gérer le cas null avant d’appeler .toUpperCase(). Le compilateur rétrécit le type au fil du contrôle de flot : après un if (typeof x === "string"), x est un string dans la branche. Les outils du narrowing : typeof, instanceof, l’opérateur in, les comparaisons d’un champ discriminant (if (event.kind === "click") sur une discriminated union), et les type guards personnalisés (function isUser(x: unknown): x is User).

Point crucial à énoncer en entretien : le typage est un outil de compilation uniquement. À l’exécution, il n’y a que du JavaScript. Une réponse d’API annotée User n’est pas vérifiée — si le serveur renvoie autre chose, TS ne le verra jamais.

💡 Valider aux frontières avec zod — on décrit un schéma runtime (z.object({ name: z.string() })), schema.parse(data) valide réellement les données, et z.infer<typeof schema> en dérive le type statique. Une seule source de vérité, vérifiée aux deux niveaux.

Concepts clés à maîtriser

  • interface vs type : quasi interchangeables pour décrire un objet. interface supporte la declaration merging (deux déclarations du même nom fusionnent, utile pour augmenter une lib) et extends ; type est plus général : unions (type Status = "ok" | "error"), tuples, mapped types. Réponse honnête : convention d’équipe, interface pour les objets publics, type pour le reste.
  • Génériques : des types paramétrés pour rester précis sans dupliquer. Exemple réel : function first<T>(arr: T[]): T | undefined { return arr[0]; } — appelé sur un string[], il rend string | undefined, pas any. Avec contrainte (K extends keyof T) : impossible de demander une clé qui n’existe pas.
  • Utility types : Partial<T> (tout optionnel — payload d’un PATCH), Pick<T, "id" | "name"> (sous-ensemble), Omit<T, "password"> (tout sauf — un DTO sans champ sensible), Record<string, number> (dictionnaire typé). Les connaître évite de redéclarer des types à la main.
  • unknown vs any : any désactive le compilateur — tout est permis, et il se propage. unknown est le contraire : « prouve-le avant d’y toucher » — il force un narrowing avant usage. Pour une entrée externe (JSON, catch), unknown est le bon choix.
  • Strict mode : "strict": true dans tsconfig.json active un lot d’options dont strictNullChecks — sans elle, null et undefined sont assignables à tout, et TS perd la moitié de sa valeur. noImplicitAny interdit les any silencieux. Tout nouveau projet démarre en strict, non négociable.
  • as (assertion de type) : dit au compilateur « fais-moi confiance », sans aucune vérification. Légitime ponctuellement (DOM, résultat de test), dangereux en réflexe (voir Pièges).

Générique et narrowing réunis dans l’exemple canonique :

// Un générique : Result<T> reste précis quel que soit T
type Result<T> =
  | { ok: true; value: T }       // variante succès
  | { ok: false; error: string } // variante échec

function unwrap<T>(r: Result<T>): T {
  if (r.ok) {
    return r.value; // narrowing : ici r est { ok: true; value: T }
  }
  throw new Error(r.error); // et ici { ok: false; error: string }
}

const n = unwrap({ ok: true, value: 42 }); // n : number, pas any

Les trois types « extrêmes » du système, à ne pas confondre :

anyunknownnever
Signifie« Fais ce que tu veux »« Prouve avant d’utiliser »« Ne peut pas exister »
Assignable à tout le resteOui (danger)NonOui (ensemble vide)
Tout lui est assignableOuiOuiNon
Usage typiqueDette, migrationEntrées externes, catchExhaustivité d’un switch

🎤 En entretien — la règle qui tient en une phrase : unknown aux frontières, any jamais, never pour prouver qu’un switch couvre tous les cas. La citer telle quelle fait mouche.

En entretien

« Qu’est-ce que TypeScript apporte par rapport à JavaScript ? » — Détection d’erreurs à la compilation (typos, arguments mal typés, null non géré), refactoring sûr à l’échelle d’un projet, autocomplétion et navigation fiables dans l’IDE, et un contrat explicite entre modules et entre développeurs. Le tout sans coût runtime : tsc émet du JS pur.

« Différence entre interface et type ? » — Pour un objet, presque aucune. interface : declaration merging, extends, messages d’erreur parfois plus lisibles. type : unions, intersections, tuples, mapped types. Citer une convention (interface pour les formes d’objets, type pour les unions) montre qu’on a pratiqué.

« any vs unknown ? »any désactive la vérification et se propage silencieusement ; unknown accepte tout en entrée mais interdit tout usage tant qu’on n’a pas rétréci le type (typeof, type guard, schéma zod) — voir le tableau plus haut.

« Comment TypeScript gère-t-il les données d’une API externe ? » — Il ne les gère pas : les types disparaissent au runtime, donc annoter fetch avec Promise<User> est une promesse non tenue. La bonne réponse : valider aux frontières avec zod (ou valibot, io-ts), et dériver le type statique du schéma avec z.infer pour n’avoir qu’une source de vérité.

« Explique les génériques avec un exemple. »function first<T>(arr: T[]): T | undefined : le type de retour dépend du type d’entrée, sans perte de précision ni duplication. Bonus : une contrainte K extends keyof T pour un accès de propriété sûr, ou un ApiResponse<T> réutilisé sur toutes les routes.

Pièges & idées reçues

⚠️ as ment au compilateur — une assertion ne vérifie rien : data as User fait juste taire tsc, et le double saut x as unknown as Y est un signal d’alarme en code review. Chaque as est une promesse non vérifiée que le runtime finira par tester à ta place.

  • « TS valide mes données » — non : la vérification est statique. Un JSON.parse rend any, une réponse d’API est ce que le serveur a décidé. Sans validation runtime (zod), le typage des frontières est déclaratif, pas garanti.
  • any qui se propage — un seul paramètre any et toute la chaîne d’appels perd son typage sans erreur ni warning (sauf noImplicitAny). Traquer avec eslint (no-explicit-any) et typer les points d’entrée.
  • Désactiver strictNullChecks « pour aller plus vite » — c’est renoncer à la protection contre l’erreur la plus fréquente de JS. Migrer un projet existant : activer strict et corriger progressivement, pas l’inverse.
  • enum : génère du code runtime (contrairement au reste de TS) et a des comportements surprenants ; les unions de littéraux (type Role = "admin" | "user") ou as const couvrent la plupart des besoins.
  • Confondre erreurs de compilation et erreurs d’exécution : un // @ts-ignore fait taire le compilateur, pas le bug.

Pour aller plus loin

The essentials

TypeScript is a typed superset of JavaScript: any valid JS code is valid TS, extended with type annotations checked at compile time. The compiler (tsc) analyzes the code, reports inconsistencies, then emits plain JavaScript — types are entirely erased at runtime.

app.ts ──tsc──▶ app.js ──node / browser──▶ execution
   │               │
 types checked    types erased: no
 at compile time  runtime checking

Why type at all? Three concrete benefits: whole classes of bugs (undefined is not a function, a typo in a property name, the wrong argument passed to a function) are caught before anything runs; refactoring becomes safe — rename a property and the compiler lists every place to fix; and autocompletion becomes living documentation — the IDE knows exactly what an object contains, which beats a stale README.

On a team project, it’s above all a contract between developers: a function’s signature says what it expects and what it returns, and the compiler enforces the contract.

How it works

TypeScript’s type system is structural (statically checked duck typing), not nominal like Java or C#. Two types are compatible if their structures are, regardless of their names: an object { name: string, age: number } is assignable to an interface Person { name: string } — it has everything required, and more. That’s what makes TS a natural fit for typing existing JS, but it surprises people coming from Java: two identical interfaces with different names are interchangeable.

Second pillar: inference. You don’t annotate everything — const x = 42 infers number, [1, 2].map(n => n * 2) infers number[]. Best practice: annotate the boundaries (function parameters, public return values, APIs) and let inference handle the inside.

Third pillar: unions and narrowing. A string | null type forces you to handle the null case before calling .toUpperCase(). The compiler narrows the type through control flow: after an if (typeof x === "string"), x is a string inside the branch. The narrowing toolbox: typeof, instanceof, the in operator, comparisons on a discriminant field (if (event.kind === "click") on a discriminated union), and custom type guards (function isUser(x: unknown): x is User).

A crucial point to state in an interview: typing is a compile-time-only tool. At runtime, there’s nothing but JavaScript. An API response annotated as User is not checked — if the server returns something else, TS will never see it.

💡 Validate at the boundaries with zod — you describe a runtime schema (z.object({ name: z.string() })), schema.parse(data) actually validates the data, and z.infer<typeof schema> derives the static type from it. One source of truth, checked at both levels.

Key concepts to master

  • interface vs type: nearly interchangeable for describing an object. interface supports declaration merging (two declarations with the same name merge, useful to augment a library) and extends; type is more general: unions (type Status = "ok" | "error"), tuples, mapped types. Honest answer: team convention — interface for public object shapes, type for the rest.
  • Generics: parameterized types that stay precise without duplication. Real example: function first<T>(arr: T[]): T | undefined { return arr[0]; } — called on a string[], it returns string | undefined, not any. With a constraint (K extends keyof T): impossible to ask for a key that doesn’t exist.
  • Utility types: Partial<T> (everything optional — a PATCH payload), Pick<T, "id" | "name"> (a subset), Omit<T, "password"> (everything except — a DTO without a sensitive field), Record<string, number> (a typed dictionary). Knowing them avoids redeclaring types by hand.
  • unknown vs any: any switches off the compiler — everything is allowed, and it spreads. unknown is the opposite: “prove it before touching it” — it forces narrowing before use. For external input (JSON, catch), unknown is the right choice.
  • Strict mode: "strict": true in tsconfig.json enables a bundle of options including strictNullChecks — without it, null and undefined are assignable to everything, and TS loses half its value. noImplicitAny forbids silent anys. Every new project starts in strict mode, non-negotiable.
  • as (type assertion): tells the compiler “trust me”, with zero checking. Legitimate occasionally (DOM, test fixtures), dangerous as a reflex (see Pitfalls).

Generics and narrowing combined in the canonical example:

// A generic: Result<T> stays precise whatever T is
type Result<T> =
  | { ok: true; value: T }       // success variant
  | { ok: false; error: string } // failure variant

function unwrap<T>(r: Result<T>): T {
  if (r.ok) {
    return r.value; // narrowing: here r is { ok: true; value: T }
  }
  throw new Error(r.error); // and here { ok: false; error: string }
}

const n = unwrap({ ok: true, value: 42 }); // n: number, not any

The type system’s three “extreme” types, not to be confused:

anyunknownnever
Means“Do whatever you want”“Prove it before using it”“Cannot exist”
Assignable to everythingYes (danger)NoYes (empty set)
Everything assignable to itYesYesNo
Typical useDebt, migrationExternal input, catchswitch exhaustiveness

🎤 In an interview — the rule that fits in one sentence: unknown at the boundaries, any never, never to prove a switch covers every case. Quoting it as-is lands well.

In an interview

“What does TypeScript bring over JavaScript?” — Compile-time error detection (typos, mistyped arguments, unhandled null), safe project-wide refactoring, reliable autocompletion and navigation in the IDE, and an explicit contract between modules and between developers. All with no runtime cost: tsc emits plain JS.

“Difference between interface and type?” — For an object, almost none. interface: declaration merging, extends, sometimes more readable error messages. type: unions, intersections, tuples, mapped types. Citing a convention (interface for object shapes, type for unions) shows you’ve actually practiced.

any vs unknown?”any disables checking and spreads silently; unknown accepts anything as input but forbids any use until you’ve narrowed the type (typeof, type guard, zod schema) — see the table above.

“How does TypeScript handle data from an external API?” — It doesn’t: types are erased at runtime, so annotating fetch with Promise<User> is an unkept promise. The right answer: validate at the boundaries with zod (or valibot, io-ts), and derive the static type from the schema with z.infer so there’s only one source of truth.

“Explain generics with an example.”function first<T>(arr: T[]): T | undefined: the return type depends on the input type, with no loss of precision and no duplication. Bonus: a K extends keyof T constraint for safe property access, or an ApiResponse<T> reused across all routes.

Pitfalls & misconceptions

⚠️ as lies to the compiler — an assertion checks nothing: data as User merely silences tsc, and the double hop x as unknown as Y is a red flag in code review. Every as is an unverified promise the runtime will eventually test for you.

  • “TS validates my data” — no: checking is static. JSON.parse returns any, an API response is whatever the server decided. Without runtime validation (zod), boundary typing is declarative, not guaranteed.
  • any that spreads — a single any parameter and the whole call chain loses its typing with no error or warning (except noImplicitAny). Track it with eslint (no-explicit-any) and type the entry points.
  • Disabling strictNullChecks “to go faster” — that’s giving up protection against JS’s most common error. Migrating an existing project: enable strict and fix incrementally, not the other way around.
  • enum: generates runtime code (unlike the rest of TS) and has surprising behaviors; literal unions (type Role = "admin" | "user") or as const cover most needs.
  • Confusing compile errors with runtime errors: a // @ts-ignore silences the compiler, not the bug.

Going further

S'entraîner sur ce sujet → Practice this topic →