TypeScript le système de types - 13 - Tuples : labeled, variadic et rest éléments

Comprendre les tuples en TypeScript. Tableaux a longueur fixe, labeled tuples, variadic tuple types et les patterns courants avec useState, les fonctions et les APIs.

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)
  • readonly sur un tuple bloque les méthodes mutantes comme push
  • Tuple[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

Sources

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