readonly T[]와 ReadonlyArray<T>는 같은 읽기 전용 배열 타입이야. push, pop, splice, sort처럼 원본을 바꾸는 메서드는 사용할 수 없고, length 읽기, 인덱스 접근, map, filter, slice 같은 읽기 연산은 그대로 쓸 수 있어.
이 제한은 컴파일 시점에만 있어. 실행 중 배열을 얼리는 것은 아니므로 다른 변경 가능한 별칭을 통해 값이 바뀔 수 있다. 실제 불변성과 참조가 가진 쓰기 권한을 구별해야 해.
함수 입력의 좋은 기본값
함수가 배열을 읽기만 한다면 매개변수를 readonly T[]로 받아. 함수 몸에서 우발적으로 정렬하거나 원소를 추가하는 일을 막고, 호출자에게도 원본을 건드리지 않는다고 알려 줘. 변경 가능한 배열은 읽기 전용 매개변수에 안전하게 넘길 수 있어.
반대 방향은 허용되지 않아. 읽기 전용 배열을 변경 가능한 변수에 넣으면 그 변수로 원본을 바꿀 수 있기 때문이야. 변경이 필요하면 스프레드 문법으로 복사해 새 배열을 만들고, 복사 비용과 소유권 전환을 코드에 드러내.
튜플에도 같은 규칙
readonly [number, string]은 길이와 위치 타입을 유지하면서 각 칸의 재대입을 막아. as const로 만든 배열 리터럴이 자동으로 읽기 전용 튜플이 되는 이유도 이 규칙이야.
읽기 전용 튜플을 받는 제네릭을 설계할 때는 제약도 readonly unknown[]처럼 써야 변경 가능한 튜플과 읽기 전용 튜플을 모두 받을 수 있어. 불필요하게 변경 가능한 타입으로 제약하면 안전한 입력을 거부할 수 있다.
얕은 제한이라는 사실
readonly User[]은 배열 칸을 바꾸지 못하게 하지만 각 User의 속성까지 얼리지는 않아. 원소도 불변이어야 하면 Readonly<User>를 함께 쓰거나 도메인 모델 자체를 불변으로 설계해야 해.
읽기만 하는 함수는 쓰기 권한을 요구하지 마. 최소 권한 원칙이 배열 타입에도 그대로 적용돼.
피파의 고백
원본을 정렬할 생각이 없던 함수에서 sort 한 줄이 들어가 호출자의 화면 순서를 바꾼 적이 있어. 매개변수에 readonly가 있었다면 컴파일러가 바로 막았을 일이야. 작은 권한 제한이 큰 추적 비용을 없애.
Code
readonly modifier — 허용되는 것, 아닌 것·typescript
// 배열의 readonly modifier.
const frozen: readonly number[] = [1, 2, 3];
frozen[0] = 99; // ❌ Index signature in type 'readonly number[]' only permits reading
frozen.push(4); // ❌ Property 'push' does not exist on type 'readonly number[]'
// 읽기 연산은 여전히 허용.
const doubled = frozen.map((n) => n * 2); // ✅
const length = frozen.length; // ✅
// readonly tuple.
type Pair = readonly [number, number];
const p: Pair = [1, 2];
p[0] = 99; // ❌ readonly
// readonly 가 one-way 관계.
const mutable: number[] = [1, 2, 3];
const alsoFrozen: readonly number[] = mutable; // ✅ readonly 로 widening
// const backToMutable: number[] = alsoFrozen; // ❌ 복사 없이 readonly 떨굴 수 없음
const copy: number[] = [...alsoFrozen]; // ✅ 명시적 복사
readonly parameter — 작은 단어, 큰 invariant·typescript
// 함수 parameter — 채택해야 할 default.
function summarize(items: readonly string[]): string {
// items.sort() // ❌ mutate 함 — 잡힘
return items.join(', ');
}
// Sort 필요하면 먼저 복사.
function summarizeSorted(items: readonly string[]): string {
return [...items].sort().join(', ');
}
// Caller 한테 신호: '너의 데이터 안 만질 거.'
const raw = ['c', 'a', 'b'];
summarize(raw); // ✅ — raw 그대로
summarizeSorted(raw); // ✅ — raw 그대로