TypeScript는 구조적 타입 시스템(structural typing), 또는 "오리 타입 시스템"(duck typing)을 사용합니다. 타입 호환성에 중요한 것은 구조(시그니처)이며, 타입의 이름이나 출처는 중요하지 않습니다.
예시:
interface Point2D { x: number; y: number; } interface Coord2D { x: number; y: number; } // 이 타입들은 서로 호환 가능하며, Point2D와 Coord2D의 구조가 동일하기 때문입니다. const foo: Point2D = { x: 1, y: 2 }; const bar: Coord2D = foo; // OK!
명명적 타입 시스템(nominal typing): 타입 호환성에 중요한 것은 "이름" 또는 "팩토리"이며, 구조는 중요하지 않습니다.
TypeScript에서는 명명적 타입 시스템이 기본적으로 지원되지 않지만, branded types를 사용하여 이를 에뮬레이트할 수 있습니다:
type USD = number & { readonly __brand: unique symbol } type EUR = number & { readonly __brand: unique symbol } let priceUSD: USD; let priceEUR: EUR; // priceUSD = priceEUR; // 오류! 브랜드가 다릅니다.
그것을 왜 사용하나요? 예를 들어, 구조는 동일하지만 의미적으로 다른 타입(통화, userID/tokenID, 물리적 양 등)을 구분하기 위해서입니다.
질문: 다음 코드가 오류 없이 컴파일되는 이유는 무엇인가요? Address와 UserId는 논리적으로 서로 다른 타입입니다.
interface Address { value: string; } interface UserId { value: string; } let id: UserId = { value: "test" }; let addr: Address = id; // OK
답변: TypeScript에서는 구조가 중요하고, 타입의 이름은 중요하지 않기 때문입니다. 두 타입 모두 "value: string"을 가진 객체일 뿐입니다.
이야기
프로젝트: USD/EUR로 계산하는 금융 시스템. 금액이 number로 전달되었습니다. 한번은 계산 중 통화를 잘못 바꾸어 더했는데, 구조적 타입 시스템 때문에 TypeScript가 이를 발견하지 못했습니다. 나중에 branded types를 도입하여 컴파일 단계에서 이러한 오류를 제외했습니다.
이야기
프로젝트: REST API를 개발하는 과정에서 서로 다른 엔티티(UserId, groupId)의 Id를 나타내기 위해 객체를 사용했습니다. 두 객체 모두 value: string 필드를 가지고 있었고, 실수로 userId가 groupId 대신 사용되었으며 비즈니스 로직이 서버에서만 이 오류를 발견했습니다.
이야기
프로젝트: DSL을 위한 파서 라이브러리에서 동일한 구조(type value = { kind: 'num'|'str', value: number|string })를 사용했습니다. 유사한 구조체들이 코드의 여러 부분에서 섞여 들어가 논리적 오류가 발생했습니다. 구분을 위해 인위적인 brand 필드를 추가했습니다.