【问题标题】: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>

但是有更好的方法吗?

【问题讨论】:

    标签: typescript generics typescript-typings typescript-generics


    【解决方案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

    【讨论】:

    • 太棒了,关于类型系统我不知道的东西太多了...
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2021-06-03
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多