管道(pipe)函数的Typescript类型

13

考虑以下TypeScript代码:

type operator<T> = (input:T) => T

const pipe = <T>(...operators:operator<T>[]) => (input:T):T => operators.reduce((output, f) => f(output), input)

const add2:operator<number> = x => x+2

const times3:operator<number> = x => x*3

console.log(pipe(add2, times3)(1))    //output 9

管道函数就是将一个操作符的输入输出到下一个操作符的结果中。

现在考虑一下这个操作符类型的新定义:

type operator<T, U> = (input:T) => U

如何重写 pipe 函数,以便 IDE 可以让我知道是否正确使用了类型?

例如:考虑这两个操作符:

const times3:operator<number, number> = x => x*3

const toStr:operator<number, string> = x => `${x}`


我希望这个能够正常工作:
pipe(times3, toStr)(1)

我希望IDE能够提醒我类型错误:

pipe(toStr, times3)(1)

我无法弄清楚这个问题,提前感谢。

5个回答

9

这是RxJS的做法

pipe(): Observable<T>;
pipe<A>(op1: OperatorFunction<T, A>): Observable<A>;
pipe<A, B>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>): Observable<B>;
pipe<A, B, C>(op1: OperatorFunction<T, A>, op2: OperatorFunction<A, B>, op3: OperatorFunction<B, C>): Observable<C>;
pipe<A, B, C, D>(
    op1: OperatorFunction<T, A>,
    op2: OperatorFunction<A, B>,
    op3: OperatorFunction<B, C>,
    op4: OperatorFunction<C, D>
): Observable<D>;
pipe<A, B, C, D, E>(
    op1: OperatorFunction<T, A>,
    op2: OperatorFunction<A, B>,
    op3: OperatorFunction<B, C>,
    op4: OperatorFunction<C, D>,
    op5: OperatorFunction<D, E>
): Observable<E>;
pipe<A, B, C, D, E, F>(
    op1: OperatorFunction<T, A>,
    op2: OperatorFunction<A, B>,
    op3: OperatorFunction<B, C>,
    op4: OperatorFunction<C, D>,
    op5: OperatorFunction<D, E>,
    op6: OperatorFunction<E, F>
): Observable<F>;
pipe<A, B, C, D, E, F, G>(
    op1: OperatorFunction<T, A>,
    op2: OperatorFunction<A, B>,
    op3: OperatorFunction<B, C>,
    op4: OperatorFunction<C, D>,
    op5: OperatorFunction<D, E>,
    op6: OperatorFunction<E, F>,
    op7: OperatorFunction<F, G>
): Observable<G>;
pipe<A, B, C, D, E, F, G, H>(
    op1: OperatorFunction<T, A>,
    op2: OperatorFunction<A, B>,
    op3: OperatorFunction<B, C>,
    op4: OperatorFunction<C, D>,
    op5: OperatorFunction<D, E>,
    op6: OperatorFunction<E, F>,
    op7: OperatorFunction<F, G>,
    op8: OperatorFunction<G, H>
): Observable<H>;
pipe<A, B, C, D, E, F, G, H, I>(
    op1: OperatorFunction<T, A>,
    op2: OperatorFunction<A, B>,
    op3: OperatorFunction<B, C>,
    op4: OperatorFunction<C, D>,
    op5: OperatorFunction<D, E>,
    op6: OperatorFunction<E, F>,
    op7: OperatorFunction<F, G>,
    op8: OperatorFunction<G, H>,
    op9: OperatorFunction<H, I>
): Observable<I>;
pipe<A, B, C, D, E, F, G, H, I>(
    op1: OperatorFunction<T, A>,
    op2: OperatorFunction<A, B>,
    op3: OperatorFunction<B, C>,
    op4: OperatorFunction<C, D>,
    op5: OperatorFunction<D, E>,
    op6: OperatorFunction<E, F>,
    op7: OperatorFunction<F, G>,
    op8: OperatorFunction<G, H>,
    op9: OperatorFunction<H, I>,
    ...operations: OperatorFunction<any, any>[]
): Observable<unknown>;

它不够美观,但它完成了工作。


