Search code examples
typescriptgenericsvariadic-functions

How to use variadic tuple types in Typescript?


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


Solution

  • 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