14 - keyof, typeof et index access types
Ce que tu vas apprendre
keyof: extraire les clés d'un type sous forme d'uniontypeof: 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 Textrait l'union des noms de propriétés d'un typetypeof valuecapture le type TypeScript d'une valeur runtimeT[K](index access) lit le type d'une propriété — fonctionne avec des unions de clés etkeyofas const+typeof+keyofest 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