我明白了,但这只能让我传输有限数量的运算符。 - Sarrio
1
是的,但源代码是有限的,这并不一定是个问题。你只需要在声明函数时比使用它的人拥有更强的打字耐力 ;-)。可以肯定地说 RxJS 团队已经调查过是否存在更好的解决方案。如果他们找到了更好的解决方案,他们就不会使用这个了,不是吗? - meriton
虽然我知道这不是相同的签名,但使用“构建器”模式可能更好。 - Goblinlord

5

我知道这不是完全相同的函数签名,但我可以建议使用构建器模式吗?

TypeScript Playground示例

const pipe = <A, B>(fn: (a: A) => B) => {
    return {
        f: function<C>(g: (x: B) => C) { return pipe((arg: A) => g(fn(arg)))},
        build: () => fn
    }
}

const compose = <A, B>(fn: (a: A) => B) => {
    return {
        f: function<C>(g: (x: C) => A) { return compose((arg: C) => fn(g(arg)))},
        build: () => fn
    }
}


const add = (x: number) => (y: number) => x + y
const format = (n: number) => `value: ${n.toString()}`
const upper = (s: string) => s.toUpperCase()

const process = pipe(add(2))
  .f(add(6))
  .f(format)
  .f(upper)
  .build()


const process2 = compose(upper)
  .f(format)
  .f(add(6))
  .f(add(5))
  .build()


console.log(process(6))
console.log(process2(6))


虽然我能让它工作,但我觉得这会产生一个基于深度堆栈的函数,而这通常是要避免的。 - gfache
基本上,这就是管道通常所做的事情(f(g(x)))。它基本上是将x函数调用层次结构变成2x函数层次结构。我完全同意这不是理想的...但是我有点喜欢它,因为它比n个重载更好,其中n是您可能想要使用的所有可能函数参数个数。 - Goblinlord
你可以在 Ramda 中看到这一点,例如:https://github.com/ramda/ramda/blob/v0.27.0/source/pipe.js#L36 这是管道的基本概念(将一些函数列表折叠)。 - Goblinlord
抱歉,“It is essentially”是指我的方法。我的方法是添加一个额外的函数作用域,以便允许类型可访问。可能有一种改进方法……但是……我不确定在TypeScript中该怎么做。 - Goblinlord
这里是 Ramda 在 reduce 函数中递归调用 pipe 的地方:https://github.com/ramda/ramda/blob/v0.27.0/source/internal/_reduce.js#L17我有点颠倒了,但这就是 Ramda 做“x => g(f(x))”的地方,几乎和我做的完全一样。 https://github.com/ramda/ramda/blob/v0.27.0/source/internal/_pipe.js#L3实际上,现在看它... 它几乎和我所做的完全一样。您应该获得确切相同的函数调用堆栈大小。我不确定您认为自己正在避免什么。如果你使用了 pipe(A, B, C),你会得到“x=>C(B(A(x)))”(每个之间有一层)。 - Goblinlord
值得注意的是,这也正是 RxJS 所做的事情: https://github.com/ReactiveX/rxjs/blob/f174d38554d404f21f98ab1079a101e80d777e95/src/internal/util/pipe.ts#L33 - Goblinlord

4

受到Goblinlord, meritonArray Sort的这个解决方案的启发。

代码

type LastElement<T> = T extends [...unknown[], infer LastItem] ? LastItem : never

type Operator<A, B> = (value: A) => B
type OperatorA<T> = T extends Operator<infer A, any> ? A : never
type OperatorB<T> = T extends Operator<any, infer B> ? B : never

type PipeOperators<Operators extends unknown[], Input> =
  Operators extends [infer Item, ...infer Tail]
  ? [Operator<Input, OperatorB<Item>>, ...PipeOperators<Tail, OperatorB<Item>>]
  : Operators
type PipeOperatorsOutput<Operators extends unknown[]> = OperatorB<LastElement<Operators>>

function pipe<Input, Operators extends unknown[]>(...operators: PipeOperators<Operators, Input>): (input: Input) => PipeOperatorsOutput<Operators> {
  return operators as never // Runtime implementation.
}



const add = (x: number) => (y: number) => x + y
const format = (n: number) => `value: ${n.toString()}`
const upper = (s: string) => s.toUpperCase()


