14 - Type erasure : ce que TypeScript efface au runtime
Ce que tu vas apprendre
- Ce que TypeScript supprime a la compilation et ce qu'il garde
- Pourquoi
instanceofne fonctionne pas sur les interfaces - Les consequences pour la validation de donnees et la reflexion
- Les stratégies pour combler le gap entre types et runtime
Prerequisites
Avoir lu les articles sur les type guards et les branded types.
TypeScript disparaît
Quand tu compiles du TypeScript, le résultat est du JavaScript pur. Tous les types, interfaces, type aliases, generics, et annotations de type sont supprimes. Il ne reste rien.
typescript// TypeScript
interface User {
id: string
name: string
email: string
}
function greet(user: User): string {
return `Hello ${user.name}`
}
const u: User = { id: "1", name: "Nicolas", email: "n@n.dev" }
greet(u)
javascript// JavaScript genere
function greet(user) {
return `Hello ${user.name}`
}
const u = { id: "1", name: "Nicolas", email: "n@n.dev" }
greet(u)
L'interface User a disparu. L'annotation : User a disparu. Le paramètre : string a disparu. Le JavaScript n'a aucune trace des types.
C'est le type erasure. C'est un choix de design delibere : TypeScript est un "superset" de JavaScript qui compile vers JavaScript standard. Pas de runtime type system, pas de reflexion sur les types, pas de metadata.
Ce qui est efface
interface,type— complètement supprimes- Annotations de type (
: string,: User) — supprimees - Generics (
<T>,<User>) — supprimes ascasts — supprimessatisfies— supprimereadonly— supprime (aucune protection au runtime)- Overload signatures — seule l'implementation reste
Ce qui est garde
Certaines constructions TypeScript generent du code JavaScript :
enum— généré un objet JavaScript (c'est une des raisons de les éviter)class— les classes existent en JavaScript- Decorateurs — generent du code (si
experimentalDecorators) import type/export type— supprimes (mais les imports normaux restent)
typescript// Garde : la classe existe au runtime
class User {
constructor(public name: string) {}
}
// Garde : l'enum genere un objet
enum Status { Active = "active" }
// Efface : le type n'existe pas au runtime
type UserId = string
Les consequences pratiques
instanceof ne fonctionne pas sur les interfaces
typescriptinterface User {
id: string
name: string
}
function isUser(value: unknown): boolean {
return value instanceof User // ❌ 'User' only refers to a type
}
User n'existe pas au runtime. Tu ne peux pas l'utiliser avec instanceof. instanceof ne fonctionne qu'avec les classes (qui existent au runtime).
Les generics sont effaces
typescriptfunction isArray<T>(value: unknown): value is T[] {
// ❌ Tu ne peux pas verifier le type T au runtime
// Tu peux seulement verifier que c'est un Array
return Array.isArray(value)
}
// Au runtime, isArray<string> et isArray<number> sont identiques
Tu ne peux pas écrire value is string[] de manière fiable sans vérifier chaque élément.
Les types ne protegent pas les frontieres
Quand des donnees arrivent de l'extérieur (API, base de donnees, fichier JSON, input utilisateur), les types ne les valident pas :
typescriptinterface ApiUser {
id: number
name: string
email: string
}
const data: ApiUser = await fetch("/api/user/1").then(r => r.json())
// ❌ Le cast est implicite — si l'API retourne autre chose, crash silencieux
Le type ApiUser est une déclaration d'intention, pas une validation. Si l'API retourne { id: "abc" } (string au lieu de number), le runtime ne le détecté pas.
Stratégies pour combler le gap
1. Validation runtime avec Zod
La solution moderne. Zod definit un schema qui valide au runtime ET produit un type TypeScript :
typescriptimport { z } from "zod"
const UserSchema = z.object({
id: z.number(),
name: z.string(),
email: z.string().email()
})
type User = z.infer<typeof UserSchema> // type derive du schema
const data = await fetch("/api/user/1").then(r => r.json())
const user = UserSchema.parse(data) // valide au runtime, type: User
Si les donnees ne correspondent pas au schema, Zod lance une erreur détaillée. Le type et la validation sont synchronises : tu ne peux pas les desynchroniser.
L'article sur Zod dans la sous-serie pratique couvre ce sujet en détail.
2. Type guards custom
Pour les cas simples sans dépendance externe :
typescriptfunction isUser(value: unknown): value is User {
return (
typeof value === "object" &&
value !== null &&
"id" in value &&
typeof (value as any).id === "number" &&
"name" in value &&
typeof (value as any).name === "string"
)
}
Verbeux et fragile (si tu ajoutes un champ a User, le guard n'est pas mis à jour automatiquement).
3. Classes avec validation dans le constructeur
typescriptclass User {
readonly id: number
readonly name: string
readonly email: string
constructor(data: unknown) {
if (typeof data !== "object" || data === null) {
throw new Error("Invalid user data")
}
const d = data as Record<string, unknown>
if (typeof d.id !== "number") throw new Error("Invalid id")
if (typeof d.name !== "string") throw new Error("Invalid name")
if (typeof d.email !== "string") throw new Error("Invalid email")
this.id = d.id
this.name = d.name
this.email = d.email
}
}
L'avantage : instanceof User fonctionne apres le constructeur. L'inconvenient : beaucoup de boilerplate.
Ou valider
La regle sur paltemps.fr et les projets clients : valide aux frontieres du système.
┌─────────────────────────────────────────────┐
│ │
│ API responses ──→ Zod parse ──→ types │
│ User input ──→ Zod parse ──→ types │
│ Database rows ──→ ORM types ──→ types │
│ Env variables ──→ Zod parse ──→ types │
│ JSON files ──→ Zod parse ──→ types │
│ │
│ Code interne ──→ types seuls suffisent │
│ │
└─────────────────────────────────────────────┘
A l'intérieur du code, les types suffisent. Le compilateur vérifié les contrats entre fonctions. Pas besoin de revalider un User que tu viens de lire de la base via Prisma (Prisma type deja les résultats).
Aux frontieres (donnees qui entrent dans le système), valide avec Zod ou un type guard. C'est la que le type erasure est un risque.
Le futur : ECMAScript type annotations
La proposition TC39 Type Annotations (stage 1) propose d'ajouter la syntaxe de types a JavaScript. Les types seraient ignores par le runtime (comme des commentaires) mais reconnus par les outils.
Si cette proposition avance, TypeScript pourrait etre exécuté nativement dans les navigateurs et Node.js sans compilation. Le type erasure ne serait plus un "step" de build mais le comportement par défaut du runtime.
Résumé
- TypeScript efface tous les types, interfaces, generics et annotations a la compilation — il ne reste que du JavaScript
instanceofne fonctionne pas sur les interfaces et les types (ils n'existent pas au runtime)- Les types ne protegent pas les frontieres du système (API, input, JSON)
- Valide aux frontieres avec Zod, a l'intérieur les types suffisent
enumetclasssurvivent a la compilation — c'est une des raisons de préférer les unions aux enums
Article précédent : 13 - Typer l'asynchrone
Article suivant : 15 - Glossaire