TypeScript·

Tuple과 readonly

  • #TypeScript

Tuple과 readonly는 별개의 개념이다

Tuple의 정의는 불변 배열이 아니라, 길이와 Index와 매칭되는 값의 타입이 정해진 타입이다.
즉, readonly 튜플이 있을 수 있고, mutable 튜플이 있을 수 있고, readonly 배열, mutable 배열이 있을 수 있다.

따라서, readonly와 튜플의 여부는 별개이므로 T extends readonly any[]로 튜플과 배열을 구분할 수는 없다.

1
// -----------------------------
2
// 1) mutable tuple (요소 변경 가능)
3
// -----------------------------
4
let mt: [number, string] = [1, "a"];
5
6
mt[0] = 2; // OK (number)
7
mt[1] = "b"; // OK (string)
8
// mt[0] = "x"; // Error: string is not assignable to number
9
10
11
// -----------------------------
12
// 2) readonly tuple (요소 변경 불가)
13
// -----------------------------
14
let rt: readonly [number, string] = [1, "a"];
15
16
// rt[0] = 2; // Error: Cannot assign to '0' because it is a read-only property
17
// rt.push("x") // Error: Property 'push' does not exist on type 'readonly [number, string]'
18
19
20
// -----------------------------
21
// 3) mutable array (요소 변경 가능)
22
// -----------------------------
23
let ma: number[] = [1, 2, 3];
24
25
ma[0] = 10; // OK
26
ma.push(4); // OK
27
28
29
// -----------------------------
30
// 4) readonly array (요소 변경 불가)
31
// -----------------------------
32
let ra: readonly number[] = [1, 2, 3];
33
34
// ra[0] = 10; // Error
35
// ra.push(4); // Error
36
37
38
// -----------------------------
39
// 5) readonly 여부와 tuple 여부는 별개 축이다.
40
// 따라서 `T extends readonly any[]`는
41
// "readonly 배열/튜플"을 모두 포함하며,
42
// tuple vs array 구분이 되지 않는다.
43
// -----------------------------
44
type IsReadonlyArrayLike<T> = T extends readonly any[] ? true : false;
45
46
type A = IsReadonlyArrayLike<number[]>; // true (mutable array도 readonly any[]에 할당 가능)
47
type B = IsReadonlyArrayLike<readonly number[]>; // true (readonly array)
48
type C = IsReadonlyArrayLike<[number, string]>; // true (mutable tuple도 readonly any[]에 할당 가능)
49
type D = IsReadonlyArrayLike<readonly [number, string]>;// true (readonly tuple)
50
51
// 결론: `T extends readonly any[]`로는 tuple/array 구분 불가

타입 수준에서 배열과 튜플의 구분

그렇다면 타입 수준에서 배열과 튜플을 구분하는 방법은 무엇인가? 바로 length 프로퍼티의 타입을 보는 것이다.
튜플의 length는 숫자 리터럴로 1, 2, 3과 같은 정해진 숫자 값이 나오는 반면, 배열은 길이가 정해져있지 않으므로 number가 나온다.

1
type IsTupleArray<T extends readonly any[]> =
2
number extends T['length'] ? "Array" : "Tuple"
3
4
const array = [1, 2, 3];
5
const tuple = [4, 5, 6] as const;
6
7
IsTupleArray<typeof tuple> // "Tuple"
8
IsTupleArray<typeof array> // "Array"

숫자 리터럴은 number에 포함되기 때문에 역으로 number extends T['length']를 해준 것이 보인다.