Reputation: 8839
Does TypeScript provide a way to avoid this sort of method overloading explosion and, in doing so, provide type safety to an unlimited number of varargs?
type Operator<FROM, TO> = (source: Stream<FROM>) => Stream<TO>
class Stream<V> {
// ...
pipe<A>(operator: Operator<VALUE, A>): Stream<A>
pipe<A, B>(op1: Operator<VALUE, A>, op2: Operator<A, B>): Stream<B>
pipe<A, B, C>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>): Stream<C>
pipe<A, B, C, D>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>, op4: Operator<C, D>): Stream<D>
pipe<A, B, C, D, E>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>, op4: Operator<C, D>, op5: Operator<D, E>): Stream<E>
pipe<A, B, C, D, E, F>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>, op4: Operator<C, D>, op5: Operator<D, E>, op6: Operator<E, F>): Stream<F>
pipe<A, B, C, D, E, F, G>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>, op4: Operator<C, D>, op5: Operator<D, E>, op6: Operator<E, F>, op7: Operator<F, G>): Stream<G>
pipe<A, B, C, D, E, F, G, H>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>, op4: Operator<C, D>, op5: Operator<D, E>, op6: Operator<E, F>, op7: Operator<F, G>, op8: Operator<G, H>): Stream<H>
pipe<A, B, C, D, E, F, G, H, I>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>, op4: Operator<C, D>, op5: Operator<D, E>, op6: Operator<E, F>, op7: Operator<F, G>, op8: Operator<G, H>, op9: Operator<H, I>): Stream<I>
pipe<A, B, C, D, E, F, G, H, I, J>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>, op4: Operator<C, D>, op5: Operator<D, E>, op6: Operator<E, F>, op7: Operator<F, G>, op8: Operator<G, H>, op9: Operator<H, I>, op10: Operator<I, J>): Stream<J>
pipe<A, B, C, D, E, F, G, H, I, J, K>(op1: Operator<VALUE, A>, op2: Operator<A, B>, op3: Operator<B, C>, op4: Operator<C, D>, op5: Operator<D, E>, op6: Operator<E, F>, op7: Operator<F, G>, op8: Operator<G, H>, op9: Operator<H, I>, op10: Operator<I, J>, ...restOps: Operator<unknown, unknown>[]): Stream<K>
pipe<TO_VALUE>(operator: Operator<VALUE, unknown>, ...restOperators: Operator<unknown, unknown>[]): Stream<TO_VALUE> {
return restOperators.reduce((stream, operator) => operator(stream), this as Stream<unknown>) as Stream<TO_VALUE>
}
}
Note that each Operator
’s output type is the next Operator
’s input type.
Upvotes: 2
Views: 147
Reputation: 3292
It's possible.
Here is a working example which requires TypeScript 4 tuple types. The idea is to use an array generic T
to keep track of each consecutive type to make sure the operators agree.
I based this answer off of this comment by @jcalz.
type Operator<FROM, TO> = (source: Stream<FROM>) => Stream<TO>
type Prev<T extends any[], K, D> = K extends keyof [ D, ...T ] ? [ D, ...T ][K] : never;
type Operators<VALUE, T extends any[]> = {
[K in keyof T]: Operator<Prev<T, K, VALUE>, T[K]>
};
type PipeResult<T extends any[]> = T extends [ ...infer _, infer U ] ? Stream<U> : never;
class Stream<VALUE> {
x?: VALUE; // Needed to make sure Stream<X> doesn't always extend Stream<Y>.
pipe<T extends any[]>(...operators: Operators<VALUE, T>): PipeResult<T> {
return operators.reduce<Stream<unknown>>((stream, operator) => operator(stream), this) as PipeResult<T>;
}
}
Testing:
const OpA: Operator<number, 'hi'> = null as any;
const OpB: Operator<string, 'hello'> = null as any;
const OpC: Operator<string, Date> = null as any;
const OpD: Operator<1521, string> = null as any;
const OpE: Operator<5, 6> = null as any;
const OpF: Operator<6, 7> = null as any;
const NumberStream = new Stream<number>();
// x0: Stream<Date>
const x0 = NumberStream.pipe(OpA, OpB, OpC);
// x1: Stream<"hello">
const x1 = NumberStream.pipe(
OpA, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB,
OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB,
OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB, OpB);
// Error on arg 3.
const x2 = NumberStream.pipe(OpA, OpC, OpC);
// Error on arg 1.
const x3 = NumberStream.pipe(OpD, OpE, OpF);
Old bad answer: Playground Link
Upvotes: 3