09 - Null safety : strictNullChecks, optional chaining, NonNullable
Ce que tu vas apprendre
- Ce que
strictNullCheckschange et pourquoi c'est non-negociable - La différence entre
nulletundefineden 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, pasundefined) - Les paramètres optionnels de fonctions sont
undefinedquand non fournis Map.get()retourneT | undefined- Les acces a des propriétés optionnelles retournent
T | undefined - Prisma retourne
nullpour les champs nullable de la base - DOM :
document.getElementById()retourneHTMLElement | 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 dansstrict: true) est non-negociable — sans lui,nullest 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 pas0,"",falsecomme des absencesNonNullable<T>retirenulletundefinedd'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