Steve
Steve

Reputation: 8839

Avoid function overload explosion

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

Answers (1)

Mingwei Samuel
Mingwei Samuel

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);

Playground Link


Old bad answer: Playground Link

Upvotes: 3

Related Questions