구조적 서브타이핑과 타입 호환성

TypeScript
구조적 서브타이핑과 타입 호환성

안녕하세요! 오늘은 타입스크립트에서 구조적 서브타이핑이란 무엇이며, 이에 따른 타입 호환성에 대해서 알아보도록 하겠습니다.

본 글은 아래 링크의 글을 읽고나서 참고하여 정리한 글입니다. https://toss.tech/article/typescript-type-compatibility

1. 구조적 서브타이핑이란? 📌

타입스크립트에서 객체 타입의 상속관계가 명시되어있지 않아도 객체의 프로퍼티를 기반으로 동일하다면 사용처에서 타입호환이 가능한 타입스크립트의 특징이다.


구조적 서브타이핑과 반대되는 개념은 명목적 서브타이핑이다. (객체 타입의 상속관계를 명시)

type Food = {
  name: string;
  calrories: number:
  price: number;
}

const printFoodName = (food: Food): void => {
  console.log(food.name);
}

// 명목적 서브타이핑
const food1: Food = {
  name: 'pizza',
  calrories: 100,
  price: 10000,
};

printFoodName(food1);

// 구조적 서브타이핑
const food2 = {
  name: 'pizza',
  calrories: 100,
  price: 10000,
};

printFoodName(food2);

// 두개의 함수 호출과정 모두 성공.

만약, 객체가 다음의 형식으로 이루어져있다고 가정해보자.

type Food = {
  name: string;
  calrories: number:
  price: number;
}

const printFoodName = (food: Food) => {
  console.log(food.name);
}

const food2 = {
  name: 'pizza',
  calrories: 100,
  price: 10000,
  useFork: true,
};

printFoodName(food2);

구조적 서브타이핑으로 이루어진 food2 객체에 useFork라는 boolean 변수가 추가되었는데, 이를 인자로 넘기면 어떻게될까?


정답은 아무일도 생기지 않는다. 상속관계가 명시되지 않아도, 객체의 프로퍼티 체크 과정에서 통과되었기 때문이다.

구조적 서브타이핑은 만약 어떤 새가 오리처럼 걷고, 헤엄치고, 꽥꽥거리는 소리를 낸다면 나는 그 새를 오리라고 부를 것이다. 라는 의미에서 덕 타이핑(duck typing) 이라고도 한다.

2. 타입호환 예외 조건: 신선도(Freshness) 🦄

위의 코드 예제와 정말 비슷한 코드가 아래에 있다.

type Food = {
  name: string;
  calrories: number:
  price: number;
}

const printFoodName = (food: Food) => {
  console.log(food.name);
}

// TypeError 발생
printFoodName({
  name: 'pizza',
  calrories: 100,
  price: 10000,
  useFork: true,
});

그러나 위의 코드는 타입호환 검사 과정에서 오류가 발생하게된다. 기존의 코드와 다른점은 변수에 객체 리터럴에 할당하였느냐, 하지 않았느냐로 볼 수 있다. 정말 미묘한 차이인거 같은데, 왜 문제가 되는걸까?


TypeScript는 구조적 서브타이핑에 기반한 타입 호환의 예외 조건과 관련하여 신선도(Freshness)라는 개념을 제공한다.


모든 object literal은 초기에 fresh 하다고 간주되며, 타입 단언(type assertion)을 하거나, 타입 추론에 의해 object literal의 타입이 확장되면 freshness가 사라지게 된다.


특정한 변수에 object literal을 할당하는 경우 이 2가지 중 한가지가 발생하게 되므로, freshness가 사라지게 되며, 함수에 인자로 object literal을 바로 전달하는 경우에는 fresh한 상태로 전달된다.

type Food = {
  name: string;
  calrories: number:
  price: number;
}

const printFoodName = (food: Food) => {
  console.log(food.name);
}

/*
  부작용 1  * 코드를 읽는 다른 개발자가 printFoodName 함수가 * burgerBrand를 사용한다고 오해할 수 있음
*/
printFoodName({
  name: '햄버거',
  calrories: 200,
  price: 5600,
  burgerBrand: '버거킹',
});

/*
  부작용 2  * birgerBrand 라는 오타가 발생하더라도  * excess property이기 때문에 호환에 의해 오류가  * 발견되지 않음
*/
printFoodName({
  name: '햄버거',
  calrories: 200,
  price: 5600,
  birgerBrand: '버거킹',
});

fresh object를 함수에 인자로 전달한 경우, 이는 특정한 변수에 할당되지 않았으므로 해당 함수에서만 사용되고 다른 곳에서 사용되지 않는다는 특징이 존재한다.

따라서 이 경우 유연함에 대한 이점보다는 부작용을 발생시킬 가능성이 높으므로 굳이 구조적 서브타이핑을 지원해야할 이유가 없다.

3. 마치며 😃

오늘은 타입스크립트의 구조적/명목적 서브타이핑이란 무엇인지, 이에 따른 타입 호환성 시스템이 어떻게 동작하는지 알아보았는데요. 평소에 개발하면서 위와 비슷한 객체 문제들을 겪었던 경험이 있었는데, 다음부터 이를 참고한다면 객체를 다루는데 어려움이 줄어들 수 있을거 같습니다! 👍


이상으로 마치겠습니다. 긴 글 읽어주셔서 감사합니다!

tsconfig.json에서 사용되는 옵션 정리
이전글

tsconfig.json에서 사용되는 옵션 정리

타입스크립트 프로젝트에서 사용되는 tsconfig.json 파일의 옵션들에 대해서 정리해봅시다.

온라인 마케팅의 핵심, UTM이 무엇일까?
다음글

온라인 마케팅의 핵심, UTM이 무엇일까?

온라인 마케팅의 효과를 분석, 추적할 수 있도록 도와주는 UTM 파라미터에 대해서 알아봅시다.