TypeScript le système de types - 09 - Null safety : strictNullChecks, optional chaining, NonNullable

Comment TypeScript gere null et undefined. strictNullChecks, optional chaining, nullish coalescing, et les patterns pour eliminer null proprement.

09 - Null safety : strictNullChecks, optional chaining, NonNullable

Ce que tu vas apprendre

  • Ce que strictNullChecks change et pourquoi c'est non-negociable
  • La différence entre null et undefined en TypeScript
  • Optional chaining (?.) et nullish coalescing (??)
  • Le type NonNullable<T> et le pattern assertion function pour eliminer null

Prerequisites

Avoir lu l'article sur les type guards.


Le billion-dollar mistake

Tony Hoare, l'inventeur de la référencé null en 1965, l'a appele son "billion-dollar mistake". Cinquante ans plus tard, null et undefined restent la première cause de crash en JavaScript : TypeError: Cannot read properties of undefined.

TypeScript peut te protéger de ca. Mais seulement si strictNullChecks est active.

strictNullChecks : le garde-fou

Sans strictNullChecks (ou avec strict: false), null et undefined sont assignables a tous les types :

typescript// tsconfig: strictNullChecks: false
const name: string = null      // ✅ compile
const age: number = undefined  // ✅ compile

C'est comme si null et undefined etaient invisibles pour le compilateur. Le type string accepte silencieusement null. Tu perds toute la protection.

Avec strictNullChecks: true (inclus dans strict: true) :

typescript// tsconfig: strict: true
const name: string = null      // ❌ Type 'null' is not assignable to type 'string'
const age: number = undefined  // ❌ Type 'undefined' is not assignable to type 'number'

Si une variable peut etre null, tu dois l'exprimer dans le type :

typescriptconst name: string | null = null // ✅ explicite

Et le compilateur te force a vérifier avant d'utiliser la valeur :

typescriptfunction greet(name: string | null) {
  console.log(name.toUpperCase()) // ❌ name is possibly 'null'

  if (name !== null) {
    console.log(name.toUpperCase()) // ✅ narrowe a string
  }
}

Mon avis : strict: true est non-negociable dans tout nouveau projet TypeScript. Si tu herites d'un projet sans strict, c'est la première migration a faire. L'article sur la migration JS vers TS couvre cette transition.

null vs undefined

JavaScript a deux "absences de valeur". C'est une bizarrerie historique, mais il faut vivre avec.

typescriptlet a: string | null = null       // absence explicite
let b: string | undefined = undefined // non initialise / absent
let c: string | undefined         // equivalent — undefined par defaut

En pratique, les conventions varient selon les ecosystemes :

  • Les APIs JSON ne connaissent pas undefined (un champ absent est omis, pas undefined)
  • Les paramètres optionnels de fonctions sont undefined quand non fournis
  • Map.get() retourne T | undefined
  • Les acces a des propriétés optionnelles retournent T | undefined
  • Prisma retourne null pour les champs nullable de la base
  • DOM : document.getElementById() retourne HTMLElement | null

Ma convention personnelle : j'utilise null quand l'absence est une valeur intentionnelle (un champ "pas encore rempli"), et undefined pour "pas fourni / pas present". Mais le plus important, c'est d'etre coherent dans un projet.

Optional chaining : ?.

L'optional chaining permet d'acceder a des propriétés potentiellement nullables sans crash :

typescriptinterface User {
  name: string
  address?: {
    city: string
    zipCode?: string
  }
}

const user: User = { name: "Nicolas" }

// Sans optional chaining
const city = user.address !== undefined ? user.address.city : undefined

// Avec optional chaining
const city = user.address?.city // type: string | undefined

Ca fonctionne sur les propriétés, les méthodes, et les index :

typescript// Propriete
user.address?.city

// Methode
const length = someArray?.length
const upper = someString?.toUpperCase()

// Index
const first = someArray?.[0]

// Chaine
user.address?.city?.toUpperCase()

Si un maillon de la chaîne est null ou undefined, l'expression retourne undefined sans lancer d'erreur. TypeScript ajuste le type en consequence.

Attention au short-circuit

L'optional chaining court-circuite toute l'expression a droite :

typescriptconst result = obj?.method().property
// si obj est null, method() n'est PAS appelee
// result est undefined

Ce n'est pas équivalent a :

typescriptconst result = (obj?.method()).property
// ❌ si obj est null, (undefined).property crash

Nullish coalescing : ??

?? retourne la valeur de droite si la gauche est null ou undefined :

typescriptconst port = config.port ?? 3000
// si config.port est null ou undefined → 3000
// si config.port est 0 → 0 (pas 3000)

La différence avec || est importante. || considéré 0, "", false, NaN comme falsy :

typescriptconst port = config.port || 3000
// si config.port est 0 → 3000 ❌ (0 est falsy)

const port = config.port ?? 3000
// si config.port est 0 → 0 ✅ (0 n'est ni null ni undefined)

J'ai vu ce bug dans un projet ou un seuil de tolérance etait a 0 et le || le remplacait par la valeur par défaut. ?? est la valeur sure.

Combiner ?. et ??

typescriptconst city = user.address?.city ?? "Ville inconnue"
// si address est undefined → "Ville inconnue"
// si address existe mais city est undefined → "Ville inconnue"
// sinon → la ville

NonNullable

Le type utilitaire NonNullable retire null et undefined d'un type :

typescripttype MaybeString = string | null | undefined
type DefiniteString = NonNullable<MaybeString> // string

Utile dans les signatures de fonctions :

typescriptfunction process<T>(items: (T | null)[]): NonNullable<T>[] {
  return items.filter((item): item is NonNullable<T> => item !== null)
}

const names = process(["Alice", null, "Bob", null])
// type: string[]

Note le type guard item is NonNullable<T> dans le filter. Sans ca, TypeScript ne sait pas que le filter elimine les null et garde le type (string | null)[].

Le non-null assertion operator : !

L'opérateur ! postfixe dit au compilateur "je sais que c'est pas null" :

typescriptconst el = document.getElementById("app")! // type: HTMLElement (pas HTMLElement | null)

C'est un cast deguise. Si l'élément n'existe pas, tu as un crash runtime. Je déconseillé son usage sauf dans les cas ou tu contrôles le HTML (tests, scripts de build). Prefere un check explicite :

typescriptconst el = document.getElementById("app")
if (!el) {
  throw new Error('Element #app introuvable')
}
// el est HTMLElement ici grace au narrowing

Ou une assertion function réutilisable :

typescriptfunction assertElement(id: string): HTMLElement {
  const el = document.getElementById(id)
  if (!el) throw new Error(`Element #${id} introuvable`)
  return el
}

const app = assertElement("app") // type: HTMLElement, throw si absent

Propriétés optionnelles vs | undefined

Deux syntaxes qui semblent identiques mais ne le sont pas :

typescriptinterface A {
  name?: string // la propriete peut etre absente
}

interface B {
  name: string | undefined // la propriete doit etre presente, mais sa valeur peut etre undefined
}

const a: A = {} // ✅ name absente
const b: B = {} // ❌ Property 'name' is missing
const b2: B = { name: undefined } // ✅

Avec exactOptionalPropertyTypes: true dans le tsconfig (recommande), la distinction est encore plus stricte : une propriété name? n'accepte pas undefined comme valeur explicite.


Résumé

  • strictNullChecks (inclus dans strict: true) est non-negociable — sans lui, null est invisible pour le compilateur
  • ?. (optional chaining) évité les crashs sur les acces a des propriétés nullable
  • ?? (nullish coalescing) est supérieur a || pour les valeurs par défaut car il ne traite pas 0, "", false comme des absences
  • NonNullable<T> retire null et undefined d'un type
  • Evite ! (non-null assertion) — préféré un check explicite ou une assertion function

Article précédent : 08 - Type guards

Article suivant : 10 - Utility types

Sources

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