TypeScript types avances - 14 - Type erasure : ce que TypeScript efface au runtime

Comprendre ce que TypeScript supprime a la compilation. Pourquoi instanceof ne marche pas sur les interfaces, et les consequences pour la validation et la reflexion.

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 instanceof ne 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
  • as casts — supprimes
  • satisfies — supprime
  • readonly — 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
  • instanceof ne 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
  • enum et class survivent 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

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.