13 - Tuples : labeled, variadic et rest éléments
Ce que tu vas apprendre
- Ce qu'est un tuple et en quoi il différé d'un tableau
- Les labeled tuples pour documenter les positions
- Les variadic tuple types pour manipuler des tuples génériques
- Les rest éléments dans les tuples
- Les patterns courants : retours de fonctions, paramètres, destructuration
Prerequisites
Avoir lu les articles sur les enums vs unions et l'inference.
Tableau vs tuple
Un tableau string[] accepte n'importe quel nombre de strings. Un tuple a une longueur fixe et chaque position a son propre type.
typescript// Tableau : longueur variable, type uniforme
const names: string[] = ["Alice", "Bob", "Charlie"]
names.push("Dave") // ✅
// Tuple : longueur fixe, type par position
const pair: [string, number] = ["Nicolas", 31]
pair.push("extra") // ✅ compile (piege — voir plus bas)
pair[0] // type: string
pair[1] // type: number
pair[2] // ❌ Tuple type '[string, number]' of length '2' has no element at index '2'
Le cas d'usage le plus connu : useState de React.
typescriptconst [count, setCount] = useState(0)
// type de retour : [number, Dispatch<SetStateAction<number>>]
Le retour est un tuple [T, setter]. La destructuration donne un type precis a chaque variable. Si useState retournait un tableau, count et setCount auraient tous les deux le type number | Dispatch<...>.
Le piège du push sur un tuple
TypeScript ne bloque pas push sur un tuple :
typescriptconst t: [string, number] = ["hello", 42]
t.push("extra") // ✅ compile
console.log(t) // ["hello", 42, "extra"]
console.log(t.length) // 3 au runtime, mais le type dit 2
C'est une limitation connue. Le type du tuple dit "longueur 2" mais les méthodes mutantes du tableau ne sont pas restreintes. Si ca te derange, utilise readonly :
typescriptconst t: readonly [string, number] = ["hello", 42]
t.push("extra") // ❌ Property 'push' does not exist on type 'readonly [string, number]'
Labeled tuples
Depuis TypeScript 4.0, tu peux nommer les positions d'un tuple. Ca ne change pas le comportement, mais ca ameliore la lisibilité dans l'IDE et les messages d'erreur :
typescripttype Point = [x: number, y: number]
type Range = [start: number, end: number]
type Entry = [key: string, value: unknown]
Sans labels, l'IDE affiche [number, number]. Avec labels, il affiche [x: number, y: number]. Quand tu as une fonction qui retourne un tuple, ca fait la différence :
typescriptfunction getRange(data: number[]): [min: number, max: number] {
return [Math.min(...data), Math.max(...data)]
}
const [min, max] = getRange([3, 1, 7, 2])
// L'IDE montre min: number (label: min) et max: number (label: max)
Optional éléments
Les tuples peuvent avoir des éléments optionnels :
typescripttype HttpResponse = [status: number, body?: string, headers?: Record<string, string>]
const ok: HttpResponse = [200]
const withBody: HttpResponse = [200, '{"id": 1}']
const full: HttpResponse = [200, '{"id": 1}', { "content-type": "application/json" }]
Les éléments optionnels doivent etre a la fin (comme les paramètres de fonctions).
Rest éléments
Un tuple peut avoir un rest élément qui capture un nombre variable d'éléments :
typescripttype LogEntry = [timestamp: Date, level: string, ...messages: string[]]
const entry: LogEntry = [new Date(), "info", "Server started", "on port 3000"]
Le rest élément peut etre au milieu depuis TypeScript 4.2 :
typescripttype Sandwich = [top: string, ...fillings: string[], bottom: string]
const blt: Sandwich = ["pain", "bacon", "laitue", "tomate", "pain"]
TypeScript sait que le premier et le dernier éléments sont des strings, et tout ce qui est entre est le rest.
Typer les paramètres rest d'une fonction
Les tuples permettent de typer precisement les ...args :
typescriptfunction log(level: string, ...args: [message: string, ...data: unknown[]]): void {
console.log(`[${level}]`, ...args)
}
log("info", "User logged in", { userId: "123" }) // ✅
log("error") // ❌ il manque au moins message
Variadic tuple types
Depuis TypeScript 4.0, tu peux manipuler des tuples avec des generics. C'est le mecanisme derrière les types de fonctions comme concat, curry, ou les builders.
Concat de tuples
typescripttype Concat<A extends unknown[], B extends unknown[]> = [...A, ...B]
type Result = Concat<[1, 2], [3, 4]> // [1, 2, 3, 4]
type Mixed = Concat<[string], [number, boolean]> // [string, number, boolean]
Prepend et Append
typescripttype Prepend<T, Arr extends unknown[]> = [T, ...Arr]
type Append<Arr extends unknown[], T> = [...Arr, T]
type A = Prepend<string, [number, boolean]> // [string, number, boolean]
type B = Append<[number, boolean], string> // [number, boolean, string]
Application concrète : typer un pipe
Un pattern avance mais concret. On veut typer une fonction pipe ou la sortie de chaque fonction est l'entree de la suivante :
typescriptfunction pipe<A, B>(value: A, fn1: (a: A) => B): B
function pipe<A, B, C>(value: A, fn1: (a: A) => B, fn2: (b: B) => C): C
function pipe<A, B, C, D>(value: A, fn1: (a: A) => B, fn2: (b: B) => C, fn3: (c: C) => D): D
function pipe(value: unknown, ...fns: Function[]): unknown {
return fns.reduce((acc, fn) => fn(acc), value)
}
const result = pipe(
" hello ",
(s: string) => s.trim(),
(s: string) => s.toUpperCase(),
(s: string) => s.length
)
// type: number ✅
Les overloads avec des tuples de generics permettent de garder le typage à travers la chaîne. Les overloads sont couverts dans la sous-serie types avances.
Extraire des types depuis un tuple
Tu peux acceder aux types d'un tuple par index :
typescripttype Entry = [string, number, boolean]
type First = Entry[0] // string
type Second = Entry[1] // number
type Third = Entry[2] // boolean
Pour extraire l'union de tous les types :
typescripttype All = Entry[number] // string | number | boolean
C'est le pattern qu'on a vu dans l'article sur les enums avec typeof ROLES[number].
Head et Tail
typescripttype Head<T extends unknown[]> = T extends [infer H, ...unknown[]] ? H : never
type Tail<T extends unknown[]> = T extends [unknown, ...infer R] ? R : never
type H = Head<[string, number, boolean]> // string
type T = Tail<[string, number, boolean]> // [number, boolean]
infer capture un type dans un pattern. C'est couvert en détail dans l'article sur les conditional types.
Patterns courants
Retour de fonction avec destructuration
typescriptfunction useToggle(initial: boolean): [value: boolean, toggle: () => void] {
let value = initial
const toggle = () => { value = !value }
return [value, toggle]
}
const [isOpen, toggleOpen] = useToggle(false)
Entries d'un objet
typescriptconst entries: [string, number][] = Object.entries({ a: 1, b: 2 })
Coordonnees et dimensions
typescripttype Point2D = [x: number, y: number]
type Point3D = [x: number, y: number, z: number]
type Size = [width: number, height: number]
function distance(a: Point2D, b: Point2D): number {
return Math.sqrt((b[0] - a[0]) ** 2 + (b[1] - a[1]) ** 2)
}
Sur paltemps.fr, j'utilise des tuples pour les coordonnees GPS des lieux : [lat: number, lng: number]. C'est plus leger qu'un objet { lat, lng } pour des calculs en masse.
Résumé
- Un tuple est un tableau a longueur fixe ou chaque position a son propre type
- Les labeled tuples ameliorent la lisibilité sans changer le comportement
- Les rest éléments permettent des tuples de longueur variable avec des types fixes aux extremites
- Les variadic tuple types permettent de manipuler des tuples avec des generics (concat, prepend, append)
readonlysur un tuple bloque les méthodes mutantes commepushTuple[number]extrait l'union de tous les types du tuple
Article précédent : 12 - Enums vs unions litterales
Article suivant : 14 - keyof, typeof et index access types