
https://toss.tech/article/typescript-type-compatibility
해당 아티클을 보고 정리 & 내용 추가한 글입니다.
TypeScript를 쓰다 보면 "이게 왜 되지?" 혹은 "왜 이건 안 되지?" 싶은 타입 호환 문제가 한 번쯤은 생깁니다. 이번 글에서는 TypeScript의 타입 호환성 원리부터 구조적 서브타이핑, 신선도(Freshness), Branded 타입까지 실제 예제와 함께 정리해보겠습니다.
TypeScript에서 두 타입이 서로 대입 가능하거나, 함수 인자에 전달 가능할 때 이를 "타입이 호환된다"고 말합니다.
let a: string = "hello";
let b: string | number = a; // OK
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); // 문제 없음
객체 리터럴은 함수 인자에 직접 전달되거나 변수 초기값으로 사용되면 fresh object literal로 간주됩니다. 이 경우 정의되지 않은 속성이 있으면 오류가 발생합니다.
calculateCalorie({
protein: 29,
carbohydrates: 48,
fat: 13,
burgerBrand: '버거킹',
}); // ❌ 타입 오류 발생
as)type Food = {
protein: number;
carbohydrates: number;
fat: number;
[key: string]: any;
};
suppressExcessPropertyErrors: true 사용브랜드 속성을 추가해 같은 구조라도 다른 타입으로 구분할 수 있습니다.
type Brand<K, T> = K & { __brand: T };
type Food = Brand<{
protein: number;
carbohydrates: number;
fat: number;
}, 'Food'>;
런타임에는 __brand가 존재하지 않으며, 컴파일러 수준에서만 작동하는 트릭입니다.
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 하면 더 안전합니다.
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이라 안심하고 사용 가능
}
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); // ❌ 타입 오류