任意数量的通用参数

Posted

技术标签:

【中文标题】任意数量的通用参数【英文标题】:Arbitrary amount of generic arguments 【发布时间】:2021-08-26 09:29:12 【问题描述】:

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

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】:

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

以下方法使用extends ReadonlyArray&lt;Fn&lt;any, any&gt;&gt; 的通用参数来实现“任意数量的通用参数”。


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

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

【讨论】:

太棒了,关于类型系统我不知道的东西太多了...

以上是关于任意数量的通用参数的主要内容,如果未能解决你的问题,请参考以下文章

如何编写一个丢弃其参数的通用可变参数 lambda?

如何在 TypeScript 3+ 中正确键入通用元组剩余参数?

为具有可变数量参数的不同函数编写通用包装器

TypeScript 3 的通用 curry 函数

通用分页的编写

“通用类型系统”(CTS)