任意数量的泛型参数

时间:2021-06-09 11:16:06

标签: typescript generics typescript-typings typescript-generics

考虑任意数量的函数,每个函数接受一个参数,并返回一个值(不要担心它们做什么,而不是重点):

function toNumber(input: string): number {
  return parseInt(input)
}

function toBoolean(input: number): boolean {
  return input > 0
}

function toString(input: boolean): string {
  return input.toString()
}

这些函数可以像这样链接(可能在 fp 中有更好的术语),因为每个函数都将前一个函数的返回类型作为参数:

type Fn<I, O> = (input: I) => O

function chain(...fns: Fn<any, any>[]): Fn<any, any> {
  return (input: any) => {
    let result = input
    for (const fn of fns) {
      result = fn(result)
    }
    return result
  }
}

const chained = chain(toNumber, toBoolean, toString)

const result = chained('5') // returns "true"

打字稿中有没有办法使 chain 函数类型安全?由于我们有任意数量的函数,因此必须有任意数量的通用参数。

我可以做这样的事情(固定数量的重载):

declare function chain<S, T1>(fn: Fn<S, T1>): Fn<S, T1>
declare function chain<S, T1, T2>(fn1: Fn<S, T1>, fn2: Fn<T1, T2>): Fn<S, T2>
declare function chain<S, T1, T2, T3>(fn1: Fn<S, T1>, fn2: Fn<T1, T2>, fn3: Fn<T2, T3>): Fn<S, T3>

但是有更好的方法吗?

1 个答案:

答案 0 :(得分:1)

我不确定我是否会称其为更好的方式,但是可能将此规则编码到类型系统中。然而,如果没有良好的文档,大多数观察者可能很难理解它的作用。

下面的方法使用了一个通用参数 extends ReadonlyArray<Fn<any, any>> 以实现“任意数量的通用参数”。


首先,有几个实用程序类型可以帮助处理数组和函数:

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

type Input<F> = F extends Fn<infer U, any> ? U : never;

// or you can use ReturnType<T>, which is provided by typescript
type Output<F> = F extends Fn<any, infer U> ? U : never;

// first elem of tuple type
type Head<T> = T extends [infer U, ...unknown[]] ? U : never;

// all but first elem of tuple type
type Tail<T> = T extends [unknown, ...(infer U)] ? U : never;

// last element of array, only works on typescript 4.2+
type Last<T> = T extends [...unknown[], infer U] ? U : never;

接下来,检查两个函数是否可以组合的类型

// `true` if T can be composed with U, `false` if not, and `never` if the types are wrong
type CanCompose<T, U> = T extends Fn<any, infer Output> 
    ? (
        U extends Fn<infer Input, any>
            ? (Input extends Output ? true : false) 
            : never
    ) 
    : never;

用于检查整个函数列表是否可以相互组合的类型

// Evaluates to a tuple of booleans. If any element in the tuple is false, you cannot compose the whole array of T.
type IsComposable<T> = T extends Fn<any, any>[] 
    ? CanCompose<Head<T>, Head<Tail<T>>> extends true 
        ? (
            Tail<T> extends never[]
                ? [true] 
                : [true, ...(IsComposable<Tail<T>>)]
        ) 
        : [false]
    : [false];

最后是chain函数的rest参数和返回值的类型

type Composable<T> = IsComposable<T> extends true[] ? T : never[];
type ComposedFunction<T> = IsComposable<T> extends true[] 
    ? Fn<Input<Head<T>>, Output<Last<T>>>
    : Fn<never, never>;

chain 函数现在获得了更令人兴奋的签名,并且在主体中进行了一些类型转换。不幸的是,我不确定如何摆脱类型转换。

function chain<T extends ReadonlyArray<Fn<any, any>>>(...fns: Composable<T>): ComposedFunction<T> {
    return ((x: Input<Head<T>>) => {
        let result = x;
        for (const fn of fns) {
            result = fn(result);
        }
        return result;
    }) as ComposedFunction<T>
}

这种方法的一个缺点是,如果用户代码违反了类型,他们会得到一个神秘的编译错误,即 chain 的第一个参数的类型不能分配给 never

Link to TS playground with all of this stuff in it

相关问题