Arrays e Tuplas Tipados

1. Fundamentos de Arrays Tipados

Em TypeScript, arrays tipados são a base para trabalhar com coleções de dados de forma segura. Existem duas sintaxes equivalentes para declarar arrays tipados:

// Sintaxe com colchetes (mais comum)
let numeros: number[] = [1, 2, 3, 4, 5];

// Sintaxe genérica
let nomes: Array<string> = ["Alice", "Bob", "Charlie"];

A inferência de tipos funciona de maneira inteligente com arrays literais:

// TypeScript infere como number[]
const valores = [10, 20, 30];

// Infere como (string | number)[]
const misto = ["texto", 42, "mais texto", 100];

Arrays vazios merecem atenção especial. Inicialmente, o TypeScript infere o tipo never[], que não permite adicionar elementos:

let arrayVazio = []; // tipo: never[]
// arrayVazio.push(1); // Erro! Argumento do tipo 'number' não é atribuível ao parâmetro do tipo 'never'

Para corrigir isso, devemos anotar o tipo explicitamente:

let arrayVazio: number[] = [];
arrayVazio.push(1); // Agora funciona

2. Operações e Métodos com Arrays Tipados

Os métodos nativos de array preservam a segurança de tipos:

const numeros: number[] = [1, 2, 3, 4, 5];

// push aceita apenas números
numeros.push(6);
// numeros.push("texto"); // Erro!

// map preserva o tipo do retorno
const dobrados: number[] = numeros.map(n => n * 2);

// filter também mantém a segurança
const pares: number[] = numeros.filter(n => n % 2 === 0);

Arrays somente leitura oferecem proteção contra mutações:

// Duas formas equivalentes
const frutas1: readonly string[] = ["maçã", "banana"];
const frutas2: ReadonlyArray<string> = ["maçã", "banana"];

// frutas1.push("laranja"); // Erro! Propriedade 'push' não existe em 'readonly string[]'
// frutas1[0] = "pêra"; // Erro! Índice somente leitura

// Métodos que não mutam ainda funcionam
const frutasMaiusculas = frutas1.map(f => f.toUpperCase());

É importante distinguir métodos que mutam o array original dos que retornam novos arrays:

const original: number[] = [1, 2, 3];

// Mutam o original
original.push(4); // original agora é [1, 2, 3, 4]
original.pop();   // original agora é [1, 2, 3]
original.sort();  // ordena in-place

// Retornam novo array (não mutam)
const copia = original.concat([4, 5]); // [1, 2, 3, 4, 5]
const mapeado = original.map(n => n * 2); // [2, 4, 6]
const filtrado = original.filter(n => n > 1); // [2, 3]

3. Tuplas: Estruturas de Tamanho Fixo

Tuplas representam arrays com número fixo de elementos, onde cada posição pode ter um tipo diferente:

// Tupla básica: [string, number]
let pessoa: [string, number] = ["Alice", 30];

// Acesso com segurança de tipos
const nome: string = pessoa[0]; // "Alice"
const idade: number = pessoa[1]; // 30

// pessoa[2] = "extra"; // Erro! Tipo 'string' não pode ser atribuído a 'undefined'

A inferência de tipos em tuplas literais requer atenção:

// TypeScript infere como (string | number)[]
const tuplaInferida = ["texto", 42];

// Para forçar tupla, use 'as const'
const tuplaExplicita = ["texto", 42] as const;
// Tipo: readonly ["texto", 42]

4. Tuplas com Elementos Opcionais e Rest

Elementos opcionais em tuplas são úteis para representar dados que podem ou não estar presentes:

// Elemento opcional no final
type Config = [string, number?];

const config1: Config = ["produção"];
const config2: Config = ["desenvolvimento", 3000];
// const config3: Config = ["teste", 3000, "extra"]; // Erro!

Elementos rest permitem tuplas com número variável de elementos:

// Tupla com um ou mais números
type PeloMenosUmNumero = [number, ...number[]];
const lista1: PeloMenosUmNumero = [1];
const lista2: PeloMenosUmNumero = [1, 2, 3, 4, 5];

