Search

타입스크립트와 빌더 패턴

subtitle
이 X끼 타입을 갖고 놀아요
Tags
typescript
디자인패턴
Created
2022/01/08
2 more properties
우리 회사 슬랙에 ‘개발노다지’라는 채널이 있다. 노다지 같은 개발 지식들을 공유하는 채널이다.
여기서 우리 팀이 즉흥 재즈 공연을 하듯이 디자인 패턴에 타입스크립트를 적용하는 쇼를 보여줬다.
‘이런 것도 되겠군요’, ‘제가 거기에 하나 덧붙여봐도 될까요?’ 주거니 받거니.
내 멋있는 팀원분들이 보여준 타입스크립트 즉흥 재즈 쇼를 공유하고자 한다.

빌더 패턴

빌더 패턴으로 만들어진 클래스를 사용할 때, 이미 체이닝된 메서드를 타입스크립트가 제안하는 메서드 리스트에서 빼버리고 싶은 경우에 사용하는 트릭이다. this에 Omit 타입을 assertion해서 체이닝된 메서드를 메서드 리스트에서 빼보자.
스캣의 첫번째 헤드

Omit

build 함수에서 런타임에 유효성 검사를 하지 않고, 타입스크립트를 통해 빌드 패턴 사용을 컴파일 타임에서 강제할 수 있다.
export class Dummy { static create() { return new this; } A<T extends Partial<this>>(this: T) { return this as Omit<T, 'A'>; } B<T extends Partial<this>>(this: T) { return this as Omit<T, 'B'>; } C<T extends Partial<this>>(this: T) { return this as Omit<T, 'C'>; } build() { console.log('Build'); } } const dummy = new Dummy(); dummy.A().B().C().build();
TypeScript
메서드를 사용할 때 this를 리턴하는데 이때 this를 메서드 A를 Omit한 타입으로 assertion하면 다음에 체이닝할 때 A는 타입스크립트가 제안하는 메서드 목록에서 빠진다.
A가 typescript가 제안하는 메서드 목록에서 빠졌다.

Pick

Omit을 응용해... Pick으로 특정 메서드 다음에 올 메서드를 강제하는 방법도 있다.
보컬의 송폼(song form)을 약간 변형한 피아노의 두번째 헤드
export class Dummy { static getBuilder() { const instance = new this; return instance as Pick<Dummy,'A'>; } A() { return this as Pick<this, 'B'>; } B() { return this as Pick<this, 'C'>; } C() { return this as Pick<this, 'build'>; } build() { console.log('Build'); } } const dummy = new Dummy(); dummy.A().B().C() ; Dummy.getBuilder().A().B().C().build();
TypeScript
A 메서드를 사용할 때 this를 리턴하는데 이때 this를 메서드 B만 Pick한 타입으로 assertion하면 다음에 체이닝할 때 B만 사용하도록 강제할 수 있다.
A 메서드로 리턴된 this에서 B 메서드만 사용하도록 강제한다

부도덕한 new 금지

constructor 함수에 접근제어자 private을 붙여서 빌더 패턴에서 감히 부도덕한 new를 쓰지 못하도록 한다.
피아노와 보컬의 트레이드
그냥 기존 builder 패턴에 private constructor() {} 를 추가하면 된다.
export class Dummy { private constructor() {} static getBuilder() { const instance = new this; return instance as Pick<Dummy,'A'>; } A() { return this as Pick<this, 'B'>; } B() { return this as Pick<this, 'C'>; } C() { return this as Pick<this, 'build'>; } build() { console.log('Build'); } } // const dummy = new Dummy(); // dummy.A().B().C() // ; Dummy.getBuilder().A().B().C().build(); // new 금지 new Dummy();
TypeScript
new 쓰면 호치호치

함수 버전

갑자기 나타난 함수로 마무리
type Chainable<T = {}> = { option<K extends keyof T, V extends T[K]>(key: K, value: V): Chainable<Omit<T, K>>; build(): T; }; class A {} class B {} class C {} type Dummy = { a: A; b: B; c: C; } declare function buildDummy(): Chainable<Dummy>; buildDummy() .option('a', new A()) .option('b', new B()) .option('c', new C()) .build();
TypeScript
option 메서드는 Chainable 타입을 반환 타입으로 하는데, 이때 타입 파라미터에 이미 사용된 K를 T에서 Omit한 타입을 넣음으로써 다음 option 메서드에 사용될 key를 제한할 수 있다. (이미 사용된 option 제외)
type Chainable<T = {}> = { option<K extends keyof T, V extends T[K]>(key: K, value: V): Chainable<Omit<T, K>>; build(): T; };
TypeScript
option ‘a’를 이미 사용한 경우 option에서 제외된다.
타입은... 인생이 담긴 거죠 | 출처: 최신유행프로그램