TypeScript 타입 호환성

post-thumbnail

https://toss.tech/article/typescript-type-compatibility

해당 아티클을 보고 정리 & 내용 추가한 글입니다.

TypeScript 타입 시스템 뜯어보기: 타입 호환성

TypeScript를 쓰다 보면 "이게 왜 되지?" 혹은 "왜 이건 안 되지?" 싶은 타입 호환 문제가 한 번쯤은 생깁니다. 이번 글에서는 TypeScript의 타입 호환성 원리부터 구조적 서브타이핑, 신선도(Freshness), Branded 타입까지 실제 예제와 함께 정리해보겠습니다.


1. 타입 호환성이란?

TypeScript에서 두 타입이 서로 대입 가능하거나, 함수 인자에 전달 가능할 때 이를 "타입이 호환된다"고 말합니다.

let a: string = "hello";
let b: string | number = a; // OK

2. 구조적 서브타이핑 (Structural Subtyping)

정의

TypeScript는 객체의 멤버 구조가 같으면 타입 이름이 달라도 호환 가능하다고 판단합니다. 이걸 흔히 덕 타이핑(Duck Typing) 이라고도 부르죠.

type Food = {
  protein: number;
  carbohydrates: number;
  fat: number;
}

function calculateCalorie(food: Food) {
  return food.protein * 4 + food.carbohydrates * 4 + food.fat * 9;
}

type Burger = Food & { burgerBrand: string };

const burger: Burger = {
  protein: 29,
  carbohydrates: 48,
  fat: 13,
  burgerBrand: '버거킹',
};

calculateCalorie(burger); // 문제 없음

특징

  • 명시적 상속이 없어도 구조가 같으면 호환
  • 유연하고 실용적이지만, 의도치 않은 호환도 생길 수 있다

3. 신선도(Freshness)란?

객체 리터럴은 함수 인자에 직접 전달되거나 변수 초기값으로 사용되면 fresh object literal로 간주됩니다. 이 경우 정의되지 않은 속성이 있으면 오류가 발생합니다.

calculateCalorie({
  protein: 29,
  carbohydrates: 48,
  fat: 13,
  burgerBrand: '버거킹',
}); // ❌ 타입 오류 발생

해결 방법

  • 변수에 먼저 할당하기
  • 타입 단언 사용 (as)
  • Index Signature 추가
type Food = {
  protein: number;
  carbohydrates: number;
  fat: number;
  [key: string]: any;
};
  • tsconfig 설정에서 suppressExcessPropertyErrors: true 사용

4. Branded Type으로 타입 강제화하기

개념

브랜드 속성을 추가해 같은 구조라도 다른 타입으로 구분할 수 있습니다.

type Brand<K, T> = K & { __brand: T };

type Food = Brand<{
  protein: number;
  carbohydrates: number;
  fat: number;
}, 'Food'>;

런타임에는 __brand가 존재하지 않으며, 컴파일러 수준에서만 작동하는 트릭입니다.

예제 1: UUID 구분하기

type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function getUser(userId: UserId) { /* ... */ }

const orderId = 'uuid-001' as OrderId;
getUser(orderId); // ❌ 타입 오류 발생

createUserId, createOrderId 같은 생성 함수로 wrapping 하면 더 안전합니다.


예제 2: 이메일 검증 후 사용

type Email = Brand<string, 'Email'>;

function validateEmail(input: string): Email | null {
  const regex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
  return regex.test(input) ? (input as Email) : null;
}
const input = 'user@example.com';
const email = validateEmail(input);

if (email) {
  sendWelcomeEmail(email);
}

function sendWelcomeEmail(email: Email) {
  // 타입이 Email이라 안심하고 사용 가능
}

예제 3: 금액 단위 구분

type KRW = Brand<number, 'KRW'>;
type USD = Brand<number, 'USD'>;

function payWithWon(amount: KRW) {
  console.log('₩', amount);
}

const priceKrw = 1000 as KRW;
payWithWon(priceKrw); // ✅ OK

const priceUsd = 1000 as USD;
payWithWon(priceUsd); // ❌ 타입 오류

0개의 댓글