typescript compose 함수 만들기 [번역]

5vermind·2022년 10월 27일
5

함수형

목록 보기
1/2

원문: https://minaluke.medium.com/typescript-compose-function-b7512a7cc012

어떻게 typescript에서 함수형 프로그래밍의 compose 함수의 타입을 체킹할 수 있을까요?

함수형 프로그래밍에서 함수 합성은 매우 일반적입니다. 먼저 네이티브 javascript에서 어떻게 구현되는지 보고 타입을 하나하나 추가하면서 검증해봅시다.

step 1

먼저 getName,getLength, isEven 세 개의 함수가 있다고 생각해봅시다. 이 세개를 javascript로 작성하면 이렇게 될 것입니다.

const getName = (p) => p.name
const getLength = (str) => str.length
const isEven = (num) => num % 2 === 0

사용법은 이렇습니다.

const person = {
  name: 'John'
}

const name = getName(person) // John
const length = getLength(name) // 4
const isEvenLength = isEven(length) // true

step 2

이 함수들을 합성해봅시다.

const myComposedFunction = isEven(getLength(getName(person))) // true

보기에 좋지는 않네요, 괄호가 너무 많아서 가독성이 안좋습니다.

어떻게 하면 이 함수들을 더 예쁘게 합성할 수 있을까요?

먼저 일반적인 방법으로 합성 함수를 작성해봅시다.

const compose =
  (...fns) =>
  (x) =>
    fns.reduceRight((acc, cur) => cur(acc), x);

함수들의 배열을 인자로 받아서 객체를 인자로 받는 함수를 반환하는 고차 함수입니다. 반환된 함수가 호출되면 해당 객체에 대해 오른쪽에서 왼쪽으로 compose 함수의 인자들로 들어간 함수들을 실행하고 그 결과를 반환합니다.

const myComposedFn = compose(isEven, getLength, getName)
const result = myComposedFn(person) // true

step 3

이제 위의 함수 세 개를 타입스크립트로 작성해봅시다.

interface IPerson {
  name: string;
}
const person: IPerson = {
  name: "5vermind",
}
const getName = (p: IPerson) => p.name
const getLength = (str: string) => str.length
const isEven = (num: number) => num % 2 === 0

step 4

더 나은 타입 체크를 위해 compose 함수를 타입스크립트로 작성해 봅시다.

합성 가능한 함수의 인자의 개수가 하나이므로 다음과 같은 함수 타입을 작성할 수 있을겁니다.

type ArityOneFn = (arg: any) => any

이제 우리는 compose 함수의 나머지 인자를 타음과 같이 표현할 수 있습니다.

const compose = (...fns: ArityOneFn[]) => (p: any) =>
  fns.reduceRight((acc: any, cur: any) => cur(acc), p)
const myComposedFn = compose(isEven, getLength, getName)
// const myComposedFN: (p: any) => any

괜찮아 보이시나요?

나쁘지 않죠. 하지만 myComposedFn의 입력과 출력이 모두 any인 것은 불편합니다.

step 5

몇 개의 헬퍼 타입을 작성해 봅시다.

  • myComposedFn의 인자의 타입은 getName의 인자의 타입입니다
  • myComposedFn의 반환 타입은 isEven의 반환 타입입니다.
  • typescript의 tuple을 사용해봅시다.
type PickLastInTuple<T extends any[]> = T extends [
  ...rest: infer U,
  argn: infer L
]
  ? L
  : never;

type FirstFnParameterType<T extends any[]> = 
  Parameters<PickLastInTuple<T>>[any]

type LastFnReturnType<T extends any[]> = ReturnType<T[0]>

이제 우리는 compose 함수에 대해 타입스크립트 버전으로 작성할 수 있습니다.

const compose =
  <T extends ArityOneFn[]>(...fns: T) =>
  (p: FirstFnParameterType<T>): LastFnReturnType<T> =>
    fns.reduceRight((acc: any, cur: any) => cur(acc), p);

끝입니다!


역자주)
type FirstFnParameterType<T extends any[]> = Parameters<PickLastInTuple<T>>[any] 이 코드에 대해서 아래와 같은 ts 오류가 뜨더라고요.

'PickLastInTuple' 형식이 '(...args: any) => any' 제약 조건을 만족하지 않습니다.
'unknown' 형식은 '(...args: any) => any' 형식에 할당할 수 없습니다.ts(2344)

PickLastInTuple가 (...arg: any) => any가 아닌 unknown으로 나와서 Parameter의 제네릭 인자로 들어갈 수 없다는 에러였습니다. 다음 포스트에서는 이 문제를 해결해보겠습니다. (못할수도있고..)

profile
발전하고 진화하는 FE developer

0개의 댓글