readonly any[] vs any[]
1. 문제
const readonlyArr: readonly number[] = [1, 2, 3, 4];
const nonReadonlyArr: number[] = [1, 2, 3, 4];
const arr1_1: readonly number[] = readonlyArr; // ok
const arr1_2: readonly number[] = nonReadonlyArr; // ok
const arr_1: number[] = readonlyArr; // error
const arr_2: number[] = nonReadonlyArr; // ok
타입스크립트를 사용하는 중에 이해가 가지 않는 코드를 마주치게 되었다. 위의 예시를 보면 알 수 있듯이, readonly 가 붙은 배열타입과 붙지 않은 배열타입간의 호환성 여부가 매우 이해가 가지 않았다.
왜 readonly 가 붙지 않은 배열타입의 변수에 readonly 가 붙은 배열타입을 할당할 수 없는 것일까?
2. 조사
타입스크립트에서 튜플이라는 개념을 제공하고 있다.
튜플은 고정된 요소 수와 각 요소의 타입을 미리 정의한 배열이다.
배열과 유사하지만 각 위치에 저장되는 값의 타입이 고정되어있기에, 배열 타입과 다르게 타입자체의 length 속성을 통해 튜플 타입의 길이를 type annotation 영역에서 확인할 수 있다.
이러한 튜플의 타입은 다음과 같이 정의할 수 있다.
type Tuple = readonly unknown[];
타입스크립트에서 제공해주고 있는 튜플의 정의를 통해 역으로 알 수 있는 점은
readonly 가 배열타입에 추가된다면, 각 요소의 개수와 타입이 고정된다는 것이다. 때문에 불변배열로써 가변배열은 가지고 있지 않은 추가적인 정보들을 가지고 있다고 볼 수 있다.
이러한 이유로 readonly 가 없는 number[] 에 리터럴 배열 형태인 number[] 는 할당할 수 있지만 readonly 리터럴 배열 타입인 readonly number[]는 할당할 수 없다.
좀 이해하기 난해하긴 하다... 다시 살펴보자.
배열에 readonly 가 붙으면 진짜로 다른 타입이 되는 걸까? 아래의 예시를 봐보자.
type answer1 = readonly any[] extends any[] ? true : false; // false
type answer2 = any[] extends readonly any[] ? true : false; // true
예시에서 answer1 은 false 이고 answer2 는 true 가 찍히게 된다. 왜일까?
extends 조건부 타입에서 A extends B 의 의미는 “A는 B에 할당 가능한가??” 를 의미한다.
즉, A는 B타입을 확장하거나 B의 서브타입이라는 걸 알 수 있다.
그러니 의미 그대로 생각해보면 readonly any[] 타입은 any[] 에 할당할 수 없지만, 그 반대인 any[] 타입은 readonly any[] 타입에 할당할 수 있다는 의미라고 볼 수 있다.
const readonlyArr: readonly number[] = [1, 2, 3, 4];
const nonReadonlyArr: number[] = [1, 2, 3, 4];
const arr1_1: readonly number[] = readonlyArr; // ok
const arr1_2: readonly number[] = nonReadonlyArr; // ok
const arr_1: number[] = readonlyArr; // error
const arr_2: number[] = nonReadonlyArr; // ok
즉, 위의 예시처럼 가변배열을 불변배열에 할당할수는 있지만 불변배열을 가변배열에 할당할수는 없다.
난 솔직히 아직도 헷갈린다.. 허니 그냥 직관적으로 외우기로 타협을 했다.
불변배열의 경우 몇개의 요소가 있는지, 각 요소의 타입이 어떤지에 대한 정보를 가지고 있기 때문에 가변배열보다 더 많은 정보를 가지고 있다고 볼 수 있고 그에 가변배열의 서브타입이다~ 정도로 이해를 했다ㅎㅎ
여튼 타입스크립트에서는 튜플과 배열을 모두 다루는 경우 많다보니 위의 호환성을 잘 챙기고 있다면 이후에 도움이 될것이라 생각된다.
3. 추가
여담으로 조금더 헷갈리는 예시를 보자. 다음의 코드는 에러를 내뿜는다.
type ReadonlyArr = readonly Array<unknown> // error
이유는 readonly 에 있다. readonly의 경우 배열에 대한 변경사항을 허용하지 않는다.
즉, Array 타입의 push, update, delete 내부 메소드들은 해당 제한사항에 적합하지 않게 된다.
또한 튜플과 같은 readonly 객체 리터럴타입을 위해서는 내부 요소들의 고정된 개수와 각 요소들의 타입이 필요하다고 했는데, 단순 Array 타입에서는 해당 정보를 빼낼 수 없다. 즉, Array 라는 가변배열의 타입 자체가 readonly(불변배열)와 함께 사용할 수 있는 방식으로 설계되지 않았기 때문에 에러가 발생한다라고 이해할 수 있다.
readonly 객체 리터럴(like 튜플)은 typescript 에서 꽤 자주 사용되니, 이해가 안가면 일단 외우자.
배열 리터럴 타입에 readonly 를 사용해야 한다~ (feat. 가변배열, 불변배열 간의 호환성)