【问题标题】:How to type pipe function using variadic tuple types in TypeScript 4?如何在 TypeScript 4 中使用可变元组类型键入管道函数?
【发布时间】:2020-12-16 07:55:48
【问题描述】:

TypeScript 4 发行说明的example 展示了如何使用可变元组类型来避免多个重载定义。我想应该可以为任意数量的参数键入这个pipe 函数

type F<P, R> = (p: P) => R

type Pipe2<T1, T2, R> = [F<T1, T2>, F<T2, R>]
type Pipe3<T1, T2, T3, R> = [F<T1, T2>, ...Pipe2<T2, T3, R>]
type Pipe4<T1, T2, T3, T4, R> = [F<T1, T2>, ...Pipe3<T2, T3, T4, R>]

function pipe<T1, R>(f1: F<T1, R>): F<T1, R>
function pipe<T1, T2, R>(...fns: Pipe2<T1, T2, R>): F<T1, R>
function pipe<T1, T2, T3, R>(...fns: Pipe3<T1, T2, T3, R>): F<T1, R>
function pipe<T1, T2, T3, T4, R>(...fns: Pipe4<T1, T2, T3, T4, R>): F<T1, R>
function pipe(...fns) {
  return x => fns.reduce((res, f) => f(res), x)
}

一个基本的开始可以是

function pipe<Fns>(...fns: PipeArgs<Fns>): PipeReturn<Fns>
function pipe(...fns) {
  return x => fns.reduce((res, f) => f(res), x)
}

帮助器类型 PipeArgs&lt;Fns&gt;PipeReturn&lt;Fns&gt; 的定义仍然缺失。如何定义它们或是否有其他方法?


