Skip to main content
Back to Journal
TypeScriptJavaScript

TypeScript 4.0 and the Features That Actually Matter

TypeScript 4.0 landed in August 2020, and every release brings a mix of features that matter to real-world code and features that only matter to library authors writing advanced generics. I have been using TypeScript since version 2.x, and 4.0 is one of those releases where several features immediately changed how I write everyday code. Here is what actually matters if you are building applications, not type-level puzzles.

Variadic Tuple Types

This is the headline feature, and it deserves that spot. Before 4.0, if you wanted to write a function that concatenated two arrays while preserving the types of each element, you had to write dozens of overloads. One for arrays of length 1 and 1, one for length 1 and 2, one for length 2 and 1, and so on. It was painful.

Variadic tuple types let you express this in a single generic signature:

function concat<T extends unknown[], U extends unknown[]>(
  arr1: [...T],
  arr2: [...U]
): [...T, ...U] {
  return [...arr1, ...arr2];
}

var result = concat([1, 2] as const, ['hello'] as const);
// Type is [1, 2, "hello"]

The spread syntax [...T, ...U] in the return type tells TypeScript to concatenate the tuple types. This works because T and U are constrained to be arrays, and the spread in the type position merges them positionally.

Where this really shines is in higher-order function types. If you are writing a pipe or compose utility, or wrapping functions to add logging or error handling, variadic tuples let you preserve the exact parameter types through the wrapper:

function tail<T extends unknown[], R>(
  fn: (head: string, ...args: T) => R,
  ...args: T
): R {
  return fn('default', ...args);
}

Before 4.0, the type system simply could not express this pattern without losing type information or requiring manual overloads.

Labeled Tuple Elements

This one is small but immediately useful. Before 4.0, tuple types looked like this:

type OldRange = [number, number];

When you hovered over a variable of this type in your editor, you saw [number, number]. Is the first element the start or the end? No idea without checking the documentation. Now you can label them:

type Range = [start: number, end: number];
type HttpResponse = [status: number, body: string, headers: Record<string, string>];

The labels show up in editor tooltips, function signatures, and error messages. They do not affect runtime behavior at all. They are purely for developer experience, and they make a real difference when you are reading unfamiliar code. I have gone back and added labels to tuple types across my codebases because the readability improvement is worth the few minutes it takes.

Class Property Inference from Constructors

TypeScript 4.0 got smarter about inferring class property types from constructor assignments. Before this, if you did not explicitly annotate a class property, it would default to any in certain cases:

class Animal {
  name;  // Before 4.0: implicitly 'any'

  constructor(name: string) {
    this.name = name;
  }
}

// In 4.0+, name is inferred as 'string'

This is especially helpful when you have noImplicitAny enabled (which you should). Previously, you would get errors on properties that were clearly being assigned a typed value in the constructor. Now TypeScript follows the assignment and infers the correct type. Less boilerplate, same type safety.

The inference also works with control flow. If a property is assigned in both branches of an if/else in the constructor, TypeScript unions the types:

class Config {
  value;

  constructor(input: string | number) {
    if (typeof input === 'string') {
      this.value = input.toUpperCase();  // string
    } else {
      this.value = input * 2;  // number
    }
  }
  // value is inferred as string | number
}

Short-Circuiting Assignment Operators

JavaScript (via the TC39 proposal that landed in ES2021) added three new assignment operators, and TypeScript 4.0 included support ahead of the spec finalization:

var a: number | null = null;
var b: string | undefined = undefined;
var c: number = 0;

a ??= 42;    // a is now 42 (nullish coalescing assignment)
b ||= 'hi';  // b is now 'hi' (logical OR assignment)
c &&= 99;    // c is still 0 (logical AND assignment, 0 is falsy)

The ??= operator is the one I use most. It assigns only if the left side is null or undefined. Compare with ||=, which assigns on any falsy value (including 0, empty string, and false). The &&= operator assigns only if the left side is truthy.

Before these operators, the pattern was:

if (a === null || a === undefined) {
  a = 42;
}
// or
a = a ?? 42;

The assignment version is shorter and makes the intent clearer, especially when the variable name is long or the expression is complex.

Unknown on Catch Clause Variables

This is a correctness improvement that I wish had existed from day one. Before 4.0, the variable in a catch clause was typed as any:

try {
  JSON.parse(someString);
} catch (err) {
  // err is 'any', you can do anything with it
  console.log(err.message);  // No error, even if err is not an Error
}

In 4.0, you can annotate catch variables as unknown:

try {
  JSON.parse(someString);
} catch (err: unknown) {
  if (err instanceof Error) {
    console.log(err.message);
  } else {
    console.log('Unknown error:', String(err));
  }
}

With unknown, TypeScript forces you to narrow the type before using it. This is correct because you genuinely do not know what will be thrown. JavaScript allows throwing strings, numbers, objects, anything. Treating catch variables as any was a type-safety hole, and unknown closes it.

I enabled the useUnknownInCatchVariables compiler option (available in 4.4, but you can start the habit now with manual annotations) and fixed about thirty silent type-safety violations across one project. Every single one was a place where I assumed the caught value was an Error instance without checking.

Template Literal Types Preview

While template literal types officially landed in TypeScript 4.1 (released November 2020), the foundation was laid in 4.0 and the preview builds were circulating. The idea: you can use string template syntax at the type level.

type EventName = 'click' | 'scroll' | 'mousemove';
type HandlerName = `on${ Capitalize<EventName> }`;
// "onClick" | "onScroll" | "onMousemove"

This enables typed string manipulation. You can enforce naming conventions at the type level, create mapped types that transform property names, and build string-based APIs with full type safety. If you have ever written an API where method names follow a pattern (like getUser, getPost, getComment), template literal types can express that pattern as a type constraint.

Practical Impact

Every TypeScript release has features that look academic in the release notes but turn out to be genuinely useful. With 4.0, the practical wins are clear. Labeled tuples improve readability with zero cost. Short-circuiting assignment cuts boilerplate. Unknown catch variables close a real type-safety hole. Class property inference reduces noise. And variadic tuples unlock patterns that were simply impossible before without pages of overloads.

If you are still on TypeScript 3.x, the upgrade path is smooth. Run npm install typescript@4, fix any new errors (there are very few breaking changes), and start using these features immediately. The type checker just keeps getting smarter, and 4.0 is a solid step forward for everyday TypeScript development.

typescriptvariadic-tuplestype-systemdeveloper-tools