const __TEST1__: string = pipe(add(2), format, upper)(1)
const __TEST2__: string = pipe(add(2), upper)(1) // Error: Type 'number' is not assignable to type 'string'.
const __TEST3__: string = pipe(add(2), format)("") // Error: Argument of type 'string' is not assignable to parameter of type 'number'.
const __TEST4__: string = pipe(add(2), format)(1)
const __TEST5__: number = pipe(add(2), add(2))(1)

有些地方我使用了anyunknown,但实际上应该使用更精确的类型。但到目前为止,这是我让代码工作的唯一方法。

如果它不能正常工作,请不要太苛刻。


1
这真的很好!对我来说,任何和未知因素都完全没问题:它们只是在其狭窄的背景下丢弃不需要的信息。 - undefined

1

Goblinlord的回答很启发人,如果运行时递归是问题所在,我们可以对实际实现进行类型擦除,这样我们就可以用迭代替换递归。类型擦除会带来缺陷可能会逃脱编译时类型检查的风险,但我认为这是我愿意承担的代价。

type Fn<T, U> = (i: T) => U
type Pipe<T, U> = { f: <K>(fn: Fn<U, K>) => Pipe<T, K>, build: () => Fn<T, U> }

function pipe<T, U>(fn: Fn<T, U>): Pipe<T, U> {
    const fns: Fn<any, any>[] = [fn]
    const p: Pipe<any, any> = {
        f: (fn) => {
            fns.push(fn);
            return p;
        },
        build: () => {
            return (input) => fns.reduce((prev, curr) => curr(prev), input);
        }
    }
    return p;
}


const add = (x: number) => (y: number) => x + y
const format = (n: number) => `value: ${n.toString()}`
const upper = (s: string) => s.toUpperCase()

const process = pipe(add(2))
  .f(add(6))
  .f(format)
  .f(upper)
  .build()

console.log(process(1))


小心处理这个问题。共享的可变状态对用户来说并不明显,会导致意外的错误。 如果你定义了一个共同的基类,然后再扩展它两次,第二次扩展也会继承第一次扩展的完整链。你需要在compose函数中克隆数组,使其可行。 - undefined

0

Typescript 抽象类 (文档)

扩展自Goblinlord的答案

这是一种强类型安全解决方案

// base.ts

type Operator<I, O> = (input: I) => O;

interface OperatorInterface<I, O> {
    build(): Operator<I, O>;
    compute(input: I): ReturnType<Operator<I, O>>;
}

abstract class BaseOperator<I, O> implements OperatorInterface<I, O> {
    protected operator: Operator<I, O>;

    public constructor(operator: Operator<I, O>) {
        this.operator = operator;
    }

    public build(): Operator<I, O> {
        return this.operator;
    }

    public compute(input: I): O {
        return this.operator(input);
    }
}

export { BaseOperator, type Operator };


// pipe/pipe.ts

import { BaseOperator, type Operator } from '../base';

interface PipeInterface<I, O> {
    to<T>(nextOperator: Operator<O, T>): PipeInterface<I, T>;
}

class Pipe<I, O> extends BaseOperator<I, O> implements PipeInterface<I, O> {
    public constructor(operator: Operator<I, O>) {
        super(operator);
    }

    public to<T>(nextOperator: Operator<O, T>): Pipe<I, T> {
        return new Pipe<I, T>((input: I) => nextOperator(this.compute(input)));
    }
}

function pipe<I, O>(operator: Operator<I, O>): Pipe<I, O> {
    return new Pipe<I, O>(operator);
}

export { Pipe };
export default pipe;


// compose/compose.ts

import { BaseOperator, type Operator } from '../base';

interface ComposeInterface<I, O> {
    from<T>(prevOperator: Operator<T, I>): ComposeInterface<T, O>;
}

class Compose<I, O>
    extends BaseOperator<I, O>
    implements ComposeInterface<I, O>
{
    public constructor(operator: Operator<I, O>) {
        super(operator);
    }

    public from<T>(prevOperator: Operator<T, I>): Compose<T, O> {
        return new Compose((input: T) => this.compute(prevOperator(input)));
    }
}

function compose<I, O>(nextCompose: Operator<I, O>): Compose<I, O> {
    return new Compose<I, O>(nextCompose);
}

export { Compose };
export default compose;


享受编程 :)


// pipe/index.ts 中,使用 export * from './pipe'; 导出模块:在 // compose/index.ts 中,使用 export * from './compose'; 导出模块: - mikoloism

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接