06 - satisfies, as const et const generics
Ce que tu vas apprendre
satisfies: valider un type sans perdre la precision de l'inferenceas consten profondeur : au-delà de ce qu'on a vu dans la serie 1- Les const type parameters (TypeScript 5.0) pour inferer des literals dans les generics
- Quand utiliser chacun et les combinaisons
Prerequisites
Avoir lu les articles sur l'inference et widening et les enums vs unions.
Le dilemme annotation vs inference
Un problème recurrent en TypeScript : tu veux valider qu'un objet respecte un type, mais tu veux aussi garder le type infere (plus precis).
typescripttype Route = {
path: string
method: "GET" | "POST" | "PUT" | "DELETE"
}
// Option 1 : annotation — perd la precision
const routes: Record<string, Route> = {
getUser: { path: "/api/users/:id", method: "GET" },
createUser: { path: "/api/users", method: "POST" }
}
routes.getUser // ✅ type: Route
routes.deleteUser // ✅ compile — Record<string, Route> accepte n'importe quelle cle
// Option 2 : pas d'annotation — perd la validation
const routes = {
getUser: { path: "/api/users/:id", method: "GET" },
createUser: { path: "/api/users", method: "POST" }
}
// Pas de verification que chaque entree est bien un Route
// Une typo dans method passerait inapercue
Avec l'annotation, tu perds l'autocompletion des clés (getUser, createUser). Sans annotation, tu perds la validation du type Route.
satisfies : valider sans elargir
satisfies (TypeScript 4.9) resout ce dilemme. Il vérifié que la valeur est compatible avec un type, mais garde le type infere :
typescriptconst routes = {
getUser: { path: "/api/users/:id", method: "GET" },
createUser: { path: "/api/users", method: "POST" }
} satisfies Record<string, Route>
routes.getUser // ✅ type: { path: string; method: "GET" }
routes.deleteUser // ❌ Property 'deleteUser' does not exist
routes.getUser.method // type: "GET" (pas "GET" | "POST" | "PUT" | "DELETE")
Le compilateur vérifié que l'objet est un Record<string, Route> valide. Mais le type infere garde les clés exactes et les valeurs literals. Tu as la validation ET la precision.
satisfies avec des unions
typescripttype Color = { r: number; g: number; b: number } | string
const palette = {
primary: { r: 0, g: 122, b: 255 },
secondary: "hsl(210, 100%, 50%)",
danger: { r: 255, g: 0, b: 0 }
} satisfies Record<string, Color>
// Le type de palette.primary est { r: number; g: number; b: number }, pas Color
palette.primary.r // ✅ pas besoin de narrowing
palette.secondary.toUpperCase() // ✅ TypeScript sait que c'est un string
Sans satisfies, si tu annotais Record<string, Color>, chaque acces aurait le type Color (union) et il faudrait narrower a chaque fois.
as const en profondeur
On a vu as const dans la serie 1. Quelques usages avances.
as const sur les paramètres de fonctions (TypeScript 5.0)
typescriptfunction defineRoutes<const T extends Record<string, { path: string; method: string }>>(routes: T): T {
return routes
}
const routes = defineRoutes({
getUser: { path: "/api/users/:id", method: "GET" },
createUser: { path: "/api/users", method: "POST" }
})
// routes.getUser.method est "GET", pas string
Le const devant T dans <const T> dit a TypeScript d'inferer le type le plus precis possible (comme si l'argument avait as const). C'est les const type parameters.
satisfies + as const
La combinaison ultime : validation + precision maximale + immutabilité.
typescriptconst config = {
port: 3000,
host: "localhost",
features: ["auth", "billing", "notifications"]
} as const satisfies {
port: number
host: string
features: readonly string[]
}
// config.port est 3000 (pas number)
// config.features est readonly ["auth", "billing", "notifications"] (pas string[])
// Et le compilateur a verifie la structure
L'ordre est as const satisfies Type. as const fige les literals, satisfies valide la structure.
Const type parameters
Avant TypeScript 5.0, pour inferer les literals dans les generics, tu devais demander as const a l'appelant :
typescript// Avant 5.0
function createConfig<T>(config: T): T { return config }
const c = createConfig({ port: 3000 }) // port: number 😞
const c2 = createConfig({ port: 3000 } as const) // port: 3000 ✅ mais verbeux
// Depuis 5.0
function createConfig<const T>(config: T): T { return config }
const c = createConfig({ port: 3000 }) // port: 3000 ✅ sans as const
Le const dans <const T> dit au compilateur : "infere T comme si l'argument avait as const". L'appelant n'a rien a faire.
C'est utile pour les builders et les fonctions de configuration :
typescriptfunction defineEndpoint<const T extends {
method: string
path: string
response: unknown
}>(config: T) {
return config
}
const endpoint = defineEndpoint({
method: "GET",
path: "/api/users",
response: {} as User[]
})
// endpoint.method est "GET", pas string
// endpoint.path est "/api/users", pas string
Sur paltemps.fr, j'utilise les const generics dans les fonctions de définition de routes API. Ca permet d'avoir l'autocompletion des méthodes et des paths sans annotations manuelles.
Quand utiliser quoi
| Outil | Utilise quand | Effet |
|---|---|---|
Annotation (: Type) |
Le type est plus important que la precision | Elargit au type annote |
as const |
Tu veux figer les literals et rendre readonly | Infere le type le plus precis |
satisfies Type |
Tu veux valider ET garder la precision | Valide sans elargir |
<const T> |
Tu veux des literals dans les generics | L'appelant n'a pas a écrire as const |
as const satisfies |
Tu veux tout : validation + precision + immutabilité | Le combo maximal |
Résumé
satisfiesvalide qu'une valeur respecte un type sans perdre la precision de l'inferenceas constfige les literals et rend tout readonly — utile pour les configurations et les constantes- Les const type parameters (
<const T>) inferent les literals dans les generics sans que l'appelant ecriveas const as const satisfies Typecombine validation, precision et immutabilité- Utilise l'annotation classique (
: Type) uniquement quand la precision n'est pas nécessaire
Article précédent : 05 - Branded types
Article suivant : 07 - Types recursifs