Menu

TypeScript 튜플(tuple): 문법, 이름 붙은 요소, 선택 요소, 나머지 요소

TypeScript 튜플은 [string, number]처럼 요소 개수가 고정되고 위치마다 타입이 정해진 배열입니다. 문법, 이름 붙은 요소, 선택 요소, 나머지 요소, readonly 튜플과 as const, 함수에서 튜플 반환하기, 튜플과 배열의 차이를 알아봅니다.

이 페이지에는 실행 가능한 에디터가 있습니다 - 편집하고 실행하면 결과를 바로 볼 수 있습니다.

TypeScript 튜플은 요소 개수가 고정되고 위치마다 고유한 타입을 가지는 배열입니다. [string, number]는 정확히 두 요소, 먼저 문자열 그다음 숫자를 뜻합니다. 값이 나오는 순서대로 대괄호 안에 타입을 씁니다.

런타임에서 튜플은 평범한 JavaScript 배열입니다. 튜플이 더하는 모든 것(고정 길이와 위치별 타입)은 컴파일러가 검사한 뒤 지워집니다.

튜플 문법

튜플 타입받는 값length 타입
[string, number]정확히 문자열 하나, 그다음 숫자 하나2
[x: number, y: number]같은 타입에 가독성을 위한 레이블2
[number, number, number?]숫자 2개 또는 3개2 | 3
[string, ...number[]]문자열 하나, 그다음 임의 개수의 숫자number
[...string[], number]임의 개수의 문자열, 그다음 숫자 하나number
readonly [number, number]수정할 수 없는 쌍2
[]빈 배열만0

각 형태는 아래에서 설명합니다. length의 타입을 눈여겨보세요. 고정된 튜플에서는 리터럴 타입이므로 컴파일러는 pair.length가 정확히 2라는 것을 압니다.

컴파일러가 검사하는 것

튜플 타입은 요소의 개수, 순서, 위치별 타입을 고정합니다. 하나라도 틀리면 컴파일 오류입니다.

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'.

일반 배열로는 마지막 오류를 잡을 수 없습니다. string[]에서 arr[2]는 런타임에 우연히 undefined일 뿐인 string입니다.

이름 붙은 튜플 요소

레이블은 각 위치가 무엇을 뜻하는지 문서화합니다. 타입이나 인덱싱 방식은 전혀 바뀌지 않지만, 에디터가 호버와 시그니처 힌트에 보여 주므로 [number, number]가 훨씬 덜 모호해집니다.

TypeScript 5.2부터는 [first: string, number]처럼 일부 위치에만 레이블을 붙일 수 있습니다. 레이블은 읽는 사람을 위한 것일 뿐입니다. [x: number, y: number]와 [number, number]는 같은 타입이며 서로 대입할 수 있습니다.

선택 요소(Optional Elements)

요소 타입 뒤에 ?를 붙이면 그 위치는 선택 사항이 됩니다. 선택 요소는 필수 요소 뒤에 와야 하며, 하나씩 늘 때마다 length 타입도 넓어집니다.

선택 요소를 읽으면 T | undefined가 되므로, 산술 연산 전에 구조 분해 패턴의 기본값(a = 1)이나 검사가 필요합니다.

나머지 요소(Rest Elements)

나머지 요소 ...T[]는 T 타입 요소 임의 개수를 뜻합니다. 끝, 처음, 중간 어디에든 올 수 있지만 튜플 하나에 최대 하나만 쓸 수 있습니다.

나머지 요소가 있는 튜플의 length는 크기가 더 이상 고정되지 않으므로 number입니다. 고정된 채로 남는 것은 타입이 정해진 위치입니다.

readonly 튜플과 as const

readonly [T, U]는 push, pop, splice, 인덱스 대입을 없앱니다. 고정 길이 값이라면 이렇게 되는 것이 맞습니다. 배열 리터럴 뒤에 as const를 쓰면 리터럴 타입의 readonly 튜플로 추론됩니다.

(typeof SIZES)[number]는 튜플을 요소 타입의 유니언으로 바꾸며, 이 패턴은 인덱스 접근 타입에서 다룹니다. readonly 튜플은 변경 가능한 튜플 타입의 매개변수에 넘길 수 없으므로, 읽기만 하는 함수는 readonly [number, number]를 받도록 하세요.

readonly 검사는 컴파일 시점에만 이루어집니다. 런타임에 배열이 동결되는 것은 아니므로(출력에서 보듯 위의 대입은 실제로 실행됐습니다), 런타임에도 보장이 필요하면 Object.freeze를 쓰세요.

함수에서 튜플 반환하기

여러 값을 튜플로 반환하는 것은 React의 useState가 동작하는 방식입니다(const [value, setValue] = useState(0)). 함정은 return의 배열 리터럴이 튜플이 아니라 배열로 추론된다는 점입니다.

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.

