我正在尝试使一个类似于RXJS pipe的运算符来提取过程列表,然后处理它们的流水线样式。但是,Typescript无法检查Reduce的动态类型(累加器具有基于先前执行的类型)。有适当的方法吗?
const pipe = (...fns:Function[])=>{
return fns.reduce((prevRes,curFn)=>curFn(prevRes),undefined)
}
pipe(
()=>true,
(res:string)=>"res should be boolean, this should yell at you"
)
答案 0 :(得分:3)
我可以用variadic tuples表示您想要的约束,但是它对类型推断有一些不幸的副作用。如果您愿意显式注释所有回调的参数,那就太好了。如果您希望编译器为您推断出这些信息,恐怕您可能不会对每个可能的函数数量都比一大堆重载做得更好,如另一个答案所示。
无论如何,让我们介绍一下这些助手:
type Last<T extends any[]> = T extends [...infer _, infer U] ? U : never;
type Prev<T extends any[], K extends keyof T, D> =
K extends keyof [D, ...T] ? [D, ...T][K] : never;
Last<T>
类型采用元组类型T
并返回其最后一个元素(因此Last<["a","b","c"]>
应该为"c"
)。 Prev<T, K, D>
采用元组类型T
和键(类型为数字)键类型K
,并返回元素“ K
-1”处的元素,并返回{ {1}}(如果您为D
传递0
(因此,K
应该是Prev<["a","b","c"],1,"d">
,而"a"
应该是Prev<["a","b","c",0,"d"]>
)。
您可以使用它们来表示与一系列功能相对应的类型:
"d"
我们希望const pipe = <T extends any[]>(
...fns: { [K in keyof T]: (arg: Prev<T, K, undefined>) => T[K] }
) => fns.reduce((prevRes, curFn) => curFn(prevRes), undefined) as Last<T>
在元组pipe()
中是通用的,该元组与作为T
传递的函数的返回类型的有序列表相对应。类型fns
是我们需要的功能类型链;对于{[K in keyof T]: (arg: Prev<T, K, undefined> => T[K]}
中的每个索引K
,结果函数的输入类型是前一个索引的元素;如果没有上一个索引(T
),则结果函数的输入类型是undefined
,而输出类型是Prev<T, K, undefined>
(T
)的当前元素。 T[K]
的输出仅为pipe()
,这是返回类型元组中的最后一个元素。
让我们看看它是如何工作的。好:
Last<T>
此编译没有错误,并且推断const good = pipe(
() => true,
(res: boolean) => res ? 123 : 456,
);
// const good: 123 | 456
的类型为val
。坏:
123 | 456
这正是您想要的错误;编译器期望const bad = pipe(
() => true,
(res: string) => res // error!
//~~~~~~~~~~~~~~~~~~~~
//Type 'boolean' is not assignable to type 'string'
)
的类型为res
,但是您已将其注释为boolean
。现在是丑陋的:
string
糟糕,我忘了在回调中注释const ugly = pipe(
() => "hello", x => x.length
); // any!!!
。编译器无法从返回类型x
上下文中键入x
,而是放弃并隐式使用() => "hello"
。这导致整个元组类型any
被推断为T
,并且所有类型的安全性都丢失了。失败的原因可能是所需推断的循环性质。元组类型any[]
取决于回调的类型,回调本身取决于T
的类型。如果这能神奇地工作,那将是很好,但事实并非如此。
但是,在任何情况下,您都可以表示约束,它甚至可能很有用。只是要小心推断中的陷阱。
答案 1 :(得分:2)
看看RxJS's implementation的管道功能。
export interface UnaryFunction<T, R> { (source: T): R; }
export function pipe<T>(): UnaryFunction<T, T>;
export function pipe<T, A>(fn1: UnaryFunction<T, A>): UnaryFunction<T, A>;
export function pipe<T, A, B>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>): UnaryFunction<T, B>;
export function pipe<T, A, B, C>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>): UnaryFunction<T, C>;
export function pipe<T, A, B, C, D>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>, fn4: UnaryFunction<C, D>): UnaryFunction<T, D>;
export function pipe<T, A, B, C, D, E>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>, fn4: UnaryFunction<C, D>, fn5: UnaryFunction<D, E>): UnaryFunction<T, E>;
export function pipe<T, A, B, C, D, E, F>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>, fn4: UnaryFunction<C, D>, fn5: UnaryFunction<D, E>, fn6: UnaryFunction<E, F>): UnaryFunction<T, F>;
export function pipe<T, A, B, C, D, E, F, G>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>, fn4: UnaryFunction<C, D>, fn5: UnaryFunction<D, E>, fn6: UnaryFunction<E, F>, fn7: UnaryFunction<F, G>): UnaryFunction<T, G>;
export function pipe<T, A, B, C, D, E, F, G, H>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>, fn4: UnaryFunction<C, D>, fn5: UnaryFunction<D, E>, fn6: UnaryFunction<E, F>, fn7: UnaryFunction<F, G>, fn8: UnaryFunction<G, H>): UnaryFunction<T, H>;
export function pipe<T, A, B, C, D, E, F, G, H, I>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>, fn4: UnaryFunction<C, D>, fn5: UnaryFunction<D, E>, fn6: UnaryFunction<E, F>, fn7: UnaryFunction<F, G>, fn8: UnaryFunction<G, H>, fn9: UnaryFunction<H, I>): UnaryFunction<T, I>;
export function pipe<T, A, B, C, D, E, F, G, H, I>(fn1: UnaryFunction<T, A>, fn2: UnaryFunction<A, B>, fn3: UnaryFunction<B, C>, fn4: UnaryFunction<C, D>, fn5: UnaryFunction<D, E>, fn6: UnaryFunction<E, F>, fn7: UnaryFunction<F, G>, fn8: UnaryFunction<G, H>, fn9: UnaryFunction<H, I>, ...fns: UnaryFunction<any, any>[]): UnaryFunction<T, {}>;
要使类型推断对操作符起作用,他们必须定义一堆带有通用占位符的重载函数签名,以供操作符签名填写。根据传递给管道函数的操作符数量确定使用哪个重载,最多9个,超过此范围将退回到any
。
Here我举了一个简化的示例,您可以使用自己的代码进行试用。
这不是Typescript最优雅的用法,但可以接受,因为在单个管道中需要这么多运算符可能不是很常见。如果您确实需要更多,则可以将另一个呼叫链接到pipe
。
幸运的是,最近发布的Typescript 4.0大大改善了对Variadic Tuple Types和推断的支持,这可以提供不需要很多函数签名重载的更好解决方案。 RxJS维护者already have it在他们的路线图上。