【问题标题】:Typing compose function in TypeScript (Flow $Compose)在 TypeScript 中键入 compose 函数(Flow $Compose)
【发布时间】:2018-08-24 22:32:06
【问题描述】:

在流程中支持$Compose 函数(请参阅recompose as example)。但是,我似乎在打字稿中找不到这样的机制。似乎最好的打字稿可以做的是https://github.com/reactjs/redux/blob/master/index.d.ts#L416-L460。 Typescript 中的$Compose 等价于什么?

编辑:我想要完成的是从recomposeredux 键入compose 函数,使其类型安全。特别是,对于反应高阶组件,我想确保一个 HOC 的输出 props 满足下一个 HOC 的输入 props。这是我目前的解决方法,并且似乎工作得相当好 - 尽管我希望有一种很好的方法可以在 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)
}

【问题讨论】:

  • $Compose 没有记录,您的链接也不清楚。你能提供一个更详细的解释(也许有例子)它做什么/你想做什么?
  • @RyanCavanaugh 澄清了。

标签: typescript types flowtype


【解决方案1】:

我读到你的问题如下:

我怎样才能给这个高阶函数一个 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 应该是一个类型对齐的函数列表 - 每个函数的输出必须与下一个。 TypeScript 的数组是同质类型的——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 是一个 infix 函数,它可以减少嵌套括号。

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) 整洁,但也不算太可怕,而且它的扩展性比编写大量重载要好。

【讨论】:

  • 不错...这就像 lodash 对数组所做的那样 - 将它们包装到已定义操作的链对象中。除了为此具有特殊的内置类型(如 Flow 具有)或能够使用 compose 函数(如 Scala)扩展所有函数之外,这可能是最好的选择。
  • 是否有可能以类似于扩展的方式破解泛型函数类型的原型(与 C# 中的含义相同),以便我们可以执行与 Scala 中相同的语法?
  • @Rasto 我相信 Function.prototype 在 JS 中是只读的,所以很遗憾你不能将方法绑定到 Function
  • @Rasto 如果你喜欢我的回答,别忘了奖励赏金!
  • 对不起,太晚了。你只有一半,太浪费了……不过,我喜欢它。仍然我的赏金帮助这个问题引起了更多的关注,我认为随着时间的推移,你会从投票中获得更多的声誉。
【解决方案2】:

从 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 }

【讨论】:

  • 这不起作用,因为其余元素必须是数组中的最后一个元素,因此您的 ...any[] 失败。
  • @azizj 正如我在顶部提到的,它从 Typescript 4 开始工作,引入了中间休息参数。它目前在我的 Typescript 4 项目中按预期工作。
【解决方案3】:

这是 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'

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 2023-01-02
    • 2017-11-23
    • 1970-01-01
    • 2017-05-12
    • 2022-01-07
    • 1970-01-01
    • 2023-01-03
    相关资源
    最近更新 更多