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; // okconst arr1_2: readonly number[] = nonReadonlyArr; // okconst arr_1: number[] = readonlyArr; // errorconst arr_2: number[] = nonReadonlyArr; // ok타입스크립트를 사용하는 중에 이해가 가지 않는 코드를 마주치게 되었다. 위의 예시를 보면 알 수 있듯이, readonly 가 붙은 배열타입과 붙지 않은 배열타입간..
2024.02.24
숫자기반의 enum vs 문자열기반 enum
Typescript에서 제공하는 Enum의 형태는 2가지가 존재한다.숫자기반의 enum 과 문자열기반의 enum이 2가지 형태의 enum 을 구분할 줄 알아야한다.각 형태에 따라 런타임 환경에서 javascript 가 값을 읽을 수 있도록 transpile 되는 형태가 다르기 때문이다.그러면 어떻게 다른지 살펴보자. 1. Enum 의 두가지 기능TypeScript의 enum은 아래의 2가지 기능을 제공한다.역방향 매핑(Reverse Mapping)숫자 기반 enum은 값에서 이름으로, 이름에서 값으로 양방향 매핑을 지원한다.이를 통해 기존의 key 로 접근하여 value 를 가져올 수 있는 형태에서, 역으로 value 에 접근해 key 를 가져올 수 있는 형태까지 가능해졌다.실행 시점 접근(Runtime..
2024.02.19
잉여 속성 체크 vs 할당 가능 검사
1. 문제이해가 가지 않았던 코드// giveninterface A { a: string;}interface B { a: string; b: number;}// whenconst aTypeVal: A = {a: 'a'}; const bTypeVal: B = {a: 'a', b: 1}; // thenlet aTemp: A;aTemp = bTypeVal; // okaTemp = {a: 'a', b: 1}; // error나는 위 코드를 작성할 때 당연히 aTemp 변수에 bTypeVal 을 할당할 수 있었으니aTemp = {a: 'a', b: 1};위 코드에서 에러가 날 것을 의심하질 못했다.헌데 실제로 작성하면 ts error가 발생한다..대체 뭐가 다르길래 타..
2024.02.04

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. 가변배열, 불변배열 간의 호환성)

 

'책장 > Typescript' 카테고리의 다른 글

숫자기반의 enum vs 문자열기반 enum  (0) 2024.02.19
잉여 속성 체크 vs 할당 가능 검사  (0) 2024.02.04

Typescript에서 제공하는 Enum의 형태는 2가지가 존재한다.
숫자기반의 enum문자열기반의 enum

이 2가지 형태의 enum 을 구분할 줄 알아야한다.
각 형태에 따라 런타임 환경에서 javascript 가 값을 읽을 수 있도록 transpile 되는 형태가 다르기 때문이다.
그러면 어떻게 다른지 살펴보자.

 

1. Enum 의 두가지 기능

TypeScript의 enum은 아래의 2가지 기능을 제공한다.

역방향 매핑(Reverse Mapping)
숫자 기반 enum은 값에서 이름으로, 이름에서 값으로 양방향 매핑을 지원한다.
이를 통해 기존의 key 로 접근하여 value 를 가져올 수 있는 형태에서, 역으로 value 에 접근해 key 를 가져올 수 있는 형태까지 가능해졌다.

실행 시점 접근(Runtime Access)
변환된 코드를 통해 실행 시점에 enum의 값에 접근하고, 이를 통해 동적인 기능을 제공한다.

 

2. 숫자 기반 enum (열거형 enum)

숫자 기반 enum은 각 멤버에 자동으로 숫자 값을 할당한다.
첫 번째 멤버는 0에서 시작하며, 이후 멤버는 이전 멤버의 값에서 1씩 증가한다.
이와 같은 특징으로 보통 숫자열 기반 enum은 연속적인 값이 필요한 경우 많이 사용된다.
요일이나 월이 적절한 예시로 생각된다.

TypeScript 컴파일러는 이 enum을 JavaScript에서 다음과 같은 형태로 변환한다.

enum DirectionNumber {
  UP,
  DOWN,
  RIGHT,
  LEFT,
}

var DirectionNumber;
(function (DirectionNumber) {
    DirectionNumber[DirectionNumber["UP"] = 0] = "UP";
    DirectionNumber[DirectionNumber["DOWN"] = 1] = "DOWN";
    DirectionNumber[DirectionNumber["RIGHT"] = 2] = "RIGHT";
    DirectionNumber[DirectionNumber["LEFT"] = 3] = "LEFT";
})(DirectionNumber || (DirectionNumber = {}));

이 코드는 enum의 각 멤버에 숫자 값을 할당하고, 숫자 값으로부터 멤버의 이름을 역으로 찾을 수 있게 해준다.
예를 들어, DirectionNumber.UP은 0이고, DirectionNumber[0]은 "UP"인 것이다.
이게 역방향 매핑이다.

역방향 매핑이 필요한거지? 라는 의문이 있을 수 있다.

타입스크립트는 역방향 매핑을 통해 enum의 유연성을 높이고,
개발자가 enum 값으로부터 멤버의 이름을 쉽게 얻을 수 있게 하려는 의도를 가지고 있다고 하니 이걸로 의문을 해소하자..

 

