2

I'm struggling to understand how to use variadic tuple types in Typescript. I've tried to read through the docs, and some issues on the GitHub, but examples always are a bit weirder than "super basic", so i wondered if someone could help me type a couple of "super basic" ones so I can maybe use those to "get it".

Say we have these functions:

function wrap(...items) {
  return items.map(item => ({ value: item }))
}

function wrapArray(items) {
  return items.map(item => ({ value: item }))
}

function unwrap(...items) {
  return items.map(item => item.value)
}

function unwrapArray(items) {
  return items.map(item => item.value)
}

How can I type these so that for example the following would go work type-wise?

const items = [{ value: 4 }, { value: 'foo' }]

const [num, str] = unwrap(...items)
console.log(num.toFixed(2))
console.log(str.charAt(0))

Here's a playground with the functions, and a "test" for each. They're using non-variadic types, which of course doesn't quite work, and I don't understand how to write them so that they do work:
TypeScript Playground

Svish
  • 152,914
  • 173
  • 462
  • 620

1 Answers1

3

Here's how I'd be inclined to type them:

type Wrapped<T extends any[]> = { [I in keyof T]: { value: T[I] } };

function wrap<T extends any[]>(...items: T) {
    return items.map(item => ({ value: item })) as Wrapped<T>;
}

function unwrap<T extends any[]>(...items: Wrapped<T>) {
    return items.map(item => item.value) as T;
}

function wrapArray<T extends any[]>(items: readonly [...T]) {
    return items.map(item => ({ value: item })) as Wrapped<T>
}

function unwrapArray<T extends any[]>(items: readonly [...Wrapped<T>]) {
    return items.map(item => item.value) as T;
}

Remarks:

  • These functions are all generic in T, the tuple of non-wrapped elements. So wrap() and wrapArray() take a value of type T as input, while unwrap() and unwrapArray() return a value of type T as output.

  • Wrapped<T> is a mapped tuple which wraps each element of T. wrap() and wrapArray() return a value of type Wrapped<T> as output, while unwrap() and unwrapArray() take a value of type Wrapped<T> as input.

  • The compiler is not able to verify or understand by itself that items.map(...) will turn a T into a Wrapped<T> or vice-versa. See this question/answer for details about why this is. Instead of trying to make the compiler do this, we simply assert that the return value of map() is of the type we want it to be (i.e., as T or as Wrapped<T>).

  • Nowhere in wrap() or unwrap() are variadic tuple types being used. Such tuple types can be identified by the use of array-or-tuple-typed rest parameters inside a tuple, like [1, ...[2, 3, 4], 5]. The wrapArray() and unwrapArray() functions use variadic tuple types as input (readonly [...T] instead of just T), but this is just to help infer tuples when using them, and is not necessary. So while you are asking about variadic tuple types, the example does not require their use.


Let's try it out:

const items = [{ value: 4 }, { value: 'foo' }] as const; // <-- note well
const [num, str] = unwrap(...items);
// or const [num, str] = unwrapArray(items);
console.log(num.toLocaleString());
console.log(str.charAt(0));

This now works, but please note that I had to use a const assertion to allow the compiler to infer that items is a tuple of exactly two elements, the first is a {value: 4} and the second is a {value: "foo"}. Without that, the compiler uses its standard heuristic of assuming an array is mutable and that its length and the identity of its elements can change... meaning that items is something like Array<{value: string} | {value: number}> and there's no hope of unwrapping it into a tuple.

Playground link to code

jcalz
  • 264,269
  • 27
  • 359
  • 360
  • 1
    I _see_ it working, and there are two things I think that still throw me off quite a bit... The first is the mapped tuple `Wrapped`. What does it do, and why must it be used rather than a simple (non-working) `type Wrapped = { value: T }` in this case? And the second is, why do you do `[...T]` in `wrapArray`, but not in `wrap`? What's the difference between `T` and `[...T]` here? – Svish Jun 29 '21 at 16:02
  • 2
    `Wrapped<[string, number]>` becomes `[{value: string}, {value: number}]` while your version would become `{value: [string, number]}` which isn't what you want. You want to *map* your version across elements of the tuple `T`; hence the mapped tuple. – jcalz Jun 29 '21 at 16:04
  • 2
    If I call `wrapArray([1, 2, 3])`, the typing `args: T` would end up making the compiler infer `T` as just `number[]`, because by default the compiler does not tend to infer tuples over arrays. The typing `args: [...T]` or `args: readonly [...T]` is a hint to the compiler that you want `T` to be something like `[number, number, number]` instead. I don't do it with `wrap()` or `unwrap()` because a rest argument like `...args: T` is already inferred as a tuple (since by default you generally care about order of arguments to a function). – jcalz Jun 29 '21 at 16:05
  • Aaaah. Awesome, thank you! Think it's starting to become clearer now! – Svish Jun 29 '21 at 16:36