考虑任意数量的函数,每个函数接受一个参数,并返回一个值(不要担心它们做什么,而不是重点):
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>
但是有更好的方法吗?
答案 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
。