问题
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.
EDIT:
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]
// ...
回答1:
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!
来源:https://stackoverflow.com/questions/55667014/extending-mapped-tuple-types