Typescript에서 Namespace의 활용 (with BrandedType, FP)

jys9962·2025년 3월 27일
0

BrandedType

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은 인스턴스 메소드나 정적 메소드, 생성자를 가질 수 없습니다.

하지만 네임스페이스를 활용하면 팩토리 메소드를 만들거나 관련 메소드를 가질 수 있습니다.


BrandedType과 네임스페이스

타입스크립트에서 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

Namespace와 함수형 라이브러리 함께 사용하기

네임스페이스로도 인스턴스 메소드는 만들 수 없는 점이 아쉽습니다.

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

그 외 사용하는 선언 병합

  • Enum 과 Namespace namespace는 type 뿐 아니라 enum 과 함께 사용 가능합니다.
    	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);
  • Type 과 Function 단순 팩토리 메소드만 필요한 BrandedType은 네임스페이스가 아닌 일반 함수로 간단하게 표현할 수 있습니다.
    // 선언
    export type UserId = string & { brand: 'UserId' }
    export function UserId(value: string) {
    	return value as UserId
    
    /*------------------*/
    
    // 사용
    const userId: UserId = UserId('123')

0개의 댓글