BrandedType이란 기본형 타입에 brand 속성을 추가한 것입니다.
type UserId = string & { brand: 'UserId' }
UserId는 문자열 타입이지만 brand 속성이 추가되었으므로 문자열보다 더 구체화된 타입입니다.
즉 문자열이 필요로 하는 곳에 들어갈 수 있지만, 일반 문자열이 UserId에 들어갈 순 없습니다.
function inputString(param: string) { /* ... */ }
function inputUserId(param: UserId) { /* ... */ }
// ✅ userId가 string 에 들어가는 건 가능
inputString('userId' as UserId)
// ❌ 일반 문자열이 UserId가 될 순 없음
inputUserId('notUserId')
BrandedType은 개발자의 입력 실수를 막고 VO 타입을 대체하는 등 많은 곳에서 사용됩니다.
export type Hour = number & { brand: 'Hour' }
export type Duration = number & { brand: 'Duration' }
export type UserId = string & { brand: 'UserId' }
export type OrderId = string & { brand: 'OrderId' }
export type Timestamp = number & { brand: 'Timestamp' }
export type OrderPrice = number & { brand: 'OrderPrice' }
declare userId: UserId;
declare orderId: OrderId;
const myFunction =
(userId: UserId, orderId: OrderId) =>
{ ... }
// ❌ 두 파라미터의 순서가 바뀌어 type이 맞지 않기 때문에 컴파일시 타입 오류 발생
myFunction(orderId, userId)
BrandedType 은 기본형 타입이기 때문에 기본형 타입에서 가능한 연산과 메소드도 모두 사용할 수 있습니다.
export type Hour = number & { brand: 'Hour' }
declare const value1: Hour;
declare const value2: Hour;
// number 타입이라 덧셈이 가능하다
console.log(value1 + value2)
BrandedType도 관련 함수를 클래스처럼 한 곳에서 관리하고, dot 표현식으로 사용하고 싶지만
타입스크립트에서 type은 인스턴스 메소드나 정적 메소드, 생성자를 가질 수 없습니다.
하지만 네임스페이스를 활용하면 팩토리 메소드를 만들거나 관련 메소드를 가질 수 있습니다.
타입스크립트에서 namespace는 type과 동일한 이름으로 생성할 수 있습니다.
선언 병합: https://www.typescriptlang.org/ko/docs/handbook/declaration-merging.html
type A = ...
namespace A {
...
}
즉 BrandedType을 Namespace와 함께 사용할 수 있습니다.
// BrandedType 선언
export type Duration = number & { brand: 'Duration' }
// 동일한 이름의 namespace 안에 팩토리 메소드와 관련 메소드 구현
export namespace Duration {
const of =
(value: number) =>
value as Duration;
export const from =
(param: {
hour?: number
minute?: number
second?: number
millisecond?: number
}) =>
of(
((param.millisecond || 0)) +
((param.second || 0) * 1000) +
((param.minute || 0) * 1000 * 60) +
((param.hour || 0) * 1000 * 60 * 60),
);
export const add =
(
value1: Duration,
value2: Duration,
) =>
of(value1 + value2)
}
사용
const fiveMinutes: Duration = Duration.from({ minute: 5 });
console.log(fiveMinutes); // 300000
const complexDuration: Duration = Duration.from({ hour: 1, minute: 30, second: 15 });
console.log(complexDuration); // 5415000
const added = Duration.add(
Duration.from({ minute: 5 }),
Duration.from({ second: 10 })
)
console.log(added); // 310000
네임스페이스로도 인스턴스 메소드는 만들 수 없는 점이 아쉽습니다.
duration1.add(duration2) 와 같이 사용할 순 없지만
함수형 라이브러리의 pipe와 함께 고차함수나 커링을 사용하는 경우 가독성에 더 유리할 수 있습니다.
export namespace Duration {
// ...
// 초 단위로 변환
export const toSecondsWithoutMs =
(self: Duration) =>
Math.floor(self / 1000);
// 고차 함수로 구현한 add
export const add =
(value: Duration) =>
(self: Duration) =>
of(self + value);
// curry 사용해서 구현한 sub
export const sub = curry(
(value: Duration, self: Duration) =>
of(self - value),
);
}
//-------------------------------------
// 사용
const result = pipe(
Duration.from({ hour: 10 }),
Duration.add(
Duration.from({ minute: 100 })
),
Duration.sub(
Duration.from({ hour: 3 })
),
Duration.toSecondsWithoutMs
);
console.log(result); // 31200
export enum MyEnum {
a = 'A',
b = 'B',
c = 'C'
}
export namespace MyEnum {
export const initial: MyEnum = MyEnum.a;
export const next =
(before: MyEnum) =>
({
[MyEnum.a]: MyEnum.b,
[MyEnum.b]: MyEnum.c,
[MyEnum.c]: MyEnum.a,
})[before];
}
const init: MyEnum = MyEnum.initial;
const next: MyEnum = MyEnum.next(init);
// 선언
export type UserId = string & { brand: 'UserId' }
export function UserId(value: string) {
return value as UserId
/*------------------*/
// 사용
const userId: UserId = UserId('123')