What's new in TypeScript 5.0?

What's new in TypeScript 5.0?

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

Did you find this article valuable?

Support Jan Malčák by becoming a sponsor. Any amount is appreciated!