Votre vérification de types a réussi. Votre application a planté quand même.

Vous avez activé strict: true dans tsconfig.json. Vous avez corrigé chaque soulignement rouge. Vous avez mis en production en étant convaincu que null et undefined étaient des problèmes résolus.

Puis une réponse du backend a changé de forme, une requête DOM n’a rien retourné, et user.profile.name a déclenché Cannot read properties of null dans le code même que TypeScript vous avait assuré être sûr. Que s’est-il passé ?

Les vérifications strictes de null de TypeScript sont un contrat à la compilation. Elles vérifient que les types que vous avez écrits sont cohérents en interne. Elles ne vérifient pas que les données arrivant à l’exécution correspondent à ces types. L’écart entre ces deux choses est là où vivent la plupart des plantages en production liés à null.

Où strictNullChecks aide réellement

Quand vous activez strictNullChecks, TypeScript traite null et undefined comme des types distincts qui doivent être traités explicitement.

// Without strictNullChecks, this compiles. With it, you get a type error.
function greet(name: string) {
  console.log(name.toUpperCase())
}

greet(null) // Error: Argument of type 'null' is not assignable to parameter of type 'string'.

Cela est réellement utile. Cela attrape les null que vous introduisez dans votre propre code : variables non initialisées, valeurs de retour manquantes, cas par défaut oubliés.

L’astuce, c’est que cela ne fonctionne que sur le code que TypeScript peut voir à la compilation. Une fois que votre application tourne, TypeScript a disparu. Le système de types s’évapore. Ce qui reste, c’est du JavaScript pur, et le JavaScript pur laissera volontiers null passer par n’importe quel trou que vous avez laissé ouvert.

Les quatre failles à l’exécution que strictNullChecks ne peut pas combler

1. Les réponses d’API prétendent correspondre à vos types

Vous avez typé votre réponse d’API. TypeScript vous a cru.

interface User {
  id: number
  profile: {
    name: string
    avatar: string
  }
}

async function fetchUser(): Promise<User> {
  const res = await fetch('/api/user')
  return res.json() as User // TypeScript trusts this cast. The backend does not.
}

const user = await fetchUser()
console.log(user.profile.name) // Crashes if profile is null

L’assertion as User dit à TypeScript d’arrêter de poser des questions. Elle ne valide pas la charge utile. Si le backend retourne { id: 1, profile: null }, TypeScript a compilé votre code contre un mensonge. Le plantage est réel.

2. Les requêtes DOM retournent null par conception

const button = document.getElementById('submit')
button.addEventListener('click', handleClick) // Crashes if the element is missing

TypeScript sait que getElementById retourne HTMLElement | null. En mode strict, il vous force à gérer le cas null. Beaucoup de développeurs contournent cela avec une assertion de non-nullité :

const button = document.getElementById('submit')!

Le ! est une promesse faite à TypeScript que vous en savez plus que lui. Quand vous vous trompez, le plantage atterrit en production.

3. noUncheckedIndexedAccess est désactivé par défaut

Même avec strict: true, TypeScript ne signale pas l’accès par index à un tableau ou un objet comme potentiellement undefined.

const users: User[] = []
const first = users[0]
first.name // No type error, but first is undefined at runtime

Il en va de même pour les records :

const cache: Record<string, string> = {}
const value = cache['missing'] // Type: string. Runtime value: undefined.

Vous avez besoin de noUncheckedIndexedAccess: true dans votre tsconfig.json pour combler ce trou. La plupart des équipes ne l’activent jamais parce que cela rend le quotidien plus bruyant. Ce bruit est le but recherché.

4. any et les assertions de type désactivent tout

const data: any = JSON.parse(raw)
const user = data.user as User // All null checks are now voluntarily disabled

Chaque any ou as dans votre codebase est une porte que vous avez laissée déverrouillée. TypeScript s’arrête au seuil. Ce qui passe de l’autre côté est de votre responsabilité.

Ce qui fonctionne réellement à l’exécution

Le travail de TypeScript est d’attraper les erreurs dans le code que vous écrivez. La sécurité à l’exécution nécessite une deuxième couche de défenses.

Valider à la frontière

Utilisez une bibliothèque de schémas comme Zod pour valider les données externes avant qu’elles n’entrent dans votre système typé.

import { z } from 'zod'

const UserSchema = z.object({
  id: z.number(),
  profile: z.object({
    name: z.string(),
    avatar: z.string().optional(),
  }).nullable(),
})

const res = await fetch('/api/user')
const raw = await res.json()
const user = UserSchema.parse(raw) // Throws if the shape is wrong

// user.profile is typed as { name: string; avatar?: string } | null
if (user.profile) {
  console.log(user.profile.name) // Safe
}

Le coût, c’est une étape de validation à l’exécution à chaque frontière. Le bénéfice, c’est que vos types ne sont plus de la pensée magique. Ils sont appliqués.

Utiliser noUncheckedIndexedAccess

Activez-le. Acceptez la friction.

// With noUncheckedIndexedAccess
const first = users[0] // Type: User | undefined
if (first) {
  console.log(first.name) // Safe
}

Oui, cela ajoute des vérifications partout où vous accédez par index. C’est exactement ce que vous voulez si vous êtes sérieux au sujet de la sécurité des null.

Privilégier le chaînage optionnel, pas les assertions de non-nullité

// Dangerous
const name = user.profile!.name

// Safe
const name = user.profile?.name ?? 'Anonymous'

Le chaînage optionnel court-circuite sur null ou undefined. L’opérateur de coalescence des nullish fournit une valeur de repli. Ce motif est défensif par défaut et explicite sur ce qui se passe quand des données manquent.

Gardez any hors de votre flux de données

Traitez any comme une substance toxique. Si vous devez l’utiliser, confinez-le à la portée la plus petite possible et validez le résultat immédiatement.

// Bad: any propagates
const data: any = JSON.parse(raw)
processData(data)

// Better: validate and narrow immediately
const parsed = JSON.parse(raw) as unknown
const data = UserSchema.parse(parsed)
processData(data)

unknown vous force à prouver la forme avant que TypeScript ne vous laisse l’utiliser. C’est any avec des conséquences.

Pourquoi les équipes ignorent ces défenses

La plupart des équipes ne les ignorent pas par ignorance. Elles les ignorent parce que les motifs plus sûrs ajoutent de la friction à chaque accès aux données, chaque appel d’API, chaque requête DOM. Le code s’allonge. Les types deviennent plus bruyants. La tentation de coller un ! ou un as sur le problème grandit à chaque deadline.

L’alternative, ce sont des plantages en production que TypeScript vous avait promis de ne pas avoir. Le compilateur a tenu sa part du marché. Il a vérifié votre code contre les types que vous avez fournis. Il ne peut pas vérifier votre code contre la réalité.

La solution, ce sont deux couches, pas une

strictNullChecks est la couche une. Elle attrape les null que vous introduisez vous-même. La couche deux, c’est tout le reste : validation de schémas, indexation défensive, chaînage optionnel, et la discipline d’éviter any.

Si votre application TypeScript plante toujours avec des null, le compilateur ne vous fait pas défaut. C’est l’écart entre vos types et vos données à l’exécution qui vous fait défaut. Comblez cet écart aux frontières, activez noUncheckedIndexedAccess, et arrêtez de faire confiance aux backends pour correspondre à vos interfaces. TypeScript restera strict. Mais au moins, vos types décriront le monde tel qu’il arrive réellement.