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.

5 min read

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:

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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:

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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.

typescripttypescript
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:

typescripttypescript
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

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.
RunePowered by Rune AI

Frequently Asked Questions

Can optional tuple elements appear in the middle?

No. Optional elements must come after all required elements. A required element cannot follow an optional one. This rule keeps the tuple structure unambiguous at each index.

What happens when I destructure an optional tuple element?

The destructured variable includes | undefined in its type. You must check for undefined before using it as if the element was always present.

How does an optional element affect the length type?

The length becomes a union of literal types. For [number, number, number?], length is typed as 2 | 3 instead of a single literal.

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.