책장/Typescript

잉여 속성 체크 vs 할당 가능 검사

TERAJOO 2024. 2. 4. 13:20

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. 요약

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