[string, number]는 정확히 두 칸인 튜플이야. 첫 칸은 문자열, 둘째 칸은 숫자라고 각 위치의 타입을 따로 추적해. 일반 배열처럼 원소가 몇 개든 들어가는 모양이 아니므로 빠뜨리거나 하나 더 넣으면 오류가 난다.
좌표 [x, y], 키와 값의 쌍, React의 [state, setState]처럼 순서가 널리 알려져 있고 구조 분해해서 곧바로 쓰는 값에 잘 맞아. 두세 칸을 넘어서 역할을 외워야 한다면 객체가 더 낫다.
선택 칸과 나머지 칸
[string, number?]처럼 마지막 칸을 선택 사항으로 둘 수 있고, [string, ...number[]]처럼 첫 칸 뒤에 같은 타입이 몇 개든 이어지게 할 수 있어. 함수 매개변수 목록과 닮은 규칙이 적용돼.
이름 있는 튜플 원소 [start: number, end: number]는 실행값을 바꾸지 않지만 편집기의 도움말에 역할을 보여 줘. 위치를 계약으로 선택했다면 최소한 이름으로 의미를 남겨 주는 게 좋아.
튜플과 객체의 선택
구조 분해한 변수 이름만으로 뜻이 곧바로 보이고 값의 수가 작다면 튜플이 간결해. 필드가 늘어날 가능성이 있거나 일부 값만 골라 쓰거나, 호출자마다 순서를 기억해야 한다면 객체가 이겨. 튜플 끝에 새 칸을 더하면 모든 구조 분해 자리가 영향을 받지만 객체는 이름으로 접근하므로 확장에 더 여유가 있어.
추론은 기본적으로 배열을 고른다
[1, 'two']만 적으면 TypeScript는 보통 숫자와 문자열이 섞인 배열로 추론해. 튜플 의도를 알려면 const pair: [number, string]처럼 표기하거나 as const로 고정해야 해. 후자는 읽기 전용 리터럴 튜플이 된다는 차이가 있어.
위치가 계약이면 튜플, 이름이 계약이면 객체. 짧다는 이유만으로 객체를 튜플로 압축하면 절약한 글자보다 독자가 외워야 할 순서가 더 비싸져.
피파의 고백
세 칸짜리 튜플까지는 익숙한 문맥이면 괜찮아. 네 칸째부터는 구조 분해한 변수 이름을 매번 확인하게 되더라. 그 순간 이미 객체가 답이야. 타입은 기계만 읽는 압축 포맷이 아니니까.
Code
Modifier 다 가진 tuple·typescript
// Tuple — 고정 길이, 위치별 타입.
type Coord = [number, number];
const origin: Coord = [0, 0];
const point: Coord = [3, 4];
// const bad: Coord = [3]; // ❌ Element 빠짐
// const bad2: Coord = [3, 4, 5]; // ❌ Element 너무 많음
// Optional 과 rest element.
type OptionalSecond = [string, number?];
const x1: OptionalSecond = ['hi'];
const x2: OptionalSecond = ['hi', 42];
type Spread = [string, ...number[]];
const y: Spread = ['header', 1, 2, 3, 4];
// Named tuple element — 가독성용 label.
function getRange(): [start: number, end: number] {
return [0, 100];
}
const [s, e] = getRange(); // s 와 e 명확성 위해 이름 붙음
Tuple 을 contract 로 — useState 와 Go-style 에러 반환·typescript
// useState 패턴 — 위치 contract.
type StateHook<T> = [T, (next: T) => void];
function useCounter(initial: number): StateHook<number> {
let value = initial;
const set = (next: number) => { value = next };
return [value, set];
}
const [count, setCount] = useCounter(0);
setCount(count + 1);
// Return-with-status 패턴.
function parse(input: string): [error: string | null, result: number | null] {
const n = Number(input);
if (Number.isNaN(n)) return ['not a number', null];
return [null, n];
}
const [err, result] = parse('42');
if (err === null) console.log(result);