Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

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'.
like image 327
just.me Avatar asked Sep 02 '17 10:09

just.me


People also ask

How do you Destructure a tuple in TypeScript?

let stud3 = Student[2]; In this tuple destructuring we simply manually break the value of tuple and assign it to variable. In destructuring, we basically assign the value of each element of tuple to some variable. This is the basic example of destructuring of a tuple.

What is tuple type in TypeScript?

A tuple is a typed array with a pre-defined length and types for each index. Tuples are great because they allow each element in the array to be a known type of value.

Are tuples immutable in TypeScript?

Tuples are mutable which means you can update or change the values of tuple elements.

Do tuples exist in TypeScript?

Typically an array contains zero to many objects of a single type. TypeScript has special analysis around arrays which contain multiple types, and where the order in which they are indexed is important. These are called tuples. Think of them as a way to connect some data, but with less syntax than keyed objects.


1 Answers

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!

like image 123
jcalz Avatar answered Oct 23 '22 08:10

jcalz