web-dev-qa-db-ja.com

TypeScriptでcompose関数を入力する(Flow $ Compose)

フローでは、$Compose関数がサポートされています( 例として再構成 を参照)。しかし、TypeScriptではそのようなメカニズムを見つけることができないようです。 TypeScriptでできる最善の方法は https://github.com/reactjs/redux/blob/master/index.d.ts#L416-L46 のようなもののようです。 Typescriptの$Composeに相当するものは何ですか?

編集:私が達成しようとしているのは、タイプセーフになるようにcomposeまたはrecomposeからredux関数を入力することです。特に、高次のコンポーネントを反応させる場合、あるHOCの出力小道具が次のHOCの入力小道具を満たすようにします。これは私の現在の回避策であり、かなりうまく機能しているようです-TypeScriptでネイティブにこれを行う良い方法があることを望んでいましたが。

/** Wraps recompose.compose in a type-safe way */
function composeHOCs<OProps, I1, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<IProps, I1>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs<OProps, I1, I2, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<I2, I1>,
  f3: InferableComponentEnhancerWithProps<IProps, I2>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs<OProps, I1, I2, I3, IProps>(
  f1: InferableComponentEnhancerWithProps<I1, OProps>,
  f2: InferableComponentEnhancerWithProps<I2, I1>,
  f3: InferableComponentEnhancerWithProps<I3, I2>,
  f4: InferableComponentEnhancerWithProps<IProps, I3>,
): ComponentEnhancer<IProps, OProps>
function composeHOCs(
  ...fns: Array<InferableComponentEnhancerWithProps<any, any>>
): ComponentEnhancer<any, any> {
  return compose(...fns)
}
12
Tony

あなたの質問は私には少し不明確です-私が行方不明になっているReact)に関するいくつかのコンテキストに依存しているようです-しかし私はそれを次のように読んでいます:

xの型がループ全体で変化できるように、この高階関数にTS型を与えるにはどうすればよいですか?

_function compose(...funs) {
    return function(x) {
        for (var i = funs.length - 1; i >= 0; i--) {
            x = funs[i](x);
        }
        return x;
    }
}
_

悪いニュースは、この関数を直接入力できないことです。 funs配列が問題です-composeに最も一般的な型を与えるには、funstype-aligned関数のリスト-出力である必要があります各関数のは、次の入力と一致する必要があります。 TS配列は同種の型であり、funsの各要素はまったく同じ型である必要があります。そのため、TypeScriptのリスト全体で型がどのように変化するかを直接表現することはできません。 (上記のJSは、型が消去され、データが均一に表されるため、実行時に機能します。)そのため、Flowの_$Compose_は特別な組み込み型です。

これを回避する1つのオプションは、例で行ったことを実行することです。さまざまな数のパラメーターを使用して、composeのオーバーロードの束を宣言します。

_function compose<T1, T2, T3>(
    f : (x : T2) => T3,
    g : (x : T1) => T2
) : (x : T1) => T3
function compose<T1, T2, T3, T4>(
    f : (x : T3) => T4,
    g : (x : T2) => T3,
    h : (x : T1) => T2
) : (x : T1) => T4
function compose<T1, T2, T3, T4, T5>(
    f : (x : T4) => T5,
    g : (x : T3) => T4,
    h : (x : T2) => T3,
    k : (x : T1) => T2
) : (x : T1) => T5
_

明らかに、これはスケーリングしません。どこかで立ち止まらなければならず、ユーザーが予想よりも多くの機能を作成する必要がある場合は、ユーザーを苦しめます。

もう1つのオプションは、一度に1つの関数のみを作成するようにコードを書き直すことです。

_function compose<T, U, R>(g : (y : U) => R, f : (x : T) => U) : (x : T) => R {
    return x => f(g(x));
}
_

これは、呼び出し元のコードをかなり混乱させます。Wordcomposeとそれに付随する括弧、O(n)回を記述する必要があります。

_compose(f, compose(g, compose(h, k)))
_

このような関数合成パイプラインは関数型言語では一般的ですが、プログラマーはこの構文上の不快感をどのように回避するのでしょうか。たとえば、Scalaでは、composeinfix関数であり、ネストされた括弧が少なくなります。

