이름 기반 타입 흉내내기
예를 들어 몇 가지의 ID 타입이 있는데, 그 각각은 시스템에서 사용하는 서로 다른 종류의 객체를 고유한 방식으로 식별해 준다고 해봅시다.
type CompanyID = string
type OrderID = string
type MemberID = string
type ID = CompanyID | OrderID | MemberID
MemberID 타입의 값이 "i4want4go4home" 같은 단순 해시값이라고 해봅시다.
따라서 비록 MemberID라는 별칭으로 사용했지만 실질적으론 일반 string입니다.
MemberID를 인수로 받는 함수는 다음처럼 정의할 수 있습니다.
type CompanyID = string
type OrderID = string
type MemberID = string
type ID = CompanyID | OrderID | MemberID
function queryForMember(id: MemberID){
// 블라 블라
}
문서화가 잘된 코드로, 다른 팀원들도 어떤 타입의 ID를 전달해야 하는지 명확하게 알 수 있습니다.
하지만 MemberID는 string의 별칭일 뿐이므로 이 정의로는 버그를 확실하게 방지할 수 없습니다.
어떤 개발자가 실수로 잘못된 ID 타입을 전달하면 타입 시스템도 어쩔 방법이 없기 때문입니다.
type CompanyID = string
type OrderID = string
type MemberID = string
type ID = CompanyID | OrderID | MemberID
function queryForMember(id: MemberID){
// 블라 블라
}
let id: CompanyID = '제회사이름은비밀'
queryForMember(id) // OK
이 상황이 바로 이름 기반 타입이 유용한 사례입니다.
TypeScript는 이름 기반 타입을 제공하진 않지만 타입 브랜딩(type branding)이라는 기법으로 이를 흉내 낼 수 있습니다.
이름 기반 타입을 지원하는 다른 언어에 비해 TypeScript에서 타입 브랜딩으로 이를 흉내내기는 조금 까다로우며 몇 가지 설정도 필요합니다.
하지만 브랜디드 타입(branded type)을 이용하면 프로그램을 한층 안전하게 만들 수 있습니다.
응용 프로그램과 개발팀의 규모에 따라 이 기능이 필요 없을 수도 있습니다.
규모가 큰 팀일수록 실수를 줄이는데 도움이 됩니다.
우선 필요한 이름 기반 타입 각각에 대응하는 임의의 타입 브랜드를 만듭니다.
type CompanyID = string & {readonly brand: unique symbol}
type OrderID = string & {readonly brand: unique symbol}
type MemberID = string & {readonly brand: unique symbol}
type ID = CompanyID | OrderID | MemberID
string과 {readonly brand: unique symbol}의 인터섹션은 물론 큰 의미는 없습니다.
예시에서 사용한 이유는 이런 타입을 만드는 자연스러운 방법이 존재하지 않으며 이 타입의 값을 만들려면 반드시 어서션을 이용해야 하기 때문입니다.
이는 브랜디드 타입의 핵심적인 특징으로, 실수로 잘못된 타입을 사용하기가 아주 어렵게 해줍니다.
예제에서 unique symbol을 '브랜드'를 사용했는데 이는 TypeScript에서 실질적으로 제공하는 두 가지 이름 기반 타입 중 하나이기 때문입니다.
그런 다음 이 브랜드를 string과 인터섹션하여 주어진 문자열이 정의한 브랜디드 타입과 같다고 어서션할 수 있도록 했습니다.
이제 CompanyID, OrderID, MemberID 타입의 값을 만드는 방법이 필요합니다.
값을 만드는 데는 컴패니언 객체 패턴을 이용할 것입니다.
이제 각 브랜디드 타입의 생성자를 만들어보겠습니다.
이때 주어진 값(id)을 앞서 정의한 난해한 타입들로 지정하는 데 타입 어서션을(as)을 사용합니다.
type CompanyID = string & {readonly brand: unique symbol}
type OrderID = string & {readonly brand: unique symbol}
type MemberID = string & {readonly brand: unique symbol}
type ID = CompanyID | OrderID | MemberID
function CompanyID(id: string){
return id as CompanyID
}
function OrderID(id: string){
return id as OrderID
}
function MemberID(id: string){
return id as MemberID
}
마지막으로 이들 타입을 아래처럼 사용할 수 있습니다.
type CompanyID = string & { readonly brand: unique symbol };
type OrderID = string & { readonly brand: unique symbol };
type MemberID = string & { readonly brand: unique symbol };
type ID = CompanyID | OrderID | MemberID;
function CompanyID(id: string) {
return id as CompanyID;
}
function OrderID(id: string) {
return id as OrderID;
}
function MemberID(id: string) {
return id as MemberID;
}
function queryForMember(id: MemberID) {
// 블라 블라
}
let companyId = CompanyID("회사이름은비밀");
let orderId = OrderID("큐릉");
let memberId = MemberID("CozyLinda");
queryForMember(memberId);
// 'CompanyID' 형식의 인수는 'MemberID' 형식의 매개 변수에 할당될 수 없습니다.
// 'CompanyID' 형식은 '{ readonly brand: unique symbol; }' 형식에 할당할 수 없습니다.
// 'brand' 속성의 형식이 호환되지 않습니다.
// 'typeof brand' 형식을 'typeof brand' 형식에 할당할 수 없습니다. 이름이 같은 2개의 서로 다른 형식이 있지만 서로 관련은 없습니다.ts(2345)
queryForMember(companyId);
런타임 오버헤드가 거의 없다는 것이 이 기법의 장점입니다.
ID 생성당 한 번의 함수 호출로 충분하며 아마 이 함수 호출조차 JavaScript VM이 인라인으로 삽입했을 것입니다.
런타임에 모든 ID는 단순한 string입니다.
즉, 브랜드는 순전히 컴파일 타임에만 쓰이는 구조물입니다.
대부분의 응용 프로그램에선 이렇게까지 하는 건 과할 수 있지만, 규모가 큰 응용 프로그램 또는 다양한 종류의 ID를 사용해 타입이 헷갈리는 작업 환경이라면 브랜디드 타입으로 안전성을 확보할 수 있습니다.
'👶 TypeScript' 카테고리의 다른 글
에러 처리 - null 반환 (0) | 2023.01.22 |
---|---|
프로토타입 안전하게 확장하기 (0) | 2023.01.22 |
탈출구 - 확실한 할당 어서션 (0) | 2023.01.22 |
탈출구 - Nonnull 어서션 (0) | 2023.01.22 |
탈출구 - 타입 어서션 (0) | 2023.01.21 |