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, etz.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
interfacevstype: quasi interchangeables pour décrire un objet.interfacesupporte la declaration merging (deux déclarations du même nom fusionnent, utile pour augmenter une lib) etextends;typeest plus général : unions (type Status = "ok" | "error"), tuples, mapped types. Réponse honnête : convention d’équipe,interfacepour les objets publics,typepour 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 unstring[], il rendstring | undefined, pasany. 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. unknownvsany:anydésactive le compilateur — tout est permis, et il se propage.unknownest le contraire : « prouve-le avant d’y toucher » — il force un narrowing avant usage. Pour une entrée externe (JSON,catch),unknownest le bon choix.- Strict mode :
"strict": truedanstsconfig.jsonactive un lot d’options dontstrictNullChecks— sans elle,nulletundefinedsont assignables à tout, et TS perd la moitié de sa valeur.noImplicitAnyinterdit lesanysilencieux. 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 :
any | unknown | never | |
|---|---|---|---|
| Signifie | « Fais ce que tu veux » | « Prouve avant d’utiliser » | « Ne peut pas exister » |
| Assignable à tout le reste | Oui (danger) | Non | Oui (ensemble vide) |
| Tout lui est assignable | Oui | Oui | Non |
| Usage typique | Dette, migration | Entrées externes, catch | Exhaustivité d’un switch |
🎤 En entretien — la règle qui tient en une phrase :
unknownaux frontières,anyjamais,neverpour prouver qu’unswitchcouvre 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
⚠️
asment au compilateur — une assertion ne vérifie rien :data as Userfait juste tairetsc, et le double sautx as unknown as Yest un signal d’alarme en code review. Chaqueasest 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.parserendany, 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. anyqui se propage — un seul paramètreanyet toute la chaîne d’appels perd son typage sans erreur ni warning (saufnoImplicitAny). Traquer aveceslint(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") ouas constcouvrent la plupart des besoins.- Confondre erreurs de compilation et erreurs d’exécution : un
// @ts-ignorefait taire le compilateur, pas le bug.
Pour aller plus loin
- Le Handbook TypeScript — la référence officielle, notamment les chapitres Narrowing et Generics
- Utility Types — la liste complète, à parcourir une fois
- Documentation zod — validation runtime + inférence de types
- Le TS Playground pour expérimenter : écrire une discriminated union et observer le narrowing en survolant les variables
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, andz.infer<typeof schema>derives the static type from it. One source of truth, checked at both levels.
Key concepts to master
interfacevstype: nearly interchangeable for describing an object.interfacesupports declaration merging (two declarations with the same name merge, useful to augment a library) andextends;typeis more general: unions (type Status = "ok" | "error"), tuples, mapped types. Honest answer: team convention —interfacefor public object shapes,typefor the rest.- Generics: parameterized types that stay precise without duplication. Real example:
function first<T>(arr: T[]): T | undefined { return arr[0]; }— called on astring[], it returnsstring | undefined, notany. 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. unknownvsany:anyswitches off the compiler — everything is allowed, and it spreads.unknownis the opposite: “prove it before touching it” — it forces narrowing before use. For external input (JSON,catch),unknownis the right choice.- Strict mode:
"strict": trueintsconfig.jsonenables a bundle of options includingstrictNullChecks— without it,nullandundefinedare assignable to everything, and TS loses half its value.noImplicitAnyforbids silentanys. 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:
any | unknown | never | |
|---|---|---|---|
| Means | “Do whatever you want” | “Prove it before using it” | “Cannot exist” |
| Assignable to everything | Yes (danger) | No | Yes (empty set) |
| Everything assignable to it | Yes | Yes | No |
| Typical use | Debt, migration | External input, catch | switch exhaustiveness |
🎤 In an interview — the rule that fits in one sentence:
unknownat the boundaries,anynever,neverto prove aswitchcovers 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
⚠️
aslies to the compiler — an assertion checks nothing:data as Usermerely silencestsc, and the double hopx as unknown as Yis a red flag in code review. Everyasis an unverified promise the runtime will eventually test for you.
- “TS validates my data” — no: checking is static.
JSON.parsereturnsany, an API response is whatever the server decided. Without runtime validation (zod), boundary typing is declarative, not guaranteed. anythat spreads — a singleanyparameter and the whole call chain loses its typing with no error or warning (exceptnoImplicitAny). Track it witheslint(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") oras constcover most needs.- Confusing compile errors with runtime errors: a
// @ts-ignoresilences the compiler, not the bug.
Going further
- The TypeScript Handbook — the official reference, especially the Narrowing and Generics chapters
- Utility Types — the full list, worth reading once
- zod documentation — runtime validation + type inference
- The TS Playground to experiment: write a discriminated union and watch narrowing happen as you hover over variables