_f.compose(g).compose(h).compose(k)
_

Haskellでは、composeのスペルは_(.)_であり、非常に簡潔な構成になります。

_f . g . h . k
_

実際、TSのインフィックスcomposeを一緒にハックすることができます。アイデアは、合成を実行するメソッドを使用して、基になる関数をオブジェクトにラップすることです。そのメソッドをcomposeと呼ぶこともできますが、ノイズが少ないので___と呼んでいます。

_class Comp<T, U> {
    readonly apply : (x : T) => U

    constructor(apply : (x : T) => U) {
        this.apply = apply;
    }

    // note the extra type parameter, and that the intermediate type T is not visible in the output type
    _<V>(f : (x : V) => T) : Comp<V, U> {
        return new Comp(x => this.apply(f(x)))
    }
}

// example
const comp : (x : T) => R = new Comp(f)._(g)._(h)._(k).apply
_

それでもcompose(f, g, h, k)ほどきれいではありませんが、それほど恐ろしいものではなく、大量のオーバーロードを書き込むよりも拡張性が高くなります。

11

TypeScriptの強力な型付き作成関数の例を次に示します。各中間関数の型をチェックしないという欠点がありますが、最終的に構成された関数の引数と戻り値の型を導出することはできます。

関数の作成

/** Helper type for single arg function */
type Func<A, B> = (a: A) => B;

/**
 * Compose 1 to n functions.
 * @param func first function
 * @param funcs additional functions
 */
export function compose<
  F1 extends Func<any, any>,
  FN extends Array<Func<any, any>>,
  R extends
    FN extends [] ? F1 :
    FN extends [Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    FN extends [any, any, any, any, Func<infer A, any>] ? (a: A) => ReturnType<F1> :
    Func<any, ReturnType<F1>> // Doubtful we'd ever want to pipe this many functions, but in the off chance someone does, we can still infer the return type
>(func: F1, ...funcs: FN): R {
  const allFuncs = [func, ...funcs];
  return function composed(raw: any) {
    return allFuncs.reduceRight((memo, func) => func(memo), raw);
  } as R
}

使用例:

// compiler is able to derive that input type is a Date from last function
// and that return type is string from the first
const c: Func<Date, string> = compose(
  (a: number) => String(a),
  (a: string) => a.length,
  (a: Date) => String(a)
);

const result: string = c(new Date());

仕組み関数の配列でreduceRightを使用して、各関数を介して入力を最後から最初にフィードします。構成の戻り値の型については、最後の関数の引数の型に基づいて引数の型を推測し、最初の関数の戻り値の型から最後の戻り値の型を推測することができます。

パイプ機能

また、データを最初の関数から次の関数にパイプする、強い型のパイプ関数を作成することもできます。

/**
 * Creates a pipeline of functions.
 * @param func first function
 * @param funcs additional functions
 */
export function pipe<
  F1 extends Func<any, any>,
  FN extends Array<Func<any, any>>,
  R extends
    FN extends [] ? F1 :
    F1 extends Func<infer A1, any> ?
      FN extends [any] ? Func<A1, ReturnType<FN[0]>> :
      FN extends [any, any] ? Func<A1, ReturnType<FN[1]>> :
      FN extends [any, any, any] ? Func<A1, ReturnType<FN[2]>> :
      FN extends [any, any, any, any] ? Func<A1, ReturnType<FN[3]>> :
      FN extends [any, any, any, any, any] ? Func<A1, ReturnType<FN[4]>> :
      Func<A1, any> // Doubtful we'd ever want to pipe this many functions, but in the off chance someone does, we can infer the arg type but not the return type
    : never
>(func: F1, ...funcs: FN): R {
  const allFuncs = [func, ...funcs];
  return function piped(raw: any) {
    return allFuncs.reduce((memo, func) => func(memo), raw);
  } as R
}

使用例

// compile is able to infer arg type of number based on arg type of first function and 
// return type based on return type of last function
const c: Func<number, string> = pipe(
  (a: number) => String(a),
  (a: string) => Number('1' + a),
  (a: number) => String(a)
);

const result: string = c(4); // yields '14'
1
bingles