简体   繁体   中英

Generic type wrapping in TypeScript for tuples

I need to add a type declaration to a function which maps elements of a tuple [Foo<A>, Foo<B>, ...] to a function () => [A, B, ...] . How can I achieve this in TypeScript?

This example is structurally similar to the relevant part of the application:

interface SomethingWithAValue<T> { value: T; }

function collect(array) {
  return () => array.map(a => a.value);
}

This returns a tuple of the values associated with each object. What would the type declaration for collect look like?

Pseudocode:

function collect<[SomethingWithAValue<T>...](array: [SomethingWithAValue<T>...]): () => [T...];

Update in response to jonrsharpe's suggestion:

interface SomethingWithAValue<T> { value: T; }

function collect<T>(array: SomethingWithAValue<T>[]): () => T[] {
  return () => array.map(a => a.value);
}

type myTupleType = [string, number];

let somethings: [SomethingWithAValue<string>, SomethingWithAValue<number>];
somethings = [{ value: 'foo' }, { value: 5 }];

let fn: () => myTupleType = collect(somethings);

This does not work:

Argument of type '[SomethingWithAValue<string>, SomethingWithAValue<number>]' is not assignable to parameter of type 'SomethingWithAValue<string>[]'.
  Types of property 'pop' are incompatible.
    Type '() => SomethingWithAValue<string> | SomethingWithAValue<number>' is not assignable to type '() => SomethingWithAValue<string>'.
      Type 'SomethingWithAValue<string> | SomethingWithAValue<number>' is not assignable to type 'SomethingWithAValue<string>'.
        Type 'SomethingWithAValue<number>' is not assignable to type 'SomethingWithAValue<string>'.
          Type 'number' is not assignable to type 'string'.

UPDATE: The answer below is obsolete as of TS3.1. I believe you can now use mapped tuple types and conditional type inference to get the behavior you want:

type ExtractValue<T extends ReadonlyArray<SomethingWithAValue<any>>> =
  { [K in keyof T]: T[K] extends SomethingWithAValue<infer V> ? V : never };

function collect<T extends ReadonlyArray<SomethingWithAValue<any>>>(
  array: T
): () => ExtractValue<T> {
  return () => array.map(a => a.value) as any;
}

And let's use it... First let's make it easy to get a tuple type with a helper function tuple() which takes in a variadic number of arguments and outputs a tuple (this was made possible as of TS3.0)

type Narrowable = string | number | boolean | undefined | null | void | {};
const tuple = <T extends Narrowable[]>(...t: T) => t;

And let's see if it works:

const result = collect(tuple({ value: 10 }, { value: "hey" }, { value: true }));
// const result: () => [number, string, boolean]

Looks good!


OLD ANSWER:

There are no variadic kinds in TypeScript, so there's no way to type a generic tuple (nothing like the syntax [T...] exists).

As a workaround, you can provide function overloads for tuples of any length up to some reasonable maximum:

function collect<A, B, C, D, E>(array: [SomethingWithAValue<A>, SomethingWithAValue<B>, SomethingWithAValue<C>, SomethingWithAValue<D>, SomethingWithAValue<E>]): () => [A, B, C, D, E];
function collect<A, B, C, D>(array: [SomethingWithAValue<A>, SomethingWithAValue<B>, SomethingWithAValue<C>, SomethingWithAValue<D>]): () => [A, B, C, D];
function collect<A, B, C>(array: [SomethingWithAValue<A>, SomethingWithAValue<B>, SomethingWithAValue<C>]): () => [A, B, C];
function collect<A, B>(array: [SomethingWithAValue<A>, SomethingWithAValue<B>]): () => [A, B];
function collect<A>(array: [SomethingWithAValue<A>]): () => [A];
function collect<T>(array: SomethingWithAValue<T>[]): () => T[] {
  // implementation
}

That should work for tuples up to length 5, and you can add other overloads at the top to get up to whatever you need in practice. It's verbose and ugly but it should work.

Hope that helps; good luck!

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