08 - Type guards : is, asserts, instanceof, in
Ce que tu vas apprendre
- Les type guards natifs :
typeof,instanceof,in, egalite - Comment écrire un custom type guard avec
is - Comment écrire une assertion function avec
asserts - Quand choisir lequel et les pièges a éviter
Prerequisites
Avoir lu les articles sur le narrowing et les discriminated unions.
Le narrowing ne suffit pas toujours
L'article sur le narrowing montrait que TypeScript sait réduire un type apres un typeof ou un if. Mais ca ne marche que pour les cas simples. Quand tu veux vérifier si un objet est d'un type spécifique, typeof ne sert a rien :
typescriptinterface User {
id: string
name: string
email: string
}
interface Bot {
id: string
name: string
model: string
}
type Actor = User | Bot
function greet(actor: Actor) {
if (typeof actor === "object") {
// Toujours Actor — typeof object ne distingue pas User de Bot
}
}
typeof ne connaît que 8 types : "string", "number", "boolean", "bigint", "symbol", "undefined", "object", "function". Tous les objets, tableaux, null inclus, retournent "object". Il faut d'autres outils.
Le opérateur in
in vérifié si une propriété existe dans un objet. TypeScript l'utilise pour narrower :
typescriptfunction greet(actor: Actor) {
if ("email" in actor) {
// actor est User — seul User a un champ email
console.log(`Salut ${actor.name}, on t'envoie un mail a ${actor.email}`)
} else {
// actor est Bot
console.log(`Bot ${actor.name} utilise le modele ${actor.model}`)
}
}
Ca fonctionne parce que email existe dans User mais pas dans Bot. TypeScript deduit le type par elimination.
Limite : si la propriété existe dans les deux types, in ne narrow pas.
instanceof
instanceof vérifié la chaîne de prototypes. Ca fonctionne avec les classes :
typescriptclass HttpError {
constructor(public status: number, public message: string) {}
}
class ValidationError {
constructor(public fields: Record<string, string>) {}
}
type AppError = HttpError | ValidationError
function handleError(err: AppError) {
if (err instanceof HttpError) {
// err est HttpError
console.log(`HTTP ${err.status}: ${err.message}`)
} else {
// err est ValidationError
console.log(`Champs invalides : ${Object.keys(err.fields).join(", ")}`)
}
}
instanceof ne fonctionne pas avec les interfaces et les types. Les interfaces n'existent pas au runtime (type erasure). Si tu as besoin de distinguer des interfaces, utilise un discriminant ou un custom type guard.
typescript// ❌ Impossible — les interfaces n'existent pas au runtime
if (actor instanceof User) { } // User n'est pas une valeur
Custom type guards avec is
Un type guard custom est une fonction qui retourne un boolean et dont le type de retour utilise le mot-clé is :
typescriptfunction isUser(actor: Actor): actor is User {
return "email" in actor
}
function greet(actor: Actor) {
if (isUser(actor)) {
// actor est User ✅
console.log(actor.email)
} else {
// actor est Bot ✅
console.log(actor.model)
}
}
Le actor is User dans le retour dit au compilateur : "si cette fonction retourne true, alors actor est de type User". C'est un contrat entre toi et le compilateur.
Le compilateur te fait confiance. Si ton guard est faux, tu introduis un bug de typage :
typescript// ❌ Guard menteur — compile mais crash au runtime
function isUser(actor: Actor): actor is User {
return true // retourne toujours true, meme pour un Bot
}
C'est un pouvoir qui vient avec une responsabilité. Teste tes type guards.
Type guards pour la validation de donnees externes
Le cas d'usage le plus frequent : valider des donnees qui viennent de l'extérieur (API, JSON, query params).
typescriptinterface ApiUser {
id: number
name: string
email: string
}
function isApiUser(data: unknown): data is ApiUser {
return (
typeof data === "object" &&
data !== null &&
"id" in data &&
typeof (data as Record<string, unknown>).id === "number" &&
"name" in data &&
typeof (data as Record<string, unknown>).name === "string" &&
"email" in data &&
typeof (data as Record<string, unknown>).email === "string"
)
}
const response = await fetch("/api/user/1")
const json: unknown = await response.json()
if (isApiUser(json)) {
// json est ApiUser — safe
console.log(json.name)
} else {
throw new Error("Format de reponse invalide")
}
C'est verbeux. Pour des structures complexes, Zod fait ca en une ligne (couvert dans la sous-serie pratique). Mais le mecanisme sous-jacent est le meme.
Assertion functions avec asserts
Une assertion function ne retourne rien si la condition est vraie, et lance une erreur si elle est fausse :
typescriptfunction assertIsUser(actor: Actor): asserts actor is User {
if (!("email" in actor)) {
throw new Error("Expected User, got Bot")
}
}
function processUser(actor: Actor) {
assertIsUser(actor) // lance si ce n'est pas un User
// Apres l'assertion, actor est User pour le reste de la fonction
console.log(actor.email) // ✅
}
La différence avec un type guard is : le guard retourne un boolean et tu decides quoi faire. L'assertion lance une erreur si la condition echoue et narrow le type pour toute la suite du scope.
asserts sans type predicate
Tu peux aussi utiliser asserts pour valider une condition sans changer le type :
typescriptfunction assertDefined<T>(value: T | null | undefined, name: string): asserts value is T {
if (value === null || value === undefined) {
throw new Error(`${name} est null ou undefined`)
}
}
function getUser(id: string) {
const user = db.findUser(id) // type: User | null
assertDefined(user, "user")
// user est User ici — le null a ete elimine
return user.name
}
C'est plus propre que if (!user) throw répété partout. L'assertion est réutilisable et le narrowing est automatique.
Comparaison des approches
| Guard | Fonctionne sur | Runtime | Narrow |
|---|---|---|---|
typeof |
Primitifs | Oui | Oui |
instanceof |
Classes | Oui | Oui |
in |
Propriétés d'objets | Oui | Oui |
=== / !== |
Valeurs literals | Oui | Oui |
is (custom) |
N'importe quoi | Oui (ta logique) | Oui |
asserts |
N'importe quoi | Oui (throw si faux) | Oui |
Combinaisons de guards
Tu peux combiner plusieurs guards dans une meme condition :
typescriptfunction processInput(input: unknown) {
if (typeof input === "string") {
return input.toUpperCase()
}
if (typeof input === "number") {
return input.toFixed(2)
}
if (input instanceof Date) {
return input.toISOString()
}
if (Array.isArray(input)) {
return input.length
}
return String(input)
}
Array.isArray est un type guard intégré. TypeScript narrow le type a unknown[] apres le check.
Erreurs courantes
Guard trop permissif
typescript// ❌ Verifie juste une propriete — insuffisant
function isUser(data: unknown): data is User {
return typeof data === "object" && data !== null && "name" in data
}
// Un { name: "test" } sans id ni email passe le guard
Verifie toutes les propriétés requises, pas juste une.
Guard sur le mauvais type
typescript// ❌ Le parametre est deja type — le guard n'apporte rien
function isString(value: string): value is string {
return typeof value === "string"
}
Les type guards servent a narrower un type union ou unknown. Si le type est deja precis, le guard est inutile.
Résumé
typeofnarrow les primitifs,instanceofles classes,inles propriétés- Les custom type guards (
is) permettent d'écrire ta propre logique de narrowing - Les assertion functions (
asserts) lancent une erreur si la condition echoue et narrowent pour la suite du scope - Le compilateur te fait confiance sur les type guards — un guard faux introduit des bugs silencieux
- Pour des validations complexes de donnees externes, préféré Zod aux guards manuels
Article précédent : 07 - Discriminated unions
Article suivant : 09 - Null safety