Uma tupla em TypeScript é um array com um número fixo de elementos, em que cada posição tem seu próprio tipo. [string, number] significa exatamente dois elementos: primeiro uma string, depois um número. Você escreve os tipos entre colchetes na ordem em que os valores aparecem.
Em tempo de execução uma tupla é um array comum do JavaScript. Tudo o que a tupla acrescenta (o tamanho fixo e o tipo de cada posição) é verificado pelo compilador e depois apagado.
Sintaxe de tupla
| Tipo de tupla | Aceita | Tipo de length |
|---|---|---|
[string, number] | exatamente uma string e depois um número | 2 |
[x: number, y: number] | o mesmo, com rótulos para facilitar a leitura | 2 |
[number, number, number?] | 2 ou 3 números | 2 | 3 |
[string, ...number[]] | uma string e depois qualquer quantidade de números | number |
[...string[], number] | qualquer quantidade de strings e depois um número | number |
readonly [number, number] | um par que não pode ser modificado | 2 |
[] | só um array vazio | 0 |
Cada forma é explicada abaixo. Vale reparar no tipo de length: em uma tupla fixa ele é um tipo literal, então o compilador sabe que pair.length é exatamente 2.
O que o compilador verifica
Um tipo de tupla fixa a quantidade de elementos, a ordem deles e o tipo de cada posição. Errar qualquer um desses é erro de compilação:
index.ts(2,7): error TS2322: Type '[string]' is not assignable to type '[string, number]'.
Source has 1 element(s) but target requires 2.
index.ts(3,36): error TS2322: Type 'number' is not assignable to type 'string'.
index.ts(3,40): error TS2322: Type 'string' is not assignable to type 'number'.
index.ts(5,16): error TS2493: Tuple type '[string, number]' of length '2' has no element at index '2'.
Um array comum nunca pegaria o último: para string[], arr[2] é só uma string que por acaso é undefined em tempo de execução.
Elementos nomeados
Os rótulos documentam o que cada posição significa. Eles não mudam nada no tipo nem na forma de indexar, mas os editores os mostram ao passar o mouse e nas dicas de assinatura, o que torna [number, number] bem menos misterioso.
Desde o TypeScript 5.2 você pode rotular algumas posições e deixar outras sem rótulo, como em [first: string, number]. Os rótulos são só para quem lê: [x: number, y: number] e [number, number] são o mesmo tipo e podem ser atribuídos um ao outro.
Elementos opcionais
Um ? depois do tipo de um elemento torna aquela posição opcional. Elementos opcionais precisam vir depois dos obrigatórios, e cada um amplia o tipo de length.
Ler um elemento opcional dá T | undefined, então é preciso um valor padrão na desestruturação (a = 1) ou uma verificação antes de fazer contas.
Elementos rest
Um elemento rest, ...T[], representa qualquer quantidade de elementos do tipo T. Ele pode ficar no fim, no início ou no meio, com no máximo um por tupla.
O length de uma tupla com elemento rest é number, já que o tamanho deixa de ser fixo. O que continua fixo é onde ficam as posições tipadas.
Tuplas readonly e as const
readonly [T, U] remove push, pop, splice e a atribuição por índice, que é o que se espera de um valor de tamanho fixo. Escrever as const depois de um array literal infere uma tupla readonly de tipos literais.
(typeof SIZES)[number] transforma a tupla em uma union dos tipos dos seus elementos, um padrão explicado em indexed access types. Uma tupla readonly não pode ser passada para um parâmetro tipado como tupla mutável, então funções que só leem devem aceitar readonly [number, number].
A verificação de readonly só existe em tempo de compilação. Em tempo de execução o array não está congelado (a atribuição acima rodou de verdade, como mostra a saída), então use Object.freeze se precisar de uma garantia em tempo de execução.
Retornando uma tupla de uma função
Retornar vários valores como uma tupla é como o useState do React funciona (const [value, setValue] = useState(0)). O problema: um array literal em um return é inferido como array, não como tupla.
index.ts(9,13): error TS2365: Operator '+' cannot be applied to types 'number | (() => number)' and 'number'.
index.ts(10,1): error TS2349: This expression is not callable.
Not all constituents of type 'number | (() => number)' are callable.
Type 'number' has no call signatures.
A função retorna (number | (() => number))[], então os dois nomes desestruturados recebem o tipo union. Há duas correções: anotar o tipo de retorno ou adicionar as const.
Um retorno em tupla deixa quem chama dar o nome que quiser a cada parte. Quando há mais de dois ou três valores, ou a ordem não é óbvia, retorne um objeto: { count, increment } se documenta sozinho.
Tuplas como parâmetros de função
Um parâmetro rest tipado como tupla descreve uma lista inteira de argumentos, inclusive os opcionais. É assim que o utility type nativo Parameters<T> representa os parâmetros de uma função.
Espalhar uma tupla em uma chamada verifica o tipo de cada argumento pela posição, algo que um spread de (string | number)[] não conseguiria.
Tupla vs array
Array (string | number)[] | Tupla [string, number] | |
|---|---|---|
| Tamanho | qualquer | fixo (ou limitado por elementos opcionais e rest) |
Tipo de x[0] | string | number | string |
Tipo de x[5] | string | number | erro de compilação TS2493 |
Tipo de length | number | 2 |
| Ordem dos tipos | não é rastreada | é rastreada |
| Valor em tempo de execução | array do JavaScript | o mesmo array do JavaScript |
| Uso típico | listas de itens parecidos | pequenos grupos fixos: pares, coordenadas, [key, value], vários valores de retorno |
Tuplas também aparecem nos tipos nativos. Object.entries(obj) retorna [string, T][], e um Map é construído a partir de tuplas [key, value]:
Uma armadilha: uma tupla mutável ainda tem todos os métodos de array, então pair.push(3) compila em um [string, number] e cria silenciosamente um array de três elementos cujo tipo diz dois. Declarar tuplas como readonly fecha essa brecha. E como os tipos são apagados, dados de fora do programa (JSON, uma API) não são verificados contra um tipo de tupla em tempo de execução: valide o tamanho e os tipos dos elementos antes de confiar neles.
Variadic tuple types
Tipos de tupla podem espalhar outros tipos de tupla, [...T, ...U]. Combinado com generics, isso tipa funções que concatenam ou adicionam elementos no início mantendo cada posição:
Os tipos de bibliotecas também dependem da inferência de tuplas: Promise.all([fetchUser(), fetchPosts()]) resolve para uma tupla com um tipo por promise de entrada.
Perguntas frequentes
O que é uma tupla no TypeScript?
Uma tupla é um tipo de array com tamanho fixo em que cada posição tem seu próprio tipo: [string, number] são exatamente dois elementos, uma string e depois um número. Em tempo de execução é um array comum do JavaScript; o tamanho e os tipos por posição são verificados só em tempo de compilação.
Qual é a diferença entre tupla e array no TypeScript?
Um tipo de array como (string | number)[] tem qualquer tamanho e todo elemento tem o mesmo tipo (union), então arr[0] é string | number. Uma tupla como [string, number] tem tamanho conhecido: t[0] é string, t[1] é number e t[2] é um erro de compilação.
Como retornar uma tupla de uma função no TypeScript?
Anote o tipo de retorno, function f(): [number, string], ou termine a expressão do return com as const, que gera uma tupla readonly. Sem nenhum dos dois, return [count, setCount] é inferido como um array de uma union, como (number | (() => void))[], e a desestruturação dá tipos union.
O que são elementos nomeados de tupla?
Rótulos nas posições, [name: string, age: number]. Eles não mudam o tipo nem a forma de acesso (continua t[0]), mas os editores os mostram ao passar o mouse e nas dicas de parâmetros de funções cujos parâmetros são tipados como tupla. Elementos opcionais e rest funcionam com rótulos: [x: number, y?: number], [head: string, ...rest: number[]].
Dá para fazer push em uma tupla no TypeScript?
Em uma tupla mutável, sim: push compila, porque tuplas herdam os métodos de array, mesmo quebrando o tamanho fixo. Declare a tupla como readonly (ou crie com as const) e push, pop e atribuição por índice passam a ser erros de compilação.