TypeScript 4.0: Variadic Tuple Types and Modern Pattern

Felipe Hlibco

TypeScript 4.0 beta dropped about five weeks ago, and I’ve been running it against a couple of our internal libraries at work. The headline feature—variadic tuple types—sounds academic until you try to type a function like concat or curry and realize the type system couldn’t express it before.

The stable release should land later this month. Here’s what’s worth paying attention to.

Variadic Tuple Types #

Before 4.0, tuple types were fixed-length. You could write [string, number], but you couldn’t write a generic type that spread one tuple into another. If you wanted to type a function that concatenated two arrays while preserving element types, you were stuck writing overloads. Lots of overloads.

// Before 4.0: manual overloads for concat
function concat<A, B>(a: [A], b: [B]): [A, B];
function concat<A, B, C>(a: [A], b: [B, C]): [A, B, C];
function concat<A, B, C, D>(a: [A, B], b: [C, D]): [A, B, C, D];
// ... and so on, forever

With variadic tuple types, you can spread generic type parameters:

// 4.0: one definition, works for any length
function concat<T extends unknown[], U extends unknown[]>(
  a: [...T], b: [...U]
): [...T, ...U] {
  return [...a, ...b];
}

const result = concat([1, "hello"], [true, 42]);
// type: [number, string, boolean, number]

The ...T spread in the return type is the new thing. It tells TypeScript to concatenate the tuple types. This also works for rest parameters, which means you can finally type higher-order functions that forward arguments without losing type information:

function apply<T extends unknown[], R>(
  fn: (...args: [...T]) => R,
  args: [...T]
): R {
  return fn(...args);
}

I’ve been using this to clean up some wrapper functions in our GraphQL resolver layer where we forward arguments through middleware. The old approach required any casts or overloads; now the types flow through naturally.

Labeled Tuple Elements #

This is a smaller feature but one I appreciate more than I expected. Tuple elements can now have names:

// Before
type Range = [number, number];

// After
type Range = [start: number, end: number];

The labels don’t affect type compatibility—[start: number, end: number] is still assignable to [number, number]. But they show up in IDE tooltips and error messages. When a function takes four positional parameters and your editor just shows [string, number, string, boolean], labels are a significant usability improvement.

One constraint: you can’t mix labeled and unlabeled elements in the same tuple. All or nothing.

Short-Circuiting Assignment Operators #

This one’s straightforward. TypeScript 4.0 adds &&=, ||=, and ??=:

// Before
opts.value = opts.value ?? defaultValue;

// After
opts.value ??= defaultValue;

These follow the TC39 Stage 4 proposal (they’re coming to JavaScript proper), so TypeScript is tracking the spec. ??= is the one I’ll use most—it’s the null-coalescing assignment. ||= and &&= have their uses but the short-circuit semantics mean they’re not always interchangeable with their expanded forms.

// ||= only assigns if left side is falsy
a ||= b;  // a = a || b (but a is only evaluated once)

// &&= only assigns if left side is truthy
a &&= b;  // a = a && b

// ??= only assigns if left side is null/undefined
a ??= b;  // a = a ?? b

The ??= form is particularly useful for initializing optional config objects or lazy properties.

Class Property Inference from Constructors #

TypeScript 4.0 can now infer class property types from constructor assignments when noImplicitAny is enabled:

class Point {
  // No explicit type annotation needed
  x;  // inferred as number from constructor
  y;  // inferred as number from constructor

  constructor(x: number, y: number) {
    this.x = x;
    this.y = y;
  }
}

Previously this would error with noImplicitAny unless you explicitly annotated each property. The inference uses control flow analysis, so it handles conditional assignments and multiple code paths.

I think this matters more for codebases migrating to stricter TypeScript settings than for greenfield code, where you’d probably annotate your properties anyway. But it lowers the friction of turning on noImplicitAny, which is worth something.

Build Speed #

The 4.0 beta includes improvements to --incremental builds. The .tsbuildinfo file now caches semantic diagnostics, meaning subsequent builds skip recomputing diagnostics for unchanged files. The TypeScript team reports 10-25% build time reduction for large projects.

We haven’t benchmarked this carefully against our monorepo yet, but anecdotally, tsc --build feels faster on the beta. I’ll have better numbers once the stable release is out and we can do a proper comparison.

Worth Upgrading? #

If you’re on 3.9, the upgrade path should be smooth—the 4.0 beta hasn’t broken anything in our codebase so far. The variadic tuple types alone justify the bump if you’re writing library code or complex type utilities. For application code, the assignment operators and class property inference are nice quality-of-life improvements.

I’d wait for the stable release before rolling it out to the full team, but running the beta in CI alongside your stable TypeScript version (using ttypescript or similar) is a low-risk way to catch any issues early.