함수는 (number | (() => number))[]를 반환하므로 구조 분해한 두 이름 모두 유니언 타입을 받습니다. 해결 방법은 두 가지입니다. 반환 타입을 표기하거나 as const를 붙입니다.

튜플을 반환하면 호출하는 쪽에서 각 부분의 이름을 마음대로 정할 수 있습니다. 값이 서너 개 이상이거나 순서가 분명하지 않다면 객체를 반환하세요. { count, increment }는 그 자체로 의미가 드러납니다.

함수 매개변수로서의 튜플

튜플 타입의 나머지 매개변수는 선택 인수를 포함한 인수 목록 전체를 기술합니다. 내장 유틸리티 타입 Parameters<T>가 함수의 매개변수를 나타내는 방식이 바로 이것입니다.

튜플을 호출에 스프레드하면 인수 하나하나가 위치별로 타입 검사됩니다. (string | number)[]를 스프레드해서는 불가능한 일입니다.

튜플과 배열 비교

배열 (string | number)[]튜플 [string, number]
길이자유고정(또는 선택 요소와 나머지 요소로 범위 지정)
x[0]의 타입string | numberstring
x[5]의 타입string | number컴파일 오류 TS2493
length의 타입number2
타입의 순서추적하지 않음추적함
런타임 값JavaScript 배열같은 JavaScript 배열
주된 용도비슷한 항목의 목록작은 고정 묶음: 쌍, 좌표, [key, value], 여러 반환값

튜플은 내장 타입에도 등장합니다. Object.entries(obj)는 [string, T][]를 반환하고, Map은 [key, value] 튜플로 생성합니다.

함정이 하나 있습니다. 변경 가능한 튜플에도 모든 배열 메서드가 있으므로 [string, number]에서 pair.push(3)이 컴파일되고, 타입은 두 요소라고 말하는데 실제로는 세 요소 배열이 조용히 만들어집니다. 튜플을 readonly로 선언하면 이 구멍이 막힙니다. 또한 타입은 지워지므로 프로그램 외부에서 온 데이터(JSON, API)는 런타임에 튜플 타입으로 검사되지 않습니다. 믿기 전에 길이와 요소 타입을 검증하세요.

가변 인자 튜플 타입(Variadic Tuple Types)

튜플 타입은 다른 튜플 타입을 스프레드할 수 있습니다: [...T, ...U]. 제네릭과 함께 쓰면 모든 위치를 유지한 채 이어 붙이거나 앞에 덧붙이는 함수에 타입을 줄 수 있습니다.

라이브러리 타입도 튜플 추론에 기댑니다. Promise.all([fetchUser(), fetchPosts()])는 입력 Promise마다 타입이 하나씩 있는 튜플로 resolve됩니다.

자주 묻는 질문

TypeScript에서 튜플이란 무엇인가요?

튜플은 길이가 고정되고 위치마다 고유한 타입을 가지는 배열 타입입니다. [string, number]는 정확히 두 요소, 먼저 문자열 그다음 숫자입니다. 런타임에는 평범한 JavaScript 배열이며, 길이와 위치별 타입은 컴파일 시점에만 검사됩니다.

TypeScript에서 튜플과 배열의 차이는 무엇인가요?

(string | number)[] 같은 배열 타입은 길이가 자유롭고 모든 요소가 같은 (유니언) 타입이므로 arr[0]은 string | number입니다. [string, number] 같은 튜플은 길이가 정해져 있고 t[0]은 string, t[1]은 number이며, t[2]는 컴파일 오류입니다.

TypeScript 함수에서 튜플은 어떻게 반환하나요?

반환 타입을 function f(): [number, string]처럼 표기하거나, 반환식 끝에 as const를 붙여 readonly 튜플로 만듭니다. 둘 다 하지 않으면 return [count, setCount]는 (number | (() => void))[] 같은 유니언 배열로 추론되어 구조 분해한 값도 유니언 타입이 됩니다.

이름 붙은 튜플 요소(named tuple elements)란 무엇인가요?

[name: string, age: number]처럼 위치에 붙인 레이블입니다. 타입이나 접근 방식은 바뀌지 않지만(여전히 t[0]), 에디터가 호버와 튜플 타입 매개변수를 가진 함수의 매개변수 힌트에 레이블을 보여 줍니다. 선택 요소와 나머지 요소도 레이블과 함께 쓸 수 있습니다: [x: number, y?: number], [head: string, ...rest: number[]].

TypeScript 튜플에 push할 수 있나요?

변경 가능한 튜플이라면 가능합니다. 튜플은 배열 메서드를 물려받으므로 고정 길이가 깨지더라도 push가 컴파일됩니다. 튜플을 readonly로 선언하거나 as const로 만들면 push, pop, 인덱스 대입이 컴파일 오류가 됩니다.

Coddy programming languages illustration

Coddy로 코딩 배우기

시작하기