打字稿随着蓄电池类型的改变而减少

时间:2020-09-08 16:35:21

标签: typescript

我正在尝试使一个类似于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"
)

2 个答案:

答案 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的类型。如果这能神奇地工作,那将是很好,但事实并非如此。

但是,在任何情况下,您都可以表示约束,它甚至可能很有用。只是要小心推断中的陷阱。

Playground link to code

答案 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在他们的路线图上。