Optional Tuple Elements in TypeScript
Optional tuple elements let you define tuples where some positions can be omitted. Learn the ? syntax, how optional affects length, and why optional elements must come last.
A TypeScript optional tuple element is a position in a tuple that does not have to be provided. You mark it with a question mark after the type, just like optional object properties.
The tuple still has a maximum length, but the trailing optional positions can be left out when you create the value. This is useful when a function can accept a variable number of positional arguments, or when a data format has optional trailing fields that are not always present.
Optional tuple elements let you express these patterns without writing multiple overload signatures.
The optional syntax
Put a question mark after the type at the position you want to make optional. The optional element must be the last element in the tuple. Here is a coordinate type where the third dimension is optional:
type Coordinate = [number, number, number?];This tuple always has at least two numbers, and the third number can be provided or omitted entirely. The compiler enforces both the minimum and maximum lengths.
let a: Coordinate = [10, 20];
let b: Coordinate = [10, 20, 30];
let c: Coordinate = [10];
let d: Coordinate = [10, 20, 30, 40];The first two assignments succeed because they provide either two or three numbers. The third fails because at least two numbers are required, and the fourth fails because the maximum is three elements.
You get the flexibility of a variable-length format with the safety of a fixed structure.
How optional affects length
When a tuple has an optional element, the length property is typed as a union of the possible lengths. This means TypeScript knows the length can be one of several specific values, not just any number.
type Coordinate = [number, number, number?];
function describe(coord: Coordinate) {
console.log(`Dimensions: ${coord.length}`);
}Inside the function, the length is typed as the union 2 or 3. This lets you check the length and TypeScript will narrow the tuple type based on the result: if length is 2, the third element is absent, and if length is 3, the third element is present.
This kind of length check is most useful when you need to branch on whether the optional element was supplied before doing anything with it.
Destructuring optional elements
When you destructure an optional tuple element, the variable's type automatically includes undefined. This is the compiler's way of saying the value might not be there.
type Coordinate = [number, number, number?];
function log2dOr3d(coord: Coordinate) {
let [x, y, z] = coord;
if (z !== undefined) {
console.log(`z: ${z.toFixed(2)}`);
}
}The first two variables x and y are plain numbers because they are always present. The third variable z has type number or undefined because the element might be omitted.
You narrow with a check before using number-specific methods like toFixed. Without the check, calling toFixed would be a compiler error because the value could be undefined.
You can also provide a default value during destructuring to avoid the undefined check entirely:
let [x, y, z = 0] = coord;When the third element is absent, z defaults to 0 and its type is plain number. This is the cleanest approach when a sensible default exists.
Optional elements must come last
TypeScript requires all optional elements to appear after all required elements. A required element cannot follow an optional one because the compiler could not determine which index maps to which declared position if an earlier element were skippable.
type Bad = [string?, number];
type Good = [string, number?];The first declaration is a compiler error. The second is valid because the optional element trails all required elements. This rule keeps the tuple structure unambiguous at every index position, since the compiler always knows which positions must be present before it looks at any optional ones.
The same restriction applies to object properties and function parameters: a required value can never come after an optional one in the same position list.
Practical use: function signatures without overloads
Optional tuples can replace function overloads when you want a function to accept a variable number of typed positional arguments. Instead of writing three separate overload signatures, you express the pattern in a single tuple type with optional elements.
function sendMessage(...args: [string, string?, number?]) {
let [content, recipient, priority] = args;
console.log(`Message: "${content}"`);
if (recipient) {
console.log(`To: ${recipient}`);
}
}The content parameter is always required because it has no question mark. The recipient is optional and is checked with a truthy guard inside the function body.
function sendUrgentMessage(...args: [string, string?, number?]) {
let [content, _, priority] = args;
if (priority !== undefined) {
console.log(`Priority: ${priority}`);
}
}Both functions accept the same three call patterns from a single definition. The priority parameter is independently optional, accessed from the third tuple position only when present. For even more flexible argument patterns, see variadic tuples in TypeScript.
Optional tuples and readonly
You can combine the readonly modifier with optional elements to prevent mutation while keeping the optional positions flexible. The readonly keyword goes before the entire tuple type.
type Config = readonly [host: string, port?: number];
let localConfig: Config = ["localhost"];
let prodConfig: Config = ["localhost", 3000];Both constructions work because the optional port is handled at creation time, either provided or left out. Once created, the readonly modifier blocks every write operation on the tuple:
let config: Config = ["localhost", 3000];
config[0] = "other";
config.push(8080);Both lines are compiler errors. The optional element behavior is unaffected by readonly: it only controls whether a position can be omitted, while readonly controls whether the tuple can be changed after creation.
Combining the two is common for configuration values that are built once, may have missing trailing fields, and should never be mutated afterward.
Common mistakes
The most frequent mistake is placing an optional element before a required one. The compiler rejects this outright with a clear message, so reorder the elements so all required positions come first.
This is the same rule covered earlier: optional tuple elements are only valid as trailing positions.
Another common mistake is forgetting to check for undefined after destructuring an optional element. Every optional element includes undefined in its type, and methods that expect a definite value will not compile until you narrow.
Use an if guard or a destructuring default value to handle the missing case cleanly. For more on narrowing patterns, see type narrowing basics in TypeScript.
Rune AI
Key Insights
- Mark a tuple element optional with a ? after its type, like [string, number?].
- Optional elements can only appear at the end, after all required elements.
- The tuple's length type becomes a union, like 2 | 3 for [number, number, number?].
- Destructuring an optional element includes | undefined in the variable's type.
- Optional tuple elements are erased at runtime -- the array is still a plain JavaScript array.
Frequently Asked Questions
Can optional tuple elements appear in the middle?
What happens when I destructure an optional tuple element?
How does an optional element affect the length type?
Conclusion
Optional tuple elements add flexibility to fixed-position data by letting trailing positions be omitted. The compiler adjusts the length type and adds undefined to destructured variables automatically. Use optional elements for function overloads, coordinate systems with an optional third dimension, or any position-based format where the last fields are genuinely optional.
More in this topic
TypeScript Compiler Explained
The TypeScript compiler (tsc) turns .ts files into .js files while checking types. Learn what it does, how to use it, and how it fits your workflow.
Set Up ESLint for TypeScript
Install and configure ESLint with typescript-eslint to catch bugs and enforce consistent code style in your TypeScript project.
Pick Utility Type in TypeScript
Pick creates a new type by selecting only the properties you name from an existing type. Use it to build focused types for specific views, API responses, or function parameters.