打字稿:可变函数参数取决于前面的参数

时间:2021-01-10 11:03:20

标签: typescript algorithm generics recursion typescript-typings

这个问题以前肯定有人问过,我几乎可以肯定。然而:

  1. 我找不到任何此类问题
  2. Typescript 在最近取得了重大飞跃,因此现有的答案可能已经过时。

我在代码中常用的东西是用于函数参数的扩展运算符,允许我接收可变长度的参数数组。我现在要做的是为一个函数创建 TS 类型定义,其中 arg1 类型取决于 arg2 类型,而 arg2 取决于 arg3,依此类推。

非常像 lodash https://github.com/DefinitelyTyped/DefinitelyTyped/blob/master/types/lodash/common/util.d.ts#L209

 flow<R2, R3, R4, R5, R6, R7>(f2: (a: ReturnType<T>) => R2, f3: (a: R2) => R3, f4: (a: R3) => R4, f5: (a: R4) => R5, f6: (a: R5) => R6, f7: (a: R6) => R7): Function<(...args: Parameters<T>) => R7>;

lodash 方法显然非常有限,并且在 8 个参数时也最大化(在本例中)。这很好,我可以接受,但是,在 2021 年有更好、更递归的方法吗?一种对(理论上)infinite 个参数执行相同操作的方法?

请随时结束问题并指出现有答案(如果存在且是最新的)。

2 个答案:

答案 0 :(得分:4)

在条件类型中进行推断,这是可能的(尽管有点难看):

// convenience alias
type Func = (...args: any[]) => any;

// check arguments in reverse
// since typescript doesn't like deconstructing arrays into init/last
type CheckArgsRev<T extends Func[]> =
    // get first two elements
    T extends [infer H1, infer H2, ...infer R]
        // typescript loses typings on the inferred types
        // so this gains them back
        ? H1 extends Func ? H2 extends Func ? R extends Func[]
            // actual check
            // ensures parameters of next argument extends return type of previous
            // you can substitute this with whatever check you want to add
            // just know that H1 is the current argument type and H2 is the previous
            // also if you change this to work with non-functions then change Func to an appropriate type
            // like unknown to work with all types
            ? Parameters<H1> extends [ReturnType<H2>]
                // it was a match, recurse onto the tail
                ? [H1, ...CheckArgsRev<[H2, ...R]>]
                : never // invalid type, become never for error
            : never : never : never // should never happen
        // base case, 0 or 1 elements should always pass
        : T;

// reverse a tuple type
type Reverse<T extends unknown[]> =
    T extends [infer H, ...infer R]
        ? [...Reverse<R>, H]
        : [];

// check args not in reverse by reversing twice
type CheckArgs<T extends Func[]> = Reverse<CheckArgsRev<Reverse<T>>>;

// make sure the argument passes the check
function flow<T extends Func[]>(...args: T & CheckArgs<T>) {
    console.log(args);
}

// this is invalid (since number cannot flow into a string)
flow((x: string) => parseInt(x, 10), (x: string) => x + "1");
// this is valid (number flows into number)
flow((x: string) => parseInt(x, 10), (x: number) => x + 1);

Playground link

答案 1 :(得分:1)

Aplet's answer 很棒!但是当您传递错误的参数时,它会放弃有关函数数组的类型信息。当错误实际上源自 Argument of type '(x: string) => number' is not assignable to parameter of type 'never'. 时仅显示 (x: string) => string 的错误不是很有帮助。

不幸的是,似乎没有一种可靠的方法可以做到这一点,但我仍然想分享我的 2 美分-

type AnyFunc = (a: any) => any;

// Given a tuple of functions - deduce how much of it is flowable
type FlowFrom<T extends readonly AnyFunc[]> = T extends [(a: infer R) => any, ...AnyFunc[]]
    ? // Infer the argument type of the first function and pass it to `FlowFrom$`
      FlowFrom$<R, T>
    : // T was empty array
      [];

// Actual function that deduces how much of a tuple is flowable, taking previous return type and remaining functions
type FlowFrom$<R, T extends readonly [(a: R) => any, ...AnyFunc[]]> = T extends [
    (a: R) => infer R$,
    ...infer Tail
]
    ? Tail extends [(a: R$) => any, ...AnyFunc[]]
        ? // Valid, continue
          [(a: R) => R$, ...FlowFrom$<R$, Tail>]
        : // Tail has either been exhausted or invalid function has been found
        Tail extends [(a: any) => any, ...AnyFunc[]]
        ? // Invalid function found, append a correct function type so it points out the specific error
          // But make the appended function optional, the flow is still valid before this point
          [(a: R) => R$, (a: R$) => any] | [(a: R) => R$]
        : // Tail exhausted
          [(a: R) => R$]
    : // T is empty, exhausted tuple (impossible - apparently)
      [];

FlowFrom 从给定的函数元组中推导出“可流动”的元组类型。本质上,它提取元组中所有流入彼此的函数,当它遇到不再可流动的函数或元组耗尽时停止。

// All the functions in the tuple type are flowable - the return type is the same
let valid: FlowFrom<[(x: string) => number, (x: number) => number]>;
//  ^ [(a: string) => number, (a: number) => number]

// First function won't flow into the second one - stop at first one
let invalid: FlowFrom<[(x: string) => number, (x: string) => number]>;
//  ^ [(a: string) => number, (a: number) => any] | [(a: string) => number]

这意味着,当您尝试将 [(x: string) => parseInt(x, 10), (x: string) => x + '1'] 分配给 invalid 时,您会得到一个很好的错误-

let invalid: FlowFrom<[(x: string) => number, (x: string) => number]> = [
    (x: string) => parseInt(x, 10),
    (x: string) => x + '1',
];
//  ^ Type 'number' is not assignable to type 'string'

但是,你仍然可以在那个断点之前分配东西-

let reducedButCorrect: FlowFrom<[(x: string) => number, (x: string) => number]> = [
    (x: string) => parseInt(x, 10),
];

(如果您不想要这种行为,例如 - 当存在断点时仍然能够在断点之前分配所有内容,请从 FlowFrom$, [(a: R) => R$, (a: R$) => any] | [(a: R) => R$] 中删除联合类型-> [(a: R) => R$, (a: R$) => any])

您现在可以像这样使用它-

declare function flow<T extends readonly AnyFunc[]>(...args: T & FlowFrom<T>): unknown;

// Valid
flow(
    (x: string) => parseInt(x, 10),
    (x: number) => Boolean(x),
    (x: boolean) => Number(x)
);

// Invalid
flow(
    (x: string) => parseInt(x, 10),
    (x: number) => Boolean(x),
    (x: string) => Number(x)
);
//  ^ Type 'boolean' is not assignable to type 'string'

playground

上尝试所有这些

只想分享我的两分钱。