在TypeScript中键入compose函数(Flow $ Compose)

时间:2018-03-15 23:27:46

标签: typescript types flowtype

在流程中,支持$Compose个功能(请参阅recompose as example)。但是,我似乎无法在打字稿中找到这样的机制。似乎最好的打字稿可以做的就像https://github.com/reactjs/redux/blob/master/index.d.ts#L416-L460。什么相当于Typescript中的$Compose

编辑:我要完成的是从composerecompose输入redux函数,使其类型安全。特别是,对于反应更高阶的组件,我想确保一个HOC的输出道具满足下一个HOC的输入道具。这是我目前的解决方法,似乎工作得相当好 - 尽管我希望有一种很好的方法可以在打字稿中本地执行此操作。

/** 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)
}

3 个答案:

答案 0 :(得分:8)

您的问题对我来说有点不清楚-似乎依赖于我所缺少的有关React的上下文-但我的读法如下:

  

如何为这种高阶函数赋予TS类型,以使x的类型在整个循环中可以变化?

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

坏消息是您不能直接键入此函数。 funs数组是个问题-为compose提供最通用的类​​型,funs应该是 type-aligned 函数列表-每个函数的输出必须匹配下一个的输入。在TS数组中,数组是同类型的-funs的每个元素必须具有完全相同的类型-因此,您无法在TypeScript中直接表达类型在整个列表中变化的方式。 (上面的JS在运行时起作用,因为类型被删除并且数据被统一表示。)这就是Flow的$Compose是一种特殊的内置类型的原因。

解决此问题的一种方法是执行您在示例中所做的事情:为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

显然,这无法扩展。您必须停在某个地方,如果用户需要编写的功能超出您的预期,那么他们将遭受祸害。

另一种选择是重写代码,这样您一次只能编写一个函数:

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

这使调用代码变得更加混乱-您现在必须写单词compose及其伴随的括号O(n)次。

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

像这样的函数组合管道在函数语言中很常见,那么程序员如何避免这种语法上的不适呢?例如,在Scala中,compose是一个中缀函数,该函数使嵌套括号更少。

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
var comp : = new Comp(f)._(g)._(h)._(k).apply  // comp : (x : T) => R

仍然不如compose(f, g, h, k)整洁,但它并不是太丑陋,并且扩展性好于编写许多重载。

答案 1 :(得分:1)

这是TypeScript中强类型的compose函数的示例。它具有不检查每个中间函数类型的缺点,但是能够导出最终组合函数的arg和返回类型。

撰写功能

/** 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来从最后到第一个通过每个函数输入输入。对于compose的返回类型,我们可以根据第一个函数的返回类型根据最后一个函数参数类型和最终返回类型来推断参数类型。

管道功能

我们还可以创建一个强类型的管道函数,该管道函数将数据从第一个函数传送到下一个函数,等等。

/**
 * 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'

答案 2 :(得分:1)

从 Typescript 4 开始,可变元组类型提供了一种组合函数的方法,其签名是从任意数量的输入函数中推断出来的。

let compose = <T, V>(...args: readonly [
        (x: T) => any,          // 1. The first function type
        ...any[],               // 2. The middle function types
        (x: any) => V           // 3. The last function type
    ]): (x: V) => T =>          // The compose return type, aka the composed function signature
{
    return (input: V) => args.reduceRight((val, fn) => fn(val), input);
};

let pipe = <T, V>(...args: readonly [
        (x: T) => any,          // 1. The first function type
        ...any[],               // 2. The middle function types
        (x: any) => V           // 3. The last function type
    ]): (x: T) => V =>          // The pipe return type, aka the composed function signature
{
    return (input: T) => args.reduce((val, fn) => fn(val), input);
};

然而,这种实现仍有两个缺点:

  1. 编译器无法验证每个函数的输出是否与下一个函数的输入匹配
  2. 编译器在使用扩展运算符时报错(但仍能成功推断出组合签名)

例如以下将在编译时和运行时工作

let f = (x: number) => x * x;
let g = (x: number) => `1${x}`;
let h = (x: string) => ({x: Number(x)});


let foo = pipe(f, g, h);
let bar = compose(h, g, f);

console.log(foo(2)); // => { x: 14 }
console.log(bar(2)); // => { x: 14 }

虽然这会在运行时报错,但正确推断签名并运行

let fns = [f, g, h];
let foo2 = pipe(...fns);

console.log(foo2(2)); // => { x: 14 }