원문: https://minaluke.medium.com/typescript-compose-function-b7512a7cc012
어떻게 typescript에서 함수형 프로그래밍의 compose 함수의 타입을 체킹할 수 있을까요?
함수형 프로그래밍에서 함수 합성은 매우 일반적입니다. 먼저 네이티브 javascript에서 어떻게 구현되는지 보고 타입을 하나하나 추가하면서 검증해봅시다.
먼저 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
이 함수들을 합성해봅시다.
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
이제 위의 함수 세 개를 타입스크립트로 작성해봅시다.
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
더 나은 타입 체크를 위해 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
인 것은 불편합니다.
몇 개의 헬퍼 타입을 작성해 봅시다.
myComposedFn
의 인자의 타입은 getName의 인자의 타입입니다myComposedFn
의 반환 타입은 isEven의 반환 타입입니다.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의 제네릭 인자로 들어갈 수 없다는 에러였습니다. 다음 포스트에서는 이 문제를 해결해보겠습니다. (못할수도있고..)