3. 문자열 기반 Enum

문자열 기반 enum은 각 멤버에 문자열 값을 명시적으로 할당한다.
TypeScript 컴파일러는 이를 JavaScript에서 아래와 같이 변환한다.

enum DirectionString {
  UP = "up",
  DOWN = "down",
  RIGHT = "right",
  LEFT = "left",
}

var DirectionString;
(function (DirectionString) {
    DirectionString["UP"] = "up";
    DirectionString["DOWN"] = "down";
    DirectionString["RIGHT"] = "right";
    DirectionString["LEFT"] = "left";
})(DirectionString || (DirectionString = {}));

숫자열 기반 enum 과는 다르게 문자열 기반 enum에서는 역방향 매핑 이 없다!
왜냐? 분명 타입스크립트는 enum의 유연성을 높이고, enum 값으로부터 멤버의 이름을 쉽게 얻을 수 있도록 역방향 매핑을 추가한다는 의도가 있다고 하지 않았었나? 싶지만

문자열 값이 고유해야 하고, 숫자와 같이 연속적이거나 자동으로 생성될 수 없기 때문에
문자열 기반 enum에서는 역방향 매핑이 일어나지 않는다고 한다..


즉, 문자열 기반의 enum 은 단방향 매핑만 생성하여
DirectionString["UP"]은 "up"을 반환하지만, "up"에서 "UP"을 찾는 것은 지원되지 않는다.

 

4. 숫자+문자열 기반 enum

enum Direction {
  MIDDLE, // 0으로 할당
  UP = "up",
  DOWN = "down",
  RIGHT = "right",
  LEFT = "left"
}

var Direction;
(function (Direction) {
    Direction[Direction["MIDDLE"] = 0] = "MIDDLE";
    Direction["UP"] = "up";
    Direction["DOWN"] = "down";
    Direction["RIGHT"] = "right";
    Direction["LEFT"] = "left";
})(Direction || (Direction = {}));

위 코드와 같이 결합된 형태로 transpile 된다고 한다.

다만 주의해야할 점은
숫자와 문자열 값을 혼용하는 경우, 문자열 값을 할당받은 이후의 멤버들은 자동으로 숫자 값을 할당받지 못한다는 점이다. 예시를 보면 아래와 같다.

enum Direction {
  MIDDLE, // 0으로 할당
  UP = "up",
  DOWN = "down",
  RIGHT = "right",
  LEFT = "left"
  CENTER,
}

위의 enum 을 선언 예시를 보면, 문자열 값을 가지고 있는 LEFT key 다음에 숫자열 기반의 CENTER 가 불쑥 선언되어있는 것을 볼 수 있다.

타입 체커는 해당 CENTER 를 보고 바로 ERROR를 내뿜는다.
문자열 기반 enum 멤버 다음에는 명시적인 문자열 기반 enum 이 와야하기 때문이다.

 

5. const enum

const enum은 TypeScript에서 제공하는 특별한 enum 타입으로,
컴파일 시에 enum 접근이 그 값으로 직접 대체되어 결과 코드에 enum 구조가 남지 않게 된다.

이는 컴파일된 코드의 크기를 줄이고 성능을 최적화하는 데 도움이 된다.
const enum은 숫자 기반 enum뿐만 아니라 문자열 기반 enum에도 사용할 수 있다.

const enum을 사용할 때, TypeScript 컴파일러는 enum 멤버를 사용하는 위치에 직접 멤버의 값을 삽입한다.
이렇게 하면 런타임에 enum 객체에 접근할 필요가 없어지므로, 성능이 개선된다고 한다.
하지만 이로 인해 역방향 매핑과 같은 enum의 일부 기능은 사용할 수 없게 된다.

그냥 런타임 시점에 하나의 "값" 으로 평가되기 때문이다.

 

헌데 주의점이 하나 있다..

요게 꽤 골치아픈데 enum 멤버에 동적으로 접근하는 코드가 있다면 에러가 터져버린다는 것이다.
바로 예시를 보자..

const enum Directions {
  Up = "UP",
  Down = "DOWN",
  Left = "LEFT",
  Right = "RIGHT"
}

const direction = "Up";
console.log(Directions[direction]); // error!!

const enum은 위에서 말했듯 성능을 위하 컴파일 시에 제거되므로,
Directions[direction]과 같은 동적 접근은 유효하지 JavaScript 코드라고 판단된다.

런타임 시점에 Direction 은 사라져 버리기 때문이다.

즉, enum 자체가 그냥 값으로만 사용되는 경우와
자체 인덱스 접근을 통해 하나의 "값"으로만 사용되는 경우에만
const enum 을 활용해주어야 한다.

실제로 프로젝트 중에 이거를 모르고 그냥 쓰다가 왜 그런지도 모르고 const enum 을 안쓴 경우가 있었다.
솔직히 해당 내용에 대한 이해도가 프로젝트 멤버간에 공유되지 않았다면 코드가 꼬여버릴 확률이 있기 때문에 미리 정보 공유를 하거나, 아예 안쓰는 것도 좋은 선택이라고 생각된다.