// Caso de uso: função com parâmetros variádicos
function somar(...numeros: [number, ...number[]]): number {
    return numeros.reduce((acc, curr) => acc + curr, 0);
}

console.log(somar(1));        // 1
console.log(somar(1, 2, 3));  // 6

5. Manipulação e Desestruturação de Tuplas

A desestruturação preserva os tipos individuais dos elementos:

type Coordenada = [number, number, string];

const ponto: Coordenada = [10, 20, "Ponto A"];

// Desestruturação com tipos preservados
const [x, y, nome] = ponto;
// x: number, y: number, nome: string

console.log(`Coordenadas de ${nome}: (${x}, ${y})`);

Labeled tuples tornam o código mais legível:

type Usuario = [nome: string, idade: number, ativo: boolean];

function criarUsuario(...args: Usuario): void {
    const [nome, idade, ativo] = args;
    console.log(`${nome} tem ${idade} anos e está ${ativo ? "ativo" : "inativo"}`);
}

criarUsuario("Alice", 30, true);

Spread de tuplas em parâmetros de função:

function logInfo(nome: string, idade: number, cidade: string): void {
    console.log(`${nome}, ${idade} anos, mora em ${cidade}`);
}

const dados: [string, number, string] = ["Bob", 25, "São Paulo"];
logInfo(...dados); // Spread da tupla

6. Tipos Avançados com Arrays e Tuplas

Union types em arrays permitem múltiplos tipos:

const misturado: (string | number | boolean)[] = [true, "texto", 42, false];

// Útil para dados heterogêneos
type Flexivel = (string | number)[];
const dados: Flexivel = ["código", 123, "nome", 456];

Arrays de objetos tipados são essenciais em aplicações reais:

interface Usuario {
    id: number;
    nome: string;
    email: string;
}

const usuarios: Usuario[] = [
    { id: 1, nome: "Alice", email: "alice@email.com" },
    { id: 2, nome: "Bob", email: "bob@email.com" }
];

// Operações seguras com objetos
const emails: string[] = usuarios.map(u => u.email);
const usuarioPorId = usuarios.find(u => u.id === 1);

Mapped types aplicados a arrays:

type Opcional<T> = { [K in keyof T]?: T[K] };

type UsuarioOpcional = Opcional<Usuario>;
// { id?: number; nome?: string; email?: string; }

// Aplicado a arrays
type ArrayOpcional<T> = Opcional<T>[];
const usuariosParciais: ArrayOpcional<Usuario> = [
    { nome: "Alice" },
    { id: 2, email: "bob@email.com" }
];

7. Boas Práticas e Armadilhas Comuns

Evite any[] a todo custo, pois ele desativa a verificação de tipos:

// Ruim
const dadosRuins: any[] = [1, "texto", { chave: "valor" }];

// Bom
type DadosBons = (number | string | { chave: string })[];
const dadosBons: DadosBons = [1, "texto", { chave: "valor" }];

Entenda a diferença fundamental entre tuplas e arrays com union types:

// Tupla: posições específicas com tipos específicos
type TuplaExemplo = [string, number];
const tupla: TuplaExemplo = ["Alice", 30]; // Correto
// const tuplaErrada: TuplaExemplo = [30, "Alice"]; // Erro!

// Array com union: qualquer posição aceita qualquer tipo
type ArrayExemplo = (string | number)[];
const array: ArrayExemplo = ["Alice", 30]; // Correto
const array2: ArrayExemplo = [30, "Alice"]; // Também correto!

Quando usar array vs tupla:

// Use array quando:
// - Número de elementos é variável
// - Todos os elementos têm o mesmo tipo
const listaDeCompras: string[] = ["arroz", "feijão", "carne"];

// Use tupla quando:
// - Número de elementos é fixo e conhecido
// - Cada posição tem um significado específico
type CoordenadasGPS = [latitude: number, longitude: number, altitude: number];
const localizacao: CoordenadasGPS = [-23.5505, -46.6333, 760];

Referências