구조적 타입 시스템은 TypeScript의 핵심 특징 중 하나입니다. 그러나 때로는 이 시스템이 초과 프로퍼티 체크와 어떻게 관련되어 있는지 이해하는 것이 어려울 수 있습니다. 본 글에서는 이러한 초과 프로퍼티 체크의 동작 방식과 TypeScript의 설계 의도를 설명하고자 합니다.

구조적 타입 시스템의 정의

TypeScript의 구조적 타입 시스템은 객체의 **"형태"**나 **"구조"**에 기반하여 타입의 호환성을 판단하는 방식입니다. 즉, 타입의 이름이나 선언 방식이 아닌, 객체의 실제 구조와 그 구조 안에 있는 프로퍼티들을 기반으로 타입 검사를 진행합니다. 이런 방식의 타입 체계는 Go(Golang)에서도 확인 할 수 있습니다. (물론, Go와 TypeScript가 완전히 동일하지는 않습니다.)

구조적 타입 시스템의 핵심은 유연성 입니다. 객체가 특정 타입의 요구사항을 충족시키면 해당 타입으로 간주됩니다 즉, 어떤 추가 프로퍼티가 있더라도 문제가 되지 않습니다. 하지만, 이러한 유연성은 때로는 예상치 못한 오류를 초래할 수 있습니다. 이를 방지하기 위해 초과 프로퍼티를 체크하게 됩니다.

초과 프로퍼티 체크

초과 프로퍼티 체크는 객체 리터럴을 특정 타입으로 지정하려 할 때, 해당 타입에 정의되지 않은 프로퍼티가 객체 리터럴에 존재하는 경우 에러를 발생시키는 체크 메커니즘입니다.

자세한 동작을 살펴보겠습니다.

구조적 타입 시스템과 초과 프로퍼티 체크의 동작

1. 변수에 다른 변수 객체를 할당할 때

interface A {
  a: number;
  b: number;
}

const b = {
  a: 1,
  b: 2,
  c: 3,
}

const a: A = b; // no error

const aa: A = {
  a: 1,
  b: 2,
  c: 3, // type error!
};

이미 선언된 변수 b 를 다른 변수에 할당하는 경우, 초과 프로퍼티 c 에 대한 에러는 발생하지 않습니다. 그러나 객체 리터럴을 직접 할당할 때는 에러가 발생합니다.

2. 함수의 반환 값으로 객체를 사용할 때

interface A {
  a: number;
  b: number;
}

type Func = () => A;

const a: Func = () => ({
  a: 1,
  b: 2,
  c: 3, // no error
});

const aa = (): A => ({
  a: 1,
  b: 2,
  c: 3, // type error!
});

함수 반환 타입을 함수 시그니처(type Func = () => **A**) 로 지정하면, 초과 프로퍼티 체크가 발생하지 않습니다. 반면, 직접 반환 타입을 지정할 경우(const aa = (): **A**) 초과 프로퍼티 체크가 동작하며 에러가 발생합니다.

이는 함수가 반환하는 객체가 리터럴이 아니라 계산된 값이기 때문입니다.