typescript로 compose, pipe 구현하기

5vermind·2022년 10월 31일

함수형

목록 보기
2/2

저번 포스트에서 typed compose function에 대한 포스트를 번역했습니다.

이번에는 해당 포스트에서 영감을 얻어서 compose와 pipe를 좀 더 확장해보았습니다.

함수 인자 확장

type ArityFunction = (...arg: any) => any;

단순히 arg...arg로 바꿔서 여러 인자를 받을 수 있게 했습니다.

인자, 리턴 타입

type FirstParameters<T extends ArityFunction[]> = T extends [
  infer FIRST extends (...args: any) => any,
  ...infer REST
]
  ? Parameters<FIRST>
  : never;

type LastParameters<T extends ArityFunction[]> = T extends [
  ...infer REST,
  infer LAST extends (...args: any) => any
]
  ? Parameters<LAST>
  : never;

type FirstReturnType<T extends ArityFunction[]> = T extends [
  infer FIRST extends (...args: any) => any,
  ...infer REST
]
  ? ReturnType<FIRST>
  : never;

type LastReturnType<T extends ArityFunction[]> = T extends [
  ...infer REST,
  infer LAST extends (...args: any) => any
]
  ? ReturnType<LAST>
  : never;

각각 ArityFunction[]에서 맨 앞과 맨 뒤 함수의 인자 타입, 리턴 타입을 알아내는 유틸리티입니다.

compose와 pipe 확장

사실 저번 포스트에서는 pipe은 없었지만 reducereduceRight이냐 차이 정도 입니다. pipe는 인자로 받은 함수 리스트를 왼쪽에서 오른쪽으로 실행하고 compose는 오른쪽에서 왼쪽으로 실행합니다.

export const compose =
  <T extends ArityFunction[]>(
    ...fns: T
  ): ((...args: LastParameters<T>) => FirstReturnType<T>) =>
  (...args: LastParameters<T>): FirstReturnType<T> =>
    fns.reduceRight((acc: any, fn) => fn(acc), args);

export const pipe =
  <T extends ArityFunction[]>(
    ...fns: T
  ): ((...args: FirstParameters<T>) => LastReturnType<T>) =>
  (...args: FirstParameters<T>): LastReturnType<T> =>
    fns.reduce((acc: any, fn) => fn(...acc), args);

위와 같이 정의하면 composepipe의 인자 타입과 리턴 타입을 명확히 할 수 있습니다!

아쉬운 점은 중간에 있는 함수들의 인자타입-리턴타입 쌍의 검증이 안된다는 것입니다. 다른 블로그에서는 함수 오버로드를 통해서 해결했지만 오버로드를 무한히 할 수는 없잖아요? 중간 타입에 대한 타이핑을 또 고민해봐야겠습니다.

또 다른 접근

export const partial = <T extends (...args: any[]) => any>(
  fn: T,
  ...partialArgs: PartialParameters<T>
) => {
  const args = partialArgs;
  return (...fullArguments: Parameters<T>) => {
    let arg = 0;
    for (let i = 0; i < args.length && arg < fullArguments.length; i++) {
      if (args[i] === undefined) {
        args[i] = fullArguments[arg++];
      }
    }
    return fn(...args);
  };
};

위 함수는 여러 인자를 받는 함수를 "미리 몇개의 인자를 채워두고" 사용할 수 있게 해주는 함수입니다. curry와 비슷한 느낌이죠.
const delayTen = partial(setTimeout, undefined, 10)이렇게 함수를 만들고 delayTen(()=>console.log('delay 10!!'))이렇게 사용합니다. 이렇게 여러 인자를 받는 함수를 미리 인자를 채워둔 다음 저번 포스트에 있는 compose와 pipe(reduce 방향을 바꿔서) 사용하면 인자 하나만으로 사용할 수도 있습니다.

compose(partial(getA, undefined, 'a'), partial(getB, undefined, 'b')..)('z')
profile
발전하고 진화하는 FE developer

0개의 댓글