2

When using tuple types in generic functions, how can you 'extend' the source type?

Let's say we want to make a RxJS mapping operator that returns the source observable value(s) together with another mapped value:

export function mapExtended<T, R>(mapping: (input: [T]) => R): OperatorFunction<[T], [T, R]>;
export function mapExtended<T1, T2, R>(mapping: (input: [T1, T2]) => R): OperatorFunction<[T1, T2], [T1, T2, R]>;
export function mapExtended<T1, T2, T3, R>(mapping: (input: [T1, T2, T3]) => R): OperatorFunction<[T1, T2, T3], [T1, T2, T3, R]>;
export function mapExtended<R>(mapping: (input: any) => R): OperatorFunction<any, {}> {
    return (source$: Observable<any>) => source$.pipe(
        map(input => {
            const mappingResult = mapping(input);
            return [...input, mappingResult];
        }),
    );
}

That seems to work but the overloads aren't detected properly. The return type for test is, Observable<[[number, number], number]> instead of the expected Observable<[number, number, number]>:

const test = combineLatest(of(1), of(2)).pipe(
    mapExtend(([s1, s2]) => s1 + s2),
);

Is there some sort of conditional type checking to say T cannot be a tuple type?


I tried accomplishing the same with mapped type support for tuples, but to no avail, as I cannot seem to (or I do not know how to) 'extend' the mapped type:

type MapExtended2Input<T> = { [P in keyof T]: T[P] };
function mapExtended2<T extends any[], R>(mapping: (input: MapExtended2Input<T>) => R): OperatorFunction<MapExtended2Input<T>, [MapExtended2Input<T>, R]> {
    return (source$: Observable<MapExtended2Input<T>>) => source$.pipe(
        map(input => {
            const mappingResult = mapping(input);
            const result: [MapExtended2Input<T>, R] = [input, mappingResult];
            return result;
        }),
    );
}

const test2 = combineLatest(of(1), of(2)).pipe(
    mapExtended2(([s1, s2]) => s1 + s2),
);

Here, the return type is also Observable<[[number, number], number]>, which is expected, but I do not know how to 'add' a type to the mapped tuple type. Intersecting doesn't seem to work or I'm doing it wrong.

An example of the desired functionality without RxJS would be:

Let's say I need a function myFunc thats has 2 generic type parameters:

  • a tuple type T, with variable number of elements
  • another type R

The result of the function should need to be a tuple type containing all elements of the tuple type T with the parameter of type R appended to it.

e.g.:

myFunc<T, R>(x, y); // Should return [T, R]
myFunc<[T1, T2], R>(x, y); // Should return [T1, T2, R]
myFunc<[T1, T2, T3], R>; // Should return [T1, T2, T3, R]
// ...
jonrsharpe
  • 115,751
  • 26
  • 228
  • 437
Nullius
  • 2,607
  • 2
  • 16
  • 22
  • What is rxjs, what do the `of` and `pipe` functions do? And if the question is about a specific library, why isn't it tagged/named appropriately? – m93a Apr 13 '19 at 15:30
  • 1
    @m93a: The usage of rxjs is irrelevant to the question, but I've added the tag to be complete, thanks. For your information, rxjs is quite a popular library: https://github.com/ReactiveX/rxjs – Nullius Apr 13 '19 at 15:44
  • 1
    The code is just too complex. Try to replace the unnecessary types like `OperatorFunction` and `Observable` with something different, so that we can replicate that in pure TypeScript. – m93a Apr 13 '19 at 16:03

1 Answers1

5

I think what you are asking for is: given a tuple type L (for "list") and another type T, produce a new tuple type with T appended onto the end of L. Sort of the resulting type if you were to call l.push(t). You can do that like this:

type Push<L extends any[], T> =
  ((r: any, ...x: L) => void) extends ((...x: infer L2) => void) ?
  { [K in keyof L2]-?: K extends keyof L ? L[K] : T } : never

This is a bit of trickery using rest tuples, conditional type inference, and mapped tuples...

It's easy enough to produce the result of prepending a type onto the beginning of a tuple, since TypeScript introduced a type-level correspondence between function parameters and tuple types, and the ability to spread tuple types into rest parameters. If L is a tuple like [string, number]. then the function type (...args: L)=>void represents a function taking two parameters of types string and number respectively. Rest parameters can have stuff before them: (first: boolean, ...args: L)=>void is a function taking three parameters. And, the conditional type ((first: boolean, ...args: L) => void) extends ((...args: infer L2)=>void) ? L2 : never produces a new tuple L2 where boolean is prepended to the contents of L... such as [boolean, string, number].

But you don't want prepending, you want appending. Well, by prepending any element to your list tuple L, we get a tuple L2 of the right length but with the wrong types. Ah, but we can map tuples! Let's map over the numeric keys K of L2, using the conditional type K extends keyof L ? L[K] : T. That is, if the numeric key K is in L ("0" or "1" if L is length 2), then just use the corresponding type from L. If not, it must be the one at the very end ("2" if L is length 2), so use the new type T there. This should have the effect of appending T to the end of L.

Let's make sure it works:

type P = Push<[1, 2, 3], 4>; // type P = [1, 2, 3, 4]

Looks good. Now you can declare your mapExtended() like this:

declare function mapExtended<T extends any[], R>(
  mapping: (input: T) => R
): OperatorFunction<T, Push<T, R>>;

Note that the implementation of mapExtended() might not type check, because the compiler can't easily verify that something is of type OperatorFunction<T, Push<T, R>> for generic T and R. So you will probably want to use a type assertion or a single overload.

I can't verify the behavior of this because I don't have rxjs installed. If you need rxjs-specific help I'm sure someone more knowledgeable about it will come along. Anyway, hope that helps. Good luck!

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • Nice! I tried using rest tuples but couldn't come up with the correct solution. Indeed, the implementation does not type check. When trying to figure it out, I also didn't think about on how to distinguish between a tuple type of multiple source Observables or a single Observable of an Array type. Although the intent of the desired operator function was clear, I might need to rethink the implementation because of the technical limitations. Nevertheless, your solution is what I was after and will certainly help with the final implementation, thanks! – Nullius Apr 14 '19 at 08:14