编辑:我不再那么自信了,但是(TypeScript 4.1.2)。主要问题是其余参数。必须推断pipe 的其余参数fns 的(元组)类型,但必须确保特定的(循环?)结构。这是我目前的方法(使用有效的PipeReturn&lt;Fns&gt;

type AssertReturn<E, _A extends E, R> = R

type Return<F> =
    F extends ((...args: any[]) => infer R)
        ? R
        : never

type Length<L extends any[]> = L['length']

type Tail<L extends any[]> =
    L extends readonly [any, ...infer LTail]
        ? LTail
        : L

type Last<L extends any[]> = L[Length<Tail<L>>]

type F<P, R> = (p: P) => R

type PipeArgs<Fns> =
    Fns extends readonly [F<infer X, infer Y>, ...infer T]
        ? T extends readonly [F<any, any>, ...any]
            ? [F<X, Y>, ...PipeArgs<T>]
            : T extends readonly []
                ? [F<X, Y>]
                : never
        : never

type PipeReturn<Fns extends F<any, any>[]> =
    Fns extends readonly [F<infer I, infer O>, ...infer T]
        ? T extends readonly [F<any, any>, ...any]
            ? F<I, Return<Last<T>>>
            : F<I, O>
        : never

在我展示我尝试过但不起作用的pipe 的签名之前,我先展示一些测试/示例及其预期行为

declare const a: any

const ae_pass_1: number = a as AssertReturn<number, number, number>
const ae_pass_2: string = a as AssertReturn<number, number, string>
// Expected compile error:
//   Type 'string' does not satisfy the constraint 'number'.
//                                                    V
const ae_pass_3: string = a as AssertReturn<number, string, string>
// Expected compile error:
//   Type 'string' is not assignable to type 'number'.
//            V
const ae_fail_returnType: number = a as AssertReturn<number, number, string>


declare const pr1: PipeReturn<[F<number, string>]>
const pr1_pass: F<number, string> = pr1
// Expected compile error:
//   Type 'F<number, string>' is not assignable to type 'F<number, boolean>'.
//       V
const pr1_fail: F<number, boolean> = pr1

declare const pr2: PipeReturn<[F<number, string>, F<string, boolean>]>
const pr2_pass: F<number, boolean> = pr2
// Expected compile error:
//   Type 'F<number, boolean>' is not assignable to type 'F<number, string>'.
//       V
const pr2_fail: F<number, string> = pr2


declare const pa1: PipeArgs<[F<number, string>]>
const pa1_pass: [F<number, string>] = pa1
// Expected compile error:
//   Type '[F<number, string>]' is not assignable to type '[F<number, boolean>]'.
//       V
const pa1_fail: [F<number, boolean>] = pa1

declare const pa2: PipeArgs<[F<number, string>, F<string, boolean>]>
const pa2_pass: [F<number, string>, F<string, boolean>] = pa2
// Expected compile error:
//   Type '[F<number, string>, F<string, boolean>]' is not assignable to type '[F<number, string>, F<number, boolean>]'.
//       V
const pa2_fail: [F<number, string>, F<number, boolean>] = pa2


declare const numberToString: F<number, string>
declare const stringToBoolean: F<string, boolean>

// no compile error expected
const pipe_pass: F<number, boolean> =
    pipe<[F<number, string>, F<string, boolean>]>(numberToString, stringToBoolean)
// no compile error expected
const pipe_pass_argTypeInfered: F<number, boolean> =
    pipe(numberToString, stringToBoolean)
// assignment should cause compile error since second function should expect
// string as parameter, but actually expects number:
//   Type 'F<number, boolean>' is not assignable to type 'F<number, string>'.
//             V
const pipe_fail_returnType: F<number, string> =
    pipe(numberToString, stringToBoolean)
// pipe call should cause compile error since second function should expect
// string as parameter, but actually expects number
// Expected compile error should be something like:
//   Type 'F<number, string>' is not assignable to type 'F<string, T>'.
//                                                                   V
const pipe_fail_args: F<number, string> = pipe(numberToString, numberToString)

以下是不同的pipe 签名以及失败的测试/示例(不符合预期)

function pipe<Fns extends F<any, any>[]>(...fns: PipeArgs<Fns>): PipeReturn<Fns>
const pipe_pass_argTypeInfered: F<number, boolean> =
//  but
//    Argument of type 'F<number, string>' is not assignable to parameter of type 'never'.(2345)
//    The call would have succeeded against this implementation, but implementation signatures of overloads are not externally visible.
//             V
    pipe(numberToString, stringToBoolean)

与以前的方法相比,添加Fns &amp;

function pipe<Fns extends F<any, any>[]>(...fns: Fns & PipeArgs<Fns>): PipeReturn<Fns>

修复了之前的错误,但不会导致这个预期的错误

// pipe call should cause compile error since second function should expect
// string as parameter, but actually expects number
// Expected compile error should be something like:
//   Type 'F<number, string>' is not assignable to type 'F<string, T>'.
//                                                                   V
const pipe_fail_args: F<number, string> = pipe(numberToString, numberToString)

另一种想法是在返回类型中断言Fns具有预期的结构,但是这个定义本身就有错误

//   Type 'Fns' does not satisfy the constraint 'PipeArgs<Fns>'.
//   Type 'F<any, any>[]' is not assignable to type 'PipeArgs<Fns>'.
//                                                                                  V
function pipe<Fns extends F<any, any>[]>(...fns: Fns): AssertReturn<PipeArgs<Fns>, Fns, PipeReturn<Fns>>

编辑 2: 顺便说一句,库 ts-toolbeltseveral type definitions 可以键入您的 pipe 函数,最多 10 个参数(不是任意数量的参数)。

【问题讨论】:

  • 你得到你想要的了吗?我成功地获得了管道最后一步的 returnType,但很难将前一个函数的 returnType 传递给下一个函数。
  • @captain-yossarian 你认为pipe 的实现方式与compose 的实现方式相似吗(即参数顺序相反)?
  • @Bonlou 不,根据 Anders Hejlsberg(TypeScript 的首席架构师)的this 评论,在类型推断中不引入新概念是不可能的。
  • 是的,我认为这是可能的。 Compose 函数只是管道的反转,不是吗?

标签: typescript


【解决方案1】:

看来,Anders comment 已经过时了。

type Foo = typeof foo
type Bar = typeof bar
type Baz = typeof baz

type Fn = (a: any) => any

type Head<T extends any[]> = T extends [infer H, ...infer _] ? H : never

type Last<T extends any[]> = T extends [infer _]
  ? never
  : T extends [...infer _, infer Tl]
  ? Tl
  : never

type Allowed<T extends Fn[], Cache extends Fn[] = []> = T extends []
  ? Cache
  : T extends [infer Lst]
  ? Lst extends Fn
    ? Allowed<[], [...Cache, Lst]>
    : never
  : T extends [infer Fst, ...infer Lst]
  ? Fst extends Fn
    ? Lst extends Fn[]
      ? Head<Lst> extends Fn
        ? ReturnType<Fst> extends Head<Parameters<Head<Lst>>>
          ? Allowed<Lst, [...Cache, Fst]>
          : never
        : never
      : never
    : never
  : never

type FirstParameterOf<T extends Fn[]> = Head<T> extends Fn
  ? Head<Parameters<Head<T>>>
  : never

type Return<T extends Fn[]> = Last<T> extends Fn ? ReturnType<Last<T>> : never

function pipe<
  T extends Fn,
  Fns extends T[],
  Allow extends {
    0: [never]
    1: [FirstParameterOf<Fns>]
  }[Allowed<Fns> extends never ? 0 : 1]
>(...args: [...Fns]): (...data: Allow) => Return<Fns>

function pipe<T extends Fn, Fns extends T[], Allow extends unknown[]>(
  ...args: [...Fns]
) {
  return (...data: Allow) => args.reduce((acc, elem) => elem(acc), data)
}

const foo = (arg: string) => [1, 2, 3]
const baz = (arg: number[]) => 42

const bar = (arg: number) => ['str']

const check = pipe(foo, baz, bar)('hello') // string[]
const check3 = pipe(baz, bar)([2]) // string[]
const check2 = pipe(baz, bar)('hello') // expected error

Playground

还有一个nice fnts 库,它使用Compose 类型,具有更好的错误处理能力

【讨论】:

  • 谢谢,看起来不错。您应该将方法命名为 pipe 而不是 compose。通常pipe从左到右调用传递的函数,compose从右到左调用传递的函数。
  • @maiermic 重命名
  • 仅供参考,我已使用 Prettier 格式化了您的代码。尤其是Allowed的三元表达式很难阅读。
  • 这里stackoverflow.com/questions/65057205/… 你可以找到如何处理匿名函数。类似但又是同一时间,有点挑战
  • 谁能指点我一些解释 TypeScript 中这种类型编程基础的教程?我很难阅读响应中的此类代码...
【解决方案2】:

我今天必须解决一个非常相似的问题。我想我想出了一个相当简单的解决方案。

// Gets last type in a tuple of types
type Last<T extends readonly any[]> = T extends readonly [...any[], infer F]
    ? F
    : never;

// Loose*<T> gives never if T isn't valid, rather than constraining T
type LooseParameters<T> = T extends (...args: infer Args) => any ? Args : never;
type LooseReturnType<T> = T extends (...args: any[]) => infer R ? R : never;
type LooseSetReturnType<NewType, T> = T extends (...args: infer Args) => any
    ? (...args: Args) => NewType
    : never;

/**
 * Gives T if T is a valid pipeline.
 *
 * Tries to give what T should be if not. Example:
 *
 * Pipeline<[(f: any) => number, (f: number[]) => any]> =
 *    [(f: any) => number[], (f: number[]) => any]
 *
 * Notice that only the return type of the first function has changed.
 */
type LoosePipeline<T extends readonly any[]> = T extends readonly [
    infer A,
    infer B,
    ...infer Rest
]
    ? readonly [
          LooseSetReturnType<LooseParameters<B>[0], A>,
          ...LoosePipeline<readonly [B, ...Rest]>
      ]
    : readonly [...T];

function pipe<T extends readonly ((arg: any, ...args: undefined[]) => any)[]>(
    ...pipeline: LoosePipeline<T>
) {
    return (arg: Parameters<T[0]>[0]): LooseReturnType<Last<T>> =>
        pipeline.reduce<any>((acc, elem) => elem(acc), arg);
}

const foo = (arg: string) => [arg.length];
const baz = (arg: number[]) => Math.max(...arg);
const bar = (arg: number) => [arg.toString()];

const check: string[] = pipe(foo, baz, bar)("hello");
const check2: string[] = pipe(baz, bar)([2]);

// @ts-expect-error
const check3 = pipe(baz, bar)("hello");

【讨论】:

  • 谢谢,据我所知,您的回答符合我的预期:)
猜你喜欢
  • 1970-01-01
  • 2021-11-17
  • 1970-01-01
  • 2021-03-09
  • 2018-06-27
  • 2020-02-27
  • 2021-04-15
  • 2019-12-21
  • 2019-09-30
相关资源
最近更新 更多