Editor’s note: This guide to tuple use cases in TypeScript was last updated by Ukeje Goodness on 14 June 2023 to reflect recent changes to TypeScript. This update also includes new sections on the benefits of using tuples and best practices. To learn more about arrays in TypeScript, check out our guide to understanding flatMap()
.
Tuples extend the capabilities of the array data type. With tuples, we can easily construct special kinds of arrays, where elements are of fixed types with respect to an index or position. Due to the nature of TypeScript, these element types are known at the point of initialization. With tuples, we can define the data type that can be stored in every position in an array.
In this tutorial, we will cover real-world use cases and applications for named tuples in TypeScript. We will learn the importance of this data type and why it is preferred in certain cases. At the end of the day, we will see how this data type contributes to the improvement of the TypeScript language in terms of allowing stricter rules as per improved documentation, maintainable code, and developer productivity.
Before we get started, readers should be familiar with the basics of TypeScript and types in general. To learn more about this topic, check this section of TypeScript’s documentation. Now, let’s get started.
Jump ahead:
Tuples are like advanced arrays with extra features that ensure type safety, particularly when we need to account for a list containing a fixed number of elements with multiple known types.
The major difference between arrays and tuples is that when we assign values to a tuple, these values must match the types defined in the tuple declaration in the same order. On the other hand, arrays can support multiple types with the any
type or the bitwise OR (|
) operator, but the order or structure of the elements doesn’t come into play.
Named tuples provide a structured approach for defining data with named properties. Named tuples combine the benefits of arrays and objects to clearly and concisely represent data points. Additionally, named tuples enhance code readability and make your intentions explicit by assigning names to the properties.
To define a named tuple in TypeScript, you’ll use a combination of square brackets and type annotations to specify the names and types of the properties. Here’s how you can define named types in TypeScript:
type MyNamedTuple = [name: string, age: number, isAdmin: boolean];
You’ve defined a named tuple MyNamedTuple
with three properties: name
of type string
, age
of type number
, and isAdmin
of type boolean
. The order of the properties in the type definition determines the order of elements in the tuple on instantiation.
Once you have defined a named tuple type, you can declare and initialize variables of that type by assigning values to the properties like this:
const person: MyNamedTuple = ['John Doe', 30, false];
You declared a variable person
of the MyNamedTuple
type and assigned values to it. The order of values corresponds to the order of properties defined in the named tuple type.
There are numerous benefits of using tuples in your TypeScript programs. First, tuples are fixed-length sequences that allow you to define an ordered collection of elements. Tuples are handy when you need to represent a sequence of values such as coordinates (x
, y
) or RGB color values (red
, green
, blue
). The fixed length helps ensure you have the right number of elements in the tuples.
Additionally, you can easily destructure tuples to extract individual elements, allowing you to conveniently assign each element to a separate variable with a single line of code. Destructuring tuples can improve readability, especially when working with functions that return multiple values.
Also, tuples share some similarities with arrays; you can perform array-like operations on them. You can access individual elements by their index, iterate over them with loops and use methods like map
, filter
, and reduce
. However, unlike arrays, tuples have a fixed length, which ensures that the structure of the tuple remains intact. Here’s an example:
// Declare a tuple type type MyTuple = [number, string, boolean]; // Create a tuple const myTuple: MyTuple = [10, "Hello", true]; // Iterate over tuple elements with a loop for (const element of myTuple) { console.log(element); } // Use methods like map, filter, and reduce const mappedTuple: MyTuple = myTuple.map((element) => element * 2); console.log(mappedTuple); // Output: [20, "HelloHello", NaN] const filteredTuple: MyTuple = myTuple.filter((element) => typeof element === "string"); console.log(filteredTuple); // Output: [NaN, "Hello", NaN] const reducedValue: number = myTuple.reduce((acc, curr) => acc + (typeof curr === "number" ? curr : 0), 0); console.log(reducedValue); // Output: 10
Here’s the result of running popular array operations on the tuples:
Tuples are preferred over arrays due to the advantages and features of tuples. Tuples enforce fixed lengths, provide type safety, and allow heterogeneous data. TypeScript supports structural pattern matching on tuples and enables concise function signatures.
Destructuring assignments, read-only properties, and memory efficiency are additional benefits. Type inference and named tuple elements make tuples powerful for structured data.
Before we begin our journey into exploring use cases for tuples in TypeScript, let’s briefly explore some simple cases where arrays can be used and how tuples can fit in perfectly well — and even better — in the same scenario.
In TypeScript, we can declare an array of a particular data type. For example, we can declare an array of numbers by specifying the type of that element followed by square brackets: []
. Let’s see how to do so:
let arr: number[]; arr = [1, 2, 3];
As we can see from the example above, to ensure type safety (which allows for easier annotation and documentation of our code), we need to use arrays, which allow for cases like this where we have lists of a particular data type. This, in fact, is the essence of a typed language like TypeScript.
For arrays with multiple data types, we can use the any
type or the |
(bitwise OR) operator. However, in this case, the order of the data is not set in stone. Let’s see an example below:
let arr: (string | number)[]; arr = ['Alex', 2020]; console.log(arr);
From the example above, we can decide to pass the number before the string, and it still works. The order in which we pass the data when the array is instantiated does not matter in this case, as we have a combination of the types specified. This is exactly what tuples aim to solve.
With tuples, we can have a list of multiple data types whereby the order in which we pass the data type must conform to the order when the tuple was declared. In essence, the structure of the tuple needs to stay the same. Let’s see an example to understand this concept better:
let tup: [string, number]; tup = ['Alex', 19087]
In the example above, we can see that we have declared a tuple with two basic data types: string
and number
. Note that when we call the tup
variable, we must also pass the element types in the order they are declared. Essentially, we can’t have a number at index 0
and a string at index 1
, like so:
tup = [19087, 'Alex]
If we had done so, we would get the error shown below:
TSError: ⨯ Unable to compile TypeScript: index.ts:6:8 - error TS2322: Type 'number' is not assignable to type 'string'. 6 tup = [19087, 'Alex'] ~~~~~ index.ts:6:15 - error TS2322: Type 'string' is not assignable to type 'number'. 6 tup = [19087, 'Alex']
As we can see from the earlier examples above, we are declaring a number array and initializing it with values. This works as long as we are dealing only with element types that are numbers. To account for arrays with multiple data types, we can use the any
type or |
operator, although in this case, the order or structure of the data is not guaranteed, which might not be what we want.
With tuples, however, we can ensure strictness with respect to the data types and the order of the data we intend to pass. Tuples allow for specifying known type boundaries around element types with a fixed number of elements.
Since tuples allow us to define both fixed types and order in an array, they are best when working with data that are related to each other in a sequential way (where order is important). That way, we can easily access the elements in a predetermined manner, making our desired responses predictable in behavior.
Below, we will be exploring some more use cases of tuple types in TypeScript based on releases up to the v4.2 release, which will generally revolve around extracting and spreading parameter lists in function signatures.
The REST parameter syntax collects parameters into a single array variable and then expands them. With the recent TypeScript release, we can now expand REST parameters with the tuple type into discrete parameters. What this means is that when a tuple
type is used as a REST parameter, it gets flattened into the rest of the parameter list.
In simple terms, when a REST parameter is a tuple type, the tuple type can be expanded into a sequence of parameter lists.
Consider the example below:
declare function example(...args: [string, number]): void;
The REST parameter expands the elements of the tuple type into discrete parameters. When the function is called, args
, which is represented as a REST parameter, is expanded to look exactly like the function signature below:
declare function example(args0: string, args1: number): void;
Therefore, the REST parameter syntax collects an argument overflow into either an array or a tuple. In summary, a tuple type forces us to pass the appropriate types to the respective function signatures. TypeScript v4.2 added the ability to spread on leading or middle elements. This is handy since you can use REST parameters to create variadic functions on leading or middle parameters as thus:
type Matches = [string, boolean]; const arsenal: Matches = ['Man City', true]; const city: Matches = ['Man United', true]; const hotspur: Matches = ['Liverpool', true]; function processMatches(...matches: [...Matches[], string]): void { const lastMatch = matches.pop(); console.log('Previous matches:'); for (const match of matches) { console.log(match[0]); } console.log('Last match:', lastMatch); } processMatches(arsenal, city, hotspur, 'Chelsea vs. Arsenal');
The processMatches
function accepts a variadic parameter with the ...
spread syntax. The parameter is of type [...Matches[], string]
, meaning it expects two or more tuples of type Matches
followed by a string. Here’s a visual:
The spread syntax expands the elements of an array or object into its element. The spread operator can also expand the elements of a tuple. When a function call includes a spread expression of a tuple type as an argument, the spread expression is expanded as a sequence of arguments corresponding to the element of the tuple type. Let’s see an example below:
type Value = [number, number]; const sample = (...value: Value) => { // do something with value here }; // create a type let sampleTuple: Value; sampleTuple = [20, 40]; // Passing the values as literals: sample(20, 40); // Passing indexes to the corresponding sampleTuple tuple sample(sampleTuple[0], sampleTuple[1]); // Using the spread operator to pass the full sampleTuple tuple sample(...sampleTuple);
N.B., as we can see from the above example, we have declared a tuple type and passed it as a parameter to the function signature.
When the function is called, we can either pass the arguments as literals or via their respective indices. However, using the spread operator is a fast and clean option for passing a tuple as an argument to a function call. Due to the nature of spread operators, the parameters are expanded as a list of arguments corresponding to the elements of the tuple type.
Because tuples are arrays under the hood, we can destructure them just like we would an array. It is important to note that the destructuring variables get the types of the corresponding tuple elements. Let‘s look at an example:
let tuple: [number, string, boolean]; tuple = [7, "hello", true]; let [a, b, c] = tuple; // a: number, b: string, c: boolean
While tuples have their advantages, it’s essential to consider the trade-offs before using them. Tuples are less flexible than arrays and objects, and modifying or extending tuples can be cumbersome. You might find arrays or objects more suitable if your data structure requires frequent modifications or additional properties.
Creating well-defined and reusable tuple types is crucial for maintaining clarity and reducing code duplication. Let’s discuss some tips to consider when defining and using tuple types in TypeScript. First, make sure that you assign meaningful names to the elements within your tuples to enhance readability and help others understand the purpose of each value. For example, instead of using [x, y]
for coordinates, consider [latitude, longitude]
.
Also, TypeScript’s type inference system can automatically infer tuple types based on their assigned values. Instead of explicitly defining types, you should rely on type inference to reduce redundancy and improve code maintainability. If some elements within a tuple are optional, use union types to indicate possible absence. The flexibility ensures your tuple types accommodate multiple scenarios.
When tuples are complex or reused across multiple parts of your codebase, consider abstracting them into interfaces or type aliases to reusability, improve code readability, and allow for more accessible modifications and extensions in the future. By following these tips, you can create meaningful and reusable tuple types that enhance the clarity and maintainability of your TypeScript programs.
There are common pitfalls that developers should be aware of to avoid potential issues. In this section, we’ll cover some common mistakes to avoid when working with tuples. Tuples are immutable by default. Attempting to modify the values of a tuple will result in a compilation error. Avoid changing tuple elements directly; create new tuples with the desired modifications.
Keep in mind that tuples rely on the order of their elements to maintain their structure. Accidentally reordering elements can introduce bugs that are difficult to spot. To prevent this, use clear and descriptive variable names and use destructuring or named tuple elements to access values by name instead of relying solely on their order.
Finally, overusing tuples can make your code harder to understand and maintain. Consider using objects or arrays if a data structure requires frequent modifications. Avoiding these mistakes will help you effectively harness the power of TypeScript tuples and reduce potential code bugs.
TypeScript tuples are like arrays with a fixed number of elements. They provide us with a fixed-size container that can store values of multiple types, where order and structure are very important. This data type is best used when we know exactly how many types we want to allow in an array. As we know, assigning an index outside of the original defined length will result in an error by the TypeScript compiler.
Note that while it is possible to modify the values of tuple elements via their indices, we must ensure to match the types provided when the tuple variable was declared. This is because we can’t alter the type or even the size of elements in the tuple once declared.
With the features we have highlighted in this post, it becomes possible to design strongly typed higher-order functions that can transform functions and their parameter lists, and in essence, ensure a robust, well-documented, and maintainable codebase, which is at the very heart of why we use TypeScript.
LogRocket is a frontend application monitoring solution that lets you replay problems as if they happened in your own browser. Instead of guessing why errors happen or asking users for screenshots and log dumps, LogRocket lets you replay the session to quickly understand what went wrong. It works perfectly with any app, regardless of framework, and has plugins to log additional context from Redux, Vuex, and @ngrx/store.
In addition to logging Redux actions and state, LogRocket records console logs, JavaScript errors, stacktraces, network requests/responses with headers + bodies, browser metadata, and custom logs. It also instruments the DOM to record the HTML and CSS on the page, recreating pixel-perfect videos of even the most complex single-page and mobile apps.
Would you be interested in joining LogRocket's developer community?
Join LogRocket’s Content Advisory Board. You’ll help inform the type of content we create and get access to exclusive meetups, social accreditation, and swag.
Sign up nowLearn how to manage memory leaks in Rust, avoid unsafe behavior, and use tools like weak references to ensure efficient programs.
Bypass anti-bot measures in Node.js with curl-impersonate. Learn how it mimics browsers to overcome bot detection for web scraping.
Handle frontend data discrepancies with eventual consistency using WebSockets, Docker Compose, and practical code examples.
Efficient initializing is crucial to smooth-running websites. One way to optimize that process is through lazy initialization in Rust 1.80.