TypeScript le système de types - 08 - Type guards : is, asserts, instanceof, in

Comment écrire des type guards pour aider TypeScript a narrower les types. Custom type guards avec is et asserts, typeof, instanceof, in.

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é

  • typeof narrow les primitifs, instanceof les classes, in les 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

Sources

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