그냥 const enum은 enum 이 과도하게 많은 경우 성능향상을 위해 선택가능한 하나의 방법 정도로 알아두자.

'책장 > Typescript' 카테고리의 다른 글

readonly any[] vs any[]  (0) 2024.02.24
잉여 속성 체크 vs 할당 가능 검사  (0) 2024.02.04

1. 문제

이해가 가지 않았던 코드

// given
interface A {
 a: string;
}
interface B {
 a: string;
 b: number;
}

// when
const aTypeVal: A = {a: 'a'}; 
const bTypeVal: B = {a: 'a', b: 1}; 

// then
let aTemp: A;
aTemp = bTypeVal; // ok
aTemp = {a: 'a', b: 1}; // error

나는 위 코드를 작성할 때 당연히 aTemp 변수에 bTypeVal 을 할당할 수 있었으니

aTemp = {a: 'a', b: 1};

위 코드에서 에러가 날 것을 의심하질 못했다.
헌데 실제로 작성하면 ts error가 발생한다..

대체 뭐가 다르길래 타입이 지정된 변수가 아닌, 해당 타입과 동일한 구조의 객체를 직접 할당하는 코드는 에러를 내뿜는 걸까??

일단 컴파일 시점에서 error 가 잡힌다는 것은 타입 체커에 의해 에러가 잡혔다는 뜻일 것이다.
그럼 어떤점이 달라서 에러 유무가 결정되는 것일까??

 

2. 이유

좀 찾아보니 다음과 같은 이유라고 한다.

타입스크립트는 타입이 명시되어 있는 변수에 객체 리터럴을 할당한다면
해당 타입의 속성이 있는지, 그리고 그 외의 속성은 없는지 확인하는 과정을 거친다.

이 때 타입이 명시되어있는 변수에 명명된 속성의 매개변수 외의 속성을 할당하려 한다면 오류가 발생한다고 한다.
이 단계를 잉여 속성 체크 라고 하는데, 해당 단계에 의해 아래 코드에서 에러가 발생했던 것이다.

aTemp: A = {a: 'a', b: 1}; // error

하지만, 나는 잘 이해가 가지 않았다.
구조적 타이핑 관점으로 생각하면 특정 속성이 있는 지만 확인된다면, 호환되는 타입으로 봐야하니 오류가 발생하면 안되는게 아닐까? 라는 생각이들었다.

확인해보니 잘못된 생각은 아니었다.
변수에 값을 할당시 할당되는 값의 구조와 할당받는 변수의 구조의 호환성을 확인하는 단계가 실제로 존재했고,
해당 단계를 "할당가능검사 단계" 라고 불렀다.

즉, 할당가능검사 단계로 인해 아래의 코드는 에러를 내뱉지 않았던 것이다.
왜냐? B 타입은 A 타입에 할당(호환)가능하기 때문이다.

aTemp: A = bVal; // ok

 

3. 정리

2가지의 단계로 약간 어지럽긴하지만..
정리하자면 타입스크립트의 잉여 속성 체크라는 특징 때문에 객체 리터럴을 직접 할당시 에러가 발생했다.

타입스크립트는 타입 시스템의 구조적 본질을 해치지 않기 위해
정확한 타입을 알 수 없는 객체 리터럴의 속성을 허용하지 않도록 ‘잉여 속성 체크’ 를 진행한다.
이를 통해 구조적 타입 시스템에서 발생할 수 있는 중요한 오류를 잡을 수 있도록 강제(가드)하는 것이다.

3.1. 잉여 속성 체크

잉여 속성 체크는 구조적 타이핑 시스템에서 허용되는 속성 이름의 오타 같은 실수를 잡아내고,
선택적 필드를 포함하는 타입에 특히 유용하지만, 적용 범위도 매우 제한적이고 오직 객체 리터럴에서만 수행한다고 한다.

즉, 할당되는 값이 객체 리터럴! 일 때는 값을 할당했을 때 정확히 해당 객체가 어떤 타입인지를 모르니 잉여 속성을 체크 한뒤 에러를 내뱉는 것이다.
따라서 객체 리터럴을 할당하더라도 아래와 같이 타입 단언을 해줌으로써 타입체커에게 어떤타입인지 알려줄 수 있다면, 잉여속성체크 단계에서 에러가 나지 않는다.

aTemp: A = {a: 'a', b: 1} as B; // ok

타입스크립트의 구조적 본질을 벗어나지 않고 정확한 타입을 단언해줌으로써
구조적인 비호환 가능성을 차단해주었기 때문에 에러가 나지 않는 것이다.

 

4. 요약

타입스크립트는 기존 타입간의 호환성을 판단하는 단계인 "할당 가능 검사" 단계를 기본으로 진행하되
객체 리터럴 값을 할당하는 경우에는 "잉여 속성 체크" 라는 별도의 구조 호환 확인 단계를 추가로 거친다.

 

'책장 > Typescript' 카테고리의 다른 글

readonly any[] vs any[]  (0) 2024.02.24
숫자기반의 enum vs 문자열기반 enum  (0) 2024.02.19