简体   繁体   中英

Generic object return type is result of method chaining

I would like to do the following:

var result = loader
    .add<number>(1)
    .add<string>("hello")
    .add<boolean>(true)
    .run();

I would like to construct this theoretical loader object in such a way to have the TYPE of result be [number, string, boolean] without needing to manually declare it as such. Is there a way to do this in TypeScript?

UPDATE: TypeScript 4.0 will feature variadic tuple types , which will allow more flexible built-in tuple manipulation. Push<T, V> will be simply implemented as [...T, V] . Therefore the entire implementation turns into the following relatively straightforward bit of code:

type Loader<T extends any[]> = {
    add<V>(x: V): Loader<[...T, V]>;
    run(): T
}
declare const loader: Loader<[]>;

var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]

Playground link


FOR TS before v4.0:

There is unfortunately no supported way in TypeScript to represent the type operation of appending a type onto the end of a tuple. I'll call this operation Push<T, V> where T is a tuple and V is any value type. There is a way to represent prepending a value onto the beginning of a tuple, which I'll call Cons<V, T> . That's because in TypeScript 3.0, a feature was introduced to treat tuples as the types of function parameters . We can also get Tail<T> , which pulls the first element (the head) off a tuple and returns the rest:

type Cons<H, T extends any[]> = 
  ((h: H, ...t: T) => void) extends ((...r: infer R) => void) ? R : never;
type Tail<T extends any[]> = 
  ((...x: T) => void) extends ((h: infer A, ...t: infer R) => void) ? R : never;

Given Cons and Tail , the natural representation of Push would be this recursive thing that doesn't work :

type BadPush<T extends any[], V> = 
  T['length'] extends 0 ? [V] : Cons<T[0], BadPush<Tail<T>, V>>; // error, circular

The idea there is that Push<[], V> should just be [V] (appending to an empty tuple is easy), and Push<[H, ...T], V> is Cons<H, Push<T, V>> (you hold onto the first element H and just push V onto the tail T ... then prepend H back onto the result).

While possible to trick the compiler into allowing such recursive types, it is not recommended . What I usually do instead is pick some maximum reasonable length of tuple I want to support modifying (say 9 or 10) and then unroll the circular definition:

type Push<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push1<Tail<T>, V>>
type Push1<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push2<Tail<T>, V>>
type Push2<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push3<Tail<T>, V>>
type Push3<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push4<Tail<T>, V>>
type Push4<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push5<Tail<T>, V>>
type Push5<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push6<Tail<T>, V>>
type Push6<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push7<Tail<T>, V>>
type Push7<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push8<Tail<T>, V>>
type Push8<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], Push9<Tail<T>, V>>
type Push9<T extends any[], V> = T['length'] extends 0 ? [V] : Cons<T[0], PushX<Tail<T>, V>>
type PushX<T extends any[], V> = Array<T[number] | V>; // give up

Each line except PushX looks just like the recursive definition, and we intentionally cut things off at PushX by giving up and just forgetting about the order of elements ( PushX<[1,2,3],4> is Array<1 | 2 | 3 | 4> ).

Now we can do this:

type Test = Push<[1, 2, 3, 4, 5, 6, 7, 8], 9> // [1, 2, 3, 4, 5, 6, 7, 8, 9]

Armed with Push , let's give a type to loader (leaving the implementation up to you):

type Loader<T extends any[]> = {
  add<V>(x: V): Loader<Push<T, V>>;
  run(): T
}
declare const loader: Loader<[]>;

And let's try it:

var result = loader.add(1).add("hello").add(true).run(); //[number, string, boolean]

Looks good. Hope that helps; good luck!


UPDATE

The above only works with --strictFunctionTypes enabled. If you must do without that compiler flag, you could use the following definition of Push instead:

type PushTuple = [[0], [0, 0], [0, 0, 0],
    [0, 0, 0, 0], [0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0],
    [0, 0, 0, 0, 0, 0, 0, 0, 0], [0, 0, 0, 0, 0, 0, 0, 0, 0, 0]
];
type Push<
    T extends any[],
    V,
    L = PushTuple[T['length']],
    P = { [K in keyof L]: K extends keyof T ? T[K] : V }
    > = P extends any[] ? P : never;

It's more terse for small supported tuple sizes, which is nice, but the repetition is quadratic in the number of supported tuples (O(n 2 ) growth) instead of linear (O(n) growth), which is less nice. Anyway it works by using mapped tuples which were introduced in TS3.1.

It's up to you.

Good luck again!

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM