TypeScript types avances - 06 - satisfies, as const et const generics

Les trois outils de precision recents de TypeScript. Comment satisfies valide sans elargir, as const fige les literals, et const generics infere les literals.

06 - satisfies, as const et const generics

Ce que tu vas apprendre

  • satisfies : valider un type sans perdre la precision de l'inference
  • as const en 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é

  • satisfies valide qu'une valeur respecte un type sans perdre la precision de l'inference
  • as const fige 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 ecrive as const
  • as const satisfies Type combine 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

Sources

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