Skip to main content

Command Palette

Search for a command to run...

What's new in TypeScript 5.0?

Updated
•4 min read•View as Markdown
What's new in TypeScript 5.0?
J
I've worked remotely for most of my software engineering career. It opened up a whole new world to me. I was able to build my own office, cut out the commute, avoid distractions, and... wear the same shirt for a week. :) Of course, there are downsides, but to me, remote work makes sense for both companies and employees. I've always aimed to build remote-first teams because I believe it leads to better documentation, smoother processes, and fewer bottlenecks. But there's always been one thing missing: that sense of belonging, of truly feeling like part of a team. So when I got the chance to help BeZoned build the best online office for remote teams, it was a dream come true. Not only did the idea immediately click with me, but I was also getting to scratch my own itch.

On the 1st of March, the team behind TypeScript released TypeScript 5.0 RC. That means the version is pretty stable and we can have a look at some new features, Let's roll!

Decorators

Decorators are part of TypeScript for a long time. However, they are still experimental and non-standard (meaning decorators are not part of the ECMAScript standard) features.

Version 5.0 brings us the implementation of the Stage 3 proposal. At this stage, the standard is almost completed and no further changes should be made.

experimentalDecorators No More

From now on, decorators are a valid part of the code. This means, that you no longer need to set experimentalDecorators to true. The flag stays in, however, new decorators are emitted and type-checked differently. As a result, old decorators won't work properly! In addition, flag emitDecoratorMetadata is not compatible at all.

How do they look?

TypeScript's Decorator type signature looks as follows:

type Decorator = (
  value: DecoratedValue,
  context: {
    kind: string;
    name: string | symbol;
    addInitializer(initializer: () => void): void;

    static: boolean;
    private: boolean;
    access: {get: () => unknown, set: (value: unknown) => void};
  }
) => void | ReplacementValue;

As you can see, a decorator is just a function, that takes two parameters - value and context.

Decorated entity

value contains the decorated entity. That may be class, method, getter/setter, field, or accessor.

Context

context adds additional data about the decorated entity:

  • kind tells us the type of decorated entity. It may contain the following values: class, method, getter, setter, field, and accessor

  • name is the name of the decorated entity (class name, method name, ...)

  • access contains get/set methods for the entity.

  • private if set to true, decorated entity is private

  • static if set to true, decorated entity is static

  • addInitializer allows the decorator to add additional initialization logic.

Not all fields are accessible all the time. The context object changes based on the type of decorated entity:

type Decorator =
  | ClassDecorator
  | ClassMethodDecorator
  | ClassGetterDecorator
  | ClassSetterDecorator
  | ClassAutoAccessorDecorator
  | ClassFieldDecorator

const Type Parameters

When you don't provide a return type, TS has to guess (infer) it. In the previous versions, TypeScript always chose the closest "general" type:

type RequestStatus = {
    code: readonly number
    message: readonly string
}

function getRequestCode<T extends RequestStatus>(status: T): T['code'] {
    return status.code
}

const code = getRequestCode({ code: 404, message: 'Not Found' })
//    ^ inferred type: number ❌

Sometimes it's desirable, to get a more specific (in this case readonly) type. One way to do so is by adding as const when passing the parameter into getRequestCode function:

const code = getRequestCode({ code: 404, message: 'Not Found` } as const )
//    ^ inferred type: 404 ✅

But that shifts responsibility to the user of your code and is easy to forget to do. Instead, with TS 5.0, you can set the parameter type as const:

function getRequestCode<const T extends RequestStatus>(status: T): T['code'] {
    return status.code
}

const code = getRequestCode({ code: 404, message: 'Not Found` })
//    ^ inferred type: 404 ✅

At the same time, const modifier does not require immutable values:

function stillReturnsMutable<const T extends readonly string[]>(param: T): T {
    return param
}

const values = ['Hoy', 'Holla']
//    ^ mutable

const result = stillReturnsMutable(values)
//    ^ inferred type: string[]

bundler Module Resolution

module options nodenext and node16 required all relative imports to include a file extension:

// index.mjs
import * from './otherFile' // ❌ TS is missing file extension

import * from './otherFile.mjs' // ✅ works as expected

Most bundlers (Vite, Parcel, esbuild ...) support a hybrid lookup strategy, which finds both files with and without the extension. That conflicted with the TS way described above.

The new bundler option for module allows TypeScript to do the same as bundlers - finding the file even without the extension.

Further reading

This post was by no means an extensive list of all changes but a brief introduction. If you are interested in the full list of changes, see the following links.

https://devblogs.microsoft.com/typescript/announcing-typescript-5-0-rc

https://github.com/tc39/proposal-decorators