TypeScript에서 `readonly` 배열의 요소 타입 추출: `[0]` vs `[number]`
readonly 배열에서 [0]은 readonly가 유지되고, [number]는 제거되는 이유를 정리했습니다.
게시일: 2025.04.21
들어가기
GraphQL을 사용할 때, 자동으로 생성되는 타입에서 ReadonlyArray<T>
혹은 readonly T[]
형태로 배열 타입이 정의되는 경우가 많습니다. 이때 배열 요소의 타입을 추출할 때, 접근 방식에 따라 readonly
속성의 유지 여부가 달라진다는 사실을 알게 되었는데요. 이 글에서는 그 차이를 정리해 보았습니다.
T[0]
: 첫 번째 요소의 타입을 정확하게 추출
T[0]
과 같은 방식은 배열에서 특정 인덱스(여기선 0번째)의 타입을 정확하게 가져올 수 있습니다. 특히 튜플처럼 길이와 각 요소의 타입이 명확히 정해져 있는 경우에 유용합니다.
type MyTuple = readonly [string, number]; type First = MyTuple[0]; // 🔹 readonly string
✅
readonly
속성도 함께 유지됩니다.
정확히 "첫 번째 요소"를 지목했기 때문입니다.
T[number]
: 모든 요소 타입을 유니언으로 추출
반면, T[number]
는 배열 내의 모든 요소 타입을 유니언 타입으로 가져옵니다.
type MyTuple = readonly [string, number]; type Element = MyTuple[number]; // 🔹 string | number
⛔
readonly
는 사라집니다.
이유는 이 접근 방식이 특정 요소가 아닌 "모든 요소의 타입이 무엇이 될 수 있는지"를 묻기 때문입니다.
왜 readonly
가 사라질까?
TypeScript에서는 container-level immutability(컨테이너 수준의 불변성) 만 추적합니다.
즉, readonly [string, number]
는 배열 자체가 불변이지만, 각 요소 하나하나의 타입에 readonly
가 붙는 건 아닙니다. 그래서 T[number]
와 같이 "요소 전체의 타입을 묻는" 방식에서는 이 불변 정보가 유지되지 않아요.
또한, T[number]
는 내부적으로 인덱스 시그니처 형태 ({ [index: number]: T }
)로 해석되기 때문에 일반 배열처럼 간주되고, readonly
속성이 제거됩니다.
비유로 이해해보기
-
T[0]
: "이 서랍장 첫 번째 서랍에 든 건 뭐지?"
→ 정확한 서랍, 정확한 타입, 그리고 잠겨 있는지 여부까지(readonly
) 알 수 있습니다. -
T[number]
: "이 서랍장에 든 어떤 물건이든 하나 꺼내면 뭐가 있을 수 있을까?"
→ 모든 서랍을 포괄하는 질문이라, 개별 서랍이 잠겨 있었는지 여부(readonly
)는 고려되지 않습니다.
참고: 인덱스 시그니처란?
type Scores = { [subject: string]: number; }; type MyArray = { [index: number]: string; };
- 객체나 배열에서 인덱스(
string
또는number
)를 기반으로 값을 가져올 수 있게 타입을 정의하는 방식입니다. - 자주 사용하는 유틸리티 타입으로는
Record<K, V>
가 있습니다.
마무리
GraphQL이나 API 기반 프로젝트에서 타입 자동 생성을 활용할 때, readonly
속성의 동작 방식을 정확히 이해하면 더 견고한 타입 추론이 가능해집니다.
- 정확한 요소를 추출할 땐
[0]
처럼 인덱스를 직접 명시하세요. - 배열의 모든 요소 타입을 유니언으로 가져올 땐
[number]
를 사용하되,readonly
가 제거된다는 점에 주의하세요.