🚀

カリー化とTypeScript

2022/03/15に公開

下記の例は渡された関数をカリー化する型。
currying に関数を渡すとカリー化された型が返ってくる。

type Curry<P, R> = P extends [infer H, ...infer T] ? (p: H) => Curry<T, R> : R;

declare function currying<F>(fn: F): F extends (...P: infer A) => infer R ? Curry<A, R> : never;

const add = (a: number, b: number, c: number) => a + b + c
const curriedAdd = currying(add) // const curriedAdd: (p: number) => (p: number) => (p: number) => number

declare を使った型プログラミングでのカリー化の型生成はできたが、tsでの実際の実装こみで、同様の型がつくようにするにはどうすればいいだろと思った。

上記では三つの数値の加算をカリー化するような型を作ってたので、愚直に書けばこういう感じの実装になるが、これだと、カリー化できる引数の数が3つのみなので汎用的ではない。

function currying<T extends (...args: number[]) => number>(func: T) {
  return (a: number) => {
    return (b: number) => {
      return (c: number) => {
        return func(a, b, c)
      }
    }
  }
}
const add = (a: number, b: number, c: number) => a + b + c
const curriedAdd = currying(add) // const curriedAdd: (a: number) => (b: number) => (c: number) => number

調べたら、それっぽいのがあった。
参考:https://stackoverflow.com/questions/51859461/generic-curry-function-with-typescript-3

type CurryFirst<T> = T extends (x: infer U, ...rest: any) => any ? U : never;
type CurryRest<T> =
    T extends (x: infer U) => infer V ? U :
    T extends (x: infer U, ...rest: infer V) => infer W ? Curried<(...args: V) => W> :
    never
type Curried<T extends (...args: any) => any> = (x: CurryFirst<T>) => CurryRest<T>

const currying = <T extends (...args: any) => any>(fn: T): Curried<T> => {
    if (!fn.length) { return fn(); }
    return (arg: CurryFirst<T>): CurryRest<T> => {
        return currying(fn.bind(null, arg) as any) as any;
    };
}
const add = (a: number, b: number, c: number) => a + b + c
const curriedAdd = currying(add) // const curriedAdd: Curried<(a: number, b: number, c: number) => number>

実装のロジック

fn.length が 0 になるまで(currying に渡された関数の引数がなくなるまで) bind を使って、関数の生成をし続けて、 fn.length が 0 になったら積み上げてきた関数の処理を実行するイメージ。

型定義のロジック

  • CurryFirstcurrying に渡された関数の最初の引数の型を返す
  • CurryRest は引数で渡した add 関数から引数が一つずつ減っていく様を表現しているイメージ。
    • 引数が複数あるなら((x: infer U, ...rest: infer V) => infer W)、再帰的に処理を続ける(Curried<(...args: V) => W>
    • 引数が一つにまで減ったら (x: infer U) => infer V ? U で、シンプルな関数の型を返す

Function.length で関数に渡している引数の数が取れる、ってことを知らないとこの再帰構造はなかなか思いつかないなと思った。

Discussion