This content originally appeared on Level Up Coding - Medium and was authored by Eamonn Boyle
In celebration of TypeScript 4.2’s recent release and the language’s continued evolution, let’s take a look at tuple types and some advanced type manipulations we can do with them.
Fundamentals
A Tuple (rhymes with ‘couple’, not ‘pupil’) is a simple container of data. A tuple object has a fixed size, and the type of each element is known but the types don’t need to be the same.
A basic tuple definition is:
This movie tuple has exactly 3 elements and they must be string, number and Date in that order. These structures can be easily destructed into their component parts:
The great thing here is that the types of title, rating and releaseDate are correct (string, number and Date). Be careful how you define it though - we can’t make use of type inference as the syntax for a tuple literal is the same as an array literal.
Above, the type of movie is (string | number | Date)[] so the type of each destructured variable is the union string | number | Date. Also, realise that at runtime, after the TypeScript code has been converted to JavaScript, tuples (and typed arrays) are implemented as a normal JavaScript arrays. Like all things related to TypeScript, the safety and constraints for tuples only exist at compile time.
Finally, we can also use rest elements within tuples. These allow us to create variadic tuples. For example:
Note that the type of the rest element must itself be a tuple or array type — in this case number[].
Benefits
We could of course have represented a movie as a class or interface, and we will always have this option. However, tuples are especially useful in these scenarios:
- Returning more than one value from a function
- Destructuring into variables with arbitrary names
- Representing parameters of a function (more on this later)
A good example of the first two points is in React’s useState function. When you call this function, it returns the current value of a piece of state and a function to update that state.
Returning both together in a tuple is useful as you can immediately destructure and use arbitrary names. To fully see the advantage, imagine if the same data was returned within a generic State object. You would need to use more verbose destructuring aliases or use object references.
Function Parameters as Tuples
What about representing parameters as tuples? You’ve probably seen rest parameters, applying ‘...’ to the last parameter of a function to create variadic functions.
At the call site we can use a variable number of arguments. These are gathered into a single array within the sum function.
In TypeScript we can represent the aggregated arguments as an array or a tuple. This allows use to define a function signature in a couple of ways. Consider a normal function taking 3 parameters:
We could rewrite using a rest parameter typed as a tuple:
Or with parameter destructuring:
This is interesting, but is it useful? It is useful because the tuple is a single entity representing multiple parameters. This is especially useful when combined with generics where we don’t specify what the type is until the call is made. Let’s look at a practical example.
Examining Promise.all
The Promise type wraps up asynchronous operations and has a few useful static methods for combining multiple promises.
The Promise.all method takes an array of promises and returns a new promise that resolves with all results when all input promises resolve. We can subsequently use the then method or async await to consume the results. For example:
Or with destructuring:
What’s interesting here is that name, age and startDate have the expected types. The input parameter types, Promise<string>, Promise<number> and Promise<Date>, are correlated back to the type of the result, a tuple of type [string, number, Date]. Let’s look at how this is achieved (I’ve simplified the definition):
As you can see, the definition uses a series of overloads with an increasing number of generic parameters up to a maximum of 10. These generic parameters correlate the input tuple of promises to the output promise of tuple.
I said the code was simplified as Promise.all actually supports taking in promise objects and normal objects. Also, the first overload actually supports any number of inputs of the same type. So the actual definition is:
The downside here is that it requires multiple overload definitions (redundancy) and it tops out at 10 parameters. If I add an 11th argument, I get a compiler error. Let’s see if we can improve it.
Improving Promise.all
We can create a function that will capture the tuple type as a single generic parameter.
We will look at RemapPromises later but for now realise that it will convert a tuple of promises type to a promise of tuple type.
In this function the generic parameter T is constrained to a tuple of any elements. The type is inferred from usage - for example:
We could also define our function using a rest parameter to make it variadic and dropping the ‘[]’ from the call site.
The big win here is that we have a single definition, and we can have more than 10 arguments.
The Remapping Type
One of the complicated bits is the RemapPromises type. Luckily, above, we had it tucked away in an alias so that we didn’t have to think about it until now. The solution for this uses some concepts discussed in one of my earlier posts, Crazy, Powerful TypeScript 4.1 Features.
First, we define a type to take a Promise<T> | T and return T.
This uses a Conditional Type along with type inference to figure out the inner type of an input promise type. If the input type, T, is not a promise type then it is used directly.
Next, we can use this with a Recursive Conditional Type that does this for all elements within a tuple, converting [Promise<T1>, Promise<T2>, ..., Promise<TN>] into [T1, T2, ..., TN].
Here we are using the conditional type and inference again to extract out the Head and Tail of the input tuple. We can then use the previously defined UnwrapPromise to unwrap the Head and recursively call UnwrapPromises (note the ‘s’) to unwrap the tail. These are combined into a resultant tuple.
Finally, we can use this tuple of ‘unwrapped’ promises to create our final promise of tuple.
Why is it not implemented like this?
The standard Promise.all definition may eventually change to something like this but there are probably issues which I’m not considering. I am only demonstrating this as a practical example to illustrate what you can do with tuple types, recursive conditional types etc. You can go off and consider other use cases in your own code.
When making changes to a language’s standard library, definitions that need to support ALL TypeScript code that exists out there, you need to apply a bit more rigour than I’m doing here. Consideration of backward compatibility, corner cases, compiler performance and more would impact any final definitions. I’d love to hear your thoughts in the comments about any issues with this as a general solution.
What does TypeScript 4.2 Add?
TypeScript 4.2 brings one feature to tuple types, and that’s the ability to spread on leading or middle elements. For example, here we have a tuple where the middle elements are a variable length series of numbers:
This could be useful when combined with rest parameters to create variadic function on leading or middle parameters. Consider an assertion function to test if all values are equal and throw an error with a specific message if not.
This function must be called with two or more numbers followed by a string which we can’t do with normal rest parameters.
I’m not sure you would create a function of this shape very often. There is no way to easily destructure the tuple as the rest destructuring must always be the last element. So here we use slicing and indexing along with explicit type assertions. I think having multiple functions, parameters as an object, overloads or trailing rest parameters will more often be the right answer.
This may prove useful for safely typing JavaScript functions that use this shape though.
Conclusion
Small, basic tuples are very useful in their own right but when you delve a bit deeper and combine with generics, recursive conditional types and rest elements you can achieve some interesting results.
Be sure to check out my TypeScript 4.1 post where I discuss recursive conditionals. Also, to see examples of tuples used with useState, have a look at my React Tutorial.
I hope you found this interesting. Be sure to check out our TypeScript course. We’re also happy to deliver React training using TypeScript. We deliver virtually to companies all over the world and are happy to customise our courses to tailor to your team’s level and specific needs.
Originally posted here.
Crazy, Powerful TypeScript Tuple Types was originally published in Level Up Coding on Medium, where people are continuing the conversation by highlighting and responding to this story.
This content originally appeared on Level Up Coding - Medium and was authored by Eamonn Boyle
Eamonn Boyle | Sciencx (2021-03-30T17:51:20+00:00) Crazy, Powerful TypeScript Tuple Types. Retrieved from https://www.scien.cx/2021/03/30/crazy-powerful-typescript-tuple-types/
Please log in to upload a file.
There are no updates yet.
Click the Upload button above to add an update.