TypeScript le système de types - 14 - keyof, typeof et index access types

Comment extraire des types depuis d'autres types avec keyof, typeof et les index access types. Les opérateurs qui connectent le runtime et les types.

14 - keyof, typeof et index access types

Ce que tu vas apprendre

  • keyof : extraire les clés d'un type sous forme d'union
  • typeof : capturer le type d'une valeur runtime
  • Index access types : T[K] pour lire le type d'une propriété
  • Les combinaisons et patterns pour créer des types dynamiques

Prerequisites

Avoir lu les articles sur les generics et les tuples.


Le problème des strings magiques

Sur un projet client, un dev avait écrit un système de traduction :

typescriptfunction translate(key: string, lang: string): string {
  return translations[lang][key]
}

translate("welcom_message", "fr") // typo dans "welcome" — aucune erreur
translate("welcome_message", "xx") // langue inexistante — aucune erreur

Les deux paramètres sont string. Le compilateur ne peut pas vérifier que la clé existe dans les traductions ni que la langue est valide. Les erreurs ne se voient qu'au runtime, quand l'utilisateur voit undefined a la place du texte.

Avec keyof et typeof, tu lies les types aux donnees reelles :

typescriptconst translations = {
  fr: { welcome_message: "Bienvenue", logout: "Deconnexion" },
  en: { welcome_message: "Welcome", logout: "Logout" }
} as const

type Lang = keyof typeof translations        // "fr" | "en"
type Key = keyof typeof translations["fr"]    // "welcome_message" | "logout"

function translate(key: Key, lang: Lang): string {
  return translations[lang][key]
}

translate("welcom_message", "fr") // ❌ typo detectee a la compilation
translate("welcome_message", "xx") // ❌ "xx" n'est pas dans Lang
translate("welcome_message", "fr") // ✅

Zero duplication. Le type derive directement des donnees.

keyof : les clés d'un type

keyof T produit l'union des noms de propriétés de T :

typescriptinterface User {
  id: string
  name: string
  email: string
  age: number
}

type UserKey = keyof User // "id" | "name" | "email" | "age"

Ca fonctionne avec les interfaces, les types, et les types resolus :

typescripttype DictKey = keyof Record<string, unknown> // string
type ArrayKey = keyof string[]               // number | "length" | "push" | "pop" | ...
type TupleKey = keyof [string, number]       // "0" | "1" | number (+ methodes)

keyof avec des generics

Le pattern classique : une fonction qui accepte une clé d'un objet.

typescriptfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: "Nicolas", age: 31 }
const name = getProperty(user, "name")  // type: string
const age = getProperty(user, "age")    // type: number
getProperty(user, "email")              // ❌ "email" n'est pas dans keyof User

On a deja vu ce pattern dans l'article sur les generics. keyof est ce qui le rend possible.

keyof avec des index signatures

typescriptinterface StringMap {
  [key: string]: number
}

type K = keyof StringMap // string | number

Pourquoi number ? Parce que JavaScript convertit les index numériques en strings. obj[0] est équivalent a obj["0"]. Donc un index signature [key: string] accepte aussi les number.

typeof : capturer le type d'une valeur

typeof en position de type (pas dans une expression if (typeof x === "string")) capture le type TypeScript d'une valeur :

typescriptconst config = {
  port: 3000,
  host: "localhost",
  debug: false
}

type Config = typeof config
// { port: number; host: string; debug: boolean }

C'est utile quand le type n'est pas déclaré explicitement (pas d'interface) et que tu veux le réutiliser.

typeof sur des fonctions

typescriptfunction createUser(name: string, email: string) {
  return { id: crypto.randomUUID(), name, email, createdAt: new Date() }
}

type CreateUserFn = typeof createUser
// (name: string, email: string) => { id: string; name: string; email: string; createdAt: Date }

type NewUser = ReturnType<typeof createUser>
// { id: string; name: string; email: string; createdAt: Date }

ReturnType<typeof fn> est le combo le plus courant. Il extrait le type de retour sans que tu aies besoin de le définir dans une interface séparée.

typeof vs as const

typeof seul capture le type elargit. Combine avec as const, il capture les literals :

typescriptconst status = "active"
type A = typeof status // string (const, mais widened par typeof... non)
// En fait : type A = "active" car c'est un const

let status2 = "active"
type B = typeof status2 // string (widened car let)

const config = { port: 3000 } as const
type C = typeof config // { readonly port: 3000 }

const config2 = { port: 3000 }
type D = typeof config2 // { port: number }

Index access types : T[K]

Tu peux lire le type d'une propriété avec la syntaxe d'acces par clé :

typescriptinterface User {
  id: string
  name: string
  settings: {
    theme: "light" | "dark"
    lang: string
  }
}

type UserId = User["id"]           // string
type Theme = User["settings"]["theme"] // "light" | "dark"
type Settings = User["settings"]    // { theme: "light" | "dark"; lang: string }

Ca marche aussi avec des unions de clés :

typescripttype NameOrEmail = User["name" | "email"] // string (les deux sont string)
type IdOrSettings = User["id" | "settings"] // string | { theme: ...; lang: ... }

Et avec keyof :

typescripttype AllValues = User[keyof User]
// string | { theme: "light" | "dark"; lang: string }

Sur les tableaux et tuples

typescriptconst roles = ["admin", "editor", "viewer"] as const
type Role = typeof roles[number] // "admin" | "editor" | "viewer"

number comme index sur un tuple ou un tableau readonly donne l'union de tous les types d'éléments. C'est le pattern qu'on a vu dans l'article sur les enums.

Sur un tuple :

typescripttype Entry = [string, number, boolean]
type First = Entry[0]    // string
type Second = Entry[1]   // number
type All = Entry[number] // string | number | boolean

Combinaisons

Objet de configuration type-safe

typescriptconst ROUTES = {
  home: "/",
  profile: "/profile/:id",
  settings: "/settings",
  admin: "/admin/dashboard"
} as const

type RouteName = keyof typeof ROUTES         // "home" | "profile" | "settings" | "admin"
type RoutePath = typeof ROUTES[RouteName]    // "/" | "/profile/:id" | "/settings" | "/admin/dashboard"

function navigate(route: RouteName) {
  window.location.href = ROUTES[route]
}

navigate("home")    // ✅
navigate("login")   // ❌

Mapping type-safe

typescriptconst ERROR_CODES = {
  NOT_FOUND: { status: 404, message: "Ressource introuvable" },
  UNAUTHORIZED: { status: 401, message: "Non autorise" },
  FORBIDDEN: { status: 403, message: "Acces interdit" },
  INTERNAL: { status: 500, message: "Erreur interne" }
} as const

type ErrorCode = keyof typeof ERROR_CODES // "NOT_FOUND" | "UNAUTHORIZED" | ...
type ErrorInfo = typeof ERROR_CODES[ErrorCode] // { status: 404; message: "..." } | ...

function throwAppError(code: ErrorCode): never {
  const info = ERROR_CODES[code]
  throw new Error(`${info.status}: ${info.message}`)
}

Event handler type-safe

Sur paltemps.fr, j'utilise ce pattern pour les event emitters types :

typescriptinterface EventMap {
  "user:login": { userId: string; timestamp: Date }
  "user:logout": { userId: string }
  "order:created": { orderId: string; total: number }
}

function on<K extends keyof EventMap>(event: K, handler: (data: EventMap[K]) => void) {
  // ...
}

on("user:login", (data) => {
  // data est { userId: string; timestamp: Date } ✅
  console.log(data.userId)
})

on("order:created", (data) => {
  // data est { orderId: string; total: number } ✅
  console.log(data.total)
})

on("unknown:event", () => {}) // ❌ n'existe pas dans EventMap

Le type du handler est automatiquement infere à partir du nom de l'événement. Pas de cast, pas de any, pas de generics explicites a passer. keyof + index access + inference font tout le travail.

Limites

typeof ne fonctionne que sur des valeurs

typescripttype A = typeof someImportedFunction // ✅ — valeur
type B = typeof SomeInterface        // ❌ — une interface n'est pas une valeur

typeof travaille sur des identifiants qui existent au runtime. Les interfaces et types n'existent qu'a la compilation.

keyof ne donne que les propriétés connues

typescriptinterface Flexible {
  [key: string]: unknown
  name: string
}

type K = keyof Flexible // string | number (a cause de l'index signature)

L'index signature [key: string] ecrase les clés spécifiques dans le résultat de keyof. Tu perds l'information que name est une clé garantie.


Résumé

  • keyof T extrait l'union des noms de propriétés d'un type
  • typeof value capture le type TypeScript d'une valeur runtime
  • T[K] (index access) lit le type d'une propriété — fonctionne avec des unions de clés et keyof
  • as const + typeof + keyof est le trio pour deriver des types depuis des donnees reelles
  • Ces opérateurs eliminent la duplication entre les valeurs et les types

Article précédent : 13 - Tuples

Article suivant : 15 - Glossaire

Sources

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