TypeScript

TypeScript 잉여 속성 체크의 한계 인지하기

2pandi 2024. 4. 12. 17:53

타입이 명시된 변수에 객체 리터럴을 할당할 때 타입스크립트는 아래의 두 항목을 확인한다.

 

1. 해당 타입의 속성이 있는지

2. 그 외의 속성은 없는지 -> 잉여속성체크

 

interface Room {
  numDoors: number;
  ceilingHeightFt: number;
}

const r: Room = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
//~~~~~~~~~~~~~~~~~~~~ Object literal may only specify known properties, 
//                  and 'elephant' does not exist in type 'Room'.
};

 

구조적 타이핑의 관점으로 보았을 때 위의 코드는 오류가 발생하지 않아야 하지만 타입스크립트를 에러를 표시한다.

 

임시변수를 도입해보면 해당 객체를 Room 타입에 할당이 가능하다

const obj = {
  numDoors: 1,
  ceilingHeightFt: 10,
  elephant: 'present',
};
const r: Room = obj;  // 정상

 

위의 코드에서 obj의 타입은 아래와 같이 추론된다.

{ numDoors: number; ceilingHeightFt: number; elephant: string }

obj 타입은 Room 타입의 부분 집합을 포함하므로, Room에 할당 가능하며 타입 체커도 통과한다.

 

객체 리터럴을 바로 할당했을 때(첫 번째 예제)는 '잉여 속성 체크'라는 과정이 수행되어 에러가 표시되었다.

잉여 속성 체크는 구조적 타입 시스템에서 발생할 수 있는 중요한 종류의 오류를 잡을 수 있도록 도와주지만

조건에 따라 동작하지 않는다는 한계가 있고, 통상적인 할당 가능 검사와 함께 쓰이면 구조적 타이핑이 무엇인지 혼란스러워질 수 있다.

 

구조적타이핑은 타입스크립트가 자바스크립트의 런타임 동작(덕 타이핑)을 모델링하기 위한 개념이며,

어떤 인터페이스에 할당 가능한 값이라면 타입 선언에 명시적으로 나열된 속성들을 가지고 있고, 이 타입은 봉인되어있지 않다.

즉, 타입에 선언된 속성 외에 임의의 속성을 추가하더라도 오류가 발생하지 않는다는 것이다.

2024.03.20 - [TypeScript] - TypeScript 구조적 타이핑에 익숙해지기

 

잉여 속성 체크가 할당 가능 검사와 별도의 과정이라는 것을 알아야 타입스크립트 타입 시스템에 대한 개념을 정확히 잡을 수 있다.

타입스크립트는 단순히 런타임에 예외를 던지는 코드에 오류를 표시하는 것뿐 아니라, 의도와 다르게 작성된 코드까지 찾으려 한다.

interface Options {
  title: string;
  darkMode?: boolean;
}

function createWindow(options: Options){
  if (options.darkMode) {
    setDarkMode();
  }
  // ...
}

createWindow({
  title: 'Spider Solitaire',
  darkmode: true,
//~~~~~~~~~~~~~~ Object literal may only specify known properties, 
//             but 'darkmode' does not exist in type 'Options'.
//             Did you mean to write 'darkMode'?
});

 

 

위의 코드를 실행하면 런타임에 어떤 오류도 발생하지 않지만, 오류 메시지처럼 의도한 대로 동작하지 않을 수 있다.

의도대로 실행하려고 한다면 darkmode가 아니라 darkMode로 작성해야 한다.

 

순수한 구조적 타입 체커는 이러한 종류의 오류를 잡아내지 못한다.

darkMode 속성에 boolean 타입이 아닌 다른 타입의 값이 들어간 경우를 제외하면,

string 타입의 title 속성을 가진 또 다른 어떤 속성을 가진 모든 객체는 Options 타입의 범위에 속한다.

 

잉여 속성 체크를 이용하면 기본적으로 타입 시스템의 구조적 본질을 해치지 않으면서도

객체 리터럴에 알 수 없는 속성을 허용하지 않음으로써, 위의 Room이나 Options 예제 같은 문제점을 방지할 수 있다.

 

const o: Options = { darkmode: true, title: 'Ski Free' };
//                   ~~~~~~~~ Object literal may only specify known properties,
//                           but 'darkmode' does not exist in type 'Options'.
//                           Did you mean to write 'darkMode'?

 

 

const intermediate = { darkmode: true, title: 'Ski Free' };
const o: Options = intermediate; // 정상

 

동일한 코드처럼 보이지만 o에 바로 객체 리터럴을 할당한 경우 잉여 속성 체크가 적용되고,

객체 리터럴이 아닌 어떤 변수를 할당한 경우 잉여 속성 체크가 적용되지 않는다.

 

const o = { darkmode: true, title: 'Ski Free' } as Options; // 정상

 

잉여 속성 체크는 위처럼 타입 단언문을 사용했을 때도 적용되지 않는다.

이것이 단언문보다 선언문을 사용해야 하는 단적인 이유 중 하나이다.

 

interface Options {
  darkMode?: boolean;
  [otherOptions: string]: unknown; // -> 인덱스 시그니처
}
const o: Options = { darkmode: true }; // 정상

 

잉여 속성 체크를 원하지 않는다면,

타입 단언문 보다는 위의 예시처럼 인덱스 시그니처를 사용하여 타입스크립트가 추가적인 속성을 예상할 수 있도록 해주면 된다.

 

선택적 속성만으로 이루어진 약한 타입(weak type)에도 '공통 속성 체크'라는 비슷한 체크가 동작한다.

interface LineChartOptions {
  logscale?: boolean;
  invertedYAxis?: boolean;
  areaChart?: boolean;
}
const opts = { logScale: true };
const o: LineChartOptions = opts;
//    ~ Type '{ logScale: boolean; }' has no properties
//      in common with type 'LineChartOptions'.

 

 

구조적 관점에서 LineChartOptions 타입은 모든 속성이 선택적 속성이므로 모든 객체를 포함할 수 있다.

타입스크립트는 이러한 약한 타입에 대해서 값 타입과 선언 타입에 공통된 속성이 있는지 확인하는 별도의 체크를 수행한다.

임시변수를 제거하고 객체 리터럴로 할당하더라도 공통 속성 체크는 여전히 동작한다.