What to Expect From TypeScript 5.0

What to Expect From TypeScript 5.0

Authored by Nazar Hussain

Until now, the most widely used Typescript version has been 4.9.5 , but that's changed with newer, recently releasedTypescript 5.0. With TypeScript 5.0 comes many new features, along with the stated intent to make TypeScript smaller, simpler, and faster.

The details of what we can expect are listed in the following issue. Below we'll go through some of the major features and changes to familiarize ourselves with these updates.

Some highlights:

  1. Decorators

  2. const Type Parameters

  3. Supporting multiple extended configurations

  4. All enums as Union enums

  5. Support for export type *

  6. New configuration flags
    --allowImportingTsExtensions --resolvePackageJsonExports --resolvePackageJsonImports --moduleResolution bundler --customConditions --verbatimModuleSyntax

  7. Passing Emit-Specific Flags Under --build --declaration --emitDeclarationOnly --declarationMap --soureMap --inlineSourceMap

For each, let's drill into the details.

Decorators

Yes, it's not Python or Java annotation, it's TypeScript, and yes, we are getting decorators' support. We can define decorators from multiple perspectives.

  1. Decorators allow users to add new functionality to an existing object without modifying its structure.

  2. Decorators allow modifying the functionality of your functions without changing the functions.

  3. Decorators allow using declarative programming patterns within functional programs.

There could be many more ways to define decorators, but eventually, this feature will enrich how we define functions, extend those, or compose functions.

Consider the following example.

class Foo {
    blah() {
        return "blah blah";
    }
}

Say we want to log the output of the function blah whenever it is invoked, without knowing details of it or even without changing any code line inside the function. Here come the decorators.

function Log<T extends (...args: any[]) => any, T2>(
    target: T2, 
    propertyKey: string | symbol, 
    descriptor: TypedPropertyDescriptor<T>) : TypedPropertyDescriptor<T> {
    
    const value: T = function (...args: any[]): any {
        if(!descriptor.value) return;
        console.log(`calling "${propertyKey.toString()}" with args "${args}"`);
        const ret = descriptor.value(...args);
        console.log(`returned "${ret}"`);
        return ret;
    } as T;
    
    return {...descriptor, value };
}

That's a decorator, but basically, a function that is extending a method. We can use it in the following way.

class Foo {
    @Log
    blah(d: string) {
        return "blah blah " + d;
    }
    @Log
    sum(a: number, b: number) {
        return a + b;
    }
}

You may be concerned about the types here, and yes, the types of the function are not modified. In the future, there may be such a feature available.

const Type Parameters

There is a feature in TypeScript called Type inferring. Most of us have used it often without thinking about its details. Consider the following example.

TypeScript tries to infer the type of the obj.type from its parameter value, and you are right, it guessed it a string.

function getObjectType<T extends {readonly type: string}>(obj: T): T["type"] {
  return obj.type;
}
const objType = getObjectType({type: "MyObject"}); // string

But if we wanted it to be a literal type, we have to specify the value as const

function getObjectType<T extends {readonly type: string}>(obj: T): T["type"] {
  return obj.type;
}
const objType = getObjectType({type: "MyObject"} as const); // MyObject

It's redundant to use as const , and we may forget it a lot. So the new TypeScript allows you to specify parameter type as const by adding const before T

function getObjectType<const T extends {readonly type: string}>(obj: T): T["type"] {
  return obj.type;
}
const objType = getObjectType({type: "MyObject"}); // MyObject

Supporting multiple extend configurations

For any TypeScript project, the tsconfig.json is the entry point. It's where we start configuring our projects. And we usually don't have one tsconfig.json but instead multiple e.g. tsconfig.build.json or tsconfig.test.json. We usually don't configure in every case but rather extends from existing ones. e.g.

{
  "extends": "./tsconfig.build.json",
  "compilerOptions": {
    "emitDeclarationOnly": false,
    "incremental": false,
    "noEmit": true
  }
}

Earlier, TypeScript allowed only to extend from one configuration. Now it allows configuring from multiple configuration files.

{ "extends": ["basic", "extended"], "compilerOptions": { } }

It's the same approach used in the configuration of another famous project eslint. And it uses the same precedence approach as ESLint.

All enums as Union enums

In TypeScript, enums have quite a history. In earlier versions, these were just numerical const, in later ones, we could also assign string values to enums. TypeScript formally had been treating enums differently based on the value assigned.

Consider the following example.

enum E {
    Foo = 10,
    Bar = 20,
    Baz = "value"
}
const obj1: E = "value";  // Raise error
const obj2: E = 30;       // WIll pass fine

Will both values raise an error as we are not using correct enum-relevant values? No, TypeScript will only raise errors on the first because the string values define E.Baz as a literal value, so you can only assign E.Baz to the value. For the numeric values, E.Foo and E.Bar it defines those as number and E was actually a union number | E.Baz

In TypeScript5, all enums will be considered unions of constant type and literal type values. So in Typescript 5, the above enum will be E.Foo | E.Bar | E.Baz where E.Foo and E.Bar will be literal types. If you compile the above code in TypeScript 5, it will raise an error on both lines.

Support for export type *

Earlier syntax export * as namespace from "./module" only supported the values, but on the other hand, the import type was supported, which was contradicting.

So now we can merge the types into namespaces and re-export those as export type * as namespace from "./module"

New configuration flags

There are quite a few new flags added to TypeScript 5. Most of those are self-explanatory by name. We will go here for a few very important and useful.

The --customConditions is linked to the exports and imports attributes of the package.json. You can also set it in the configuration for compilerOptions.customConditions: []. It allows you to enable custom conditions for exports/imports. Say we have the following package.json, which enlists an export with a specific name, say that is a custom device we want to build our project for.

{ 
    // ... 
    "exports": { 
        ".": { 
            "my-device": "./my-device-module.mjs", 
            "node": "./my-module.mjs",
        } 
    } 
}

While compiling for that custom device, we can use --customConditions my-device, and TypeScriptwill import my-device-module.mjs instead of my-module.mjs.

The --importsNotUsedAsValues and --preserveValueImports is deprecated in favor of --verbatimModuleSyntax. The new approach allows easy, consistent, and faster decisions for filtering the emitting code. Not that much use if you are already using ECMAScript code, but if you are writing a mix of CommonJS and ECMAScript, then you could try that flag and notice the difference in the output.

Passing Emit-Specific Flags Under

The following new flags are available to be used along with tsc --build. Earlier, these were only available in the configuration. This will help to modify the build with specific needs without changing the configuration.

  • --declaration

  • --soureMap

  • --inlineSourceMap

  • --declarationMap

  • --emitDeclarationOnly

Summary

With this update, a lot changed internally, and massive refactoring has been done in TypeScript 5.0.

The result is that it's possible to reduce around 26 MB of its package size - bringing it to 39.2 MB from 66.8 MB. The refactoring also enables achieving a higher build time.

Courtesy: https://devblogs.microsoft.com/

If you want to explore more details of each change, check out the following blog post or GitHub issue.

About ChainSafe

ChainSafe is a leading blockchain research and development firm specializing in infrastructure solutions for web3. Alongside its contributions to major ecosystems such as Ethereum, Polkadot, Filecoin, Mina, and more, ChainSafe creates solutions for developers and teams across the web3 space utilizing expertise in gaming, bridging, NFTs, and decentralized storage. As part of its mission to build innovative products for users and improved tooling for developers, ChainSafe embodies an open source and community-oriented ethos to advance the future of the internet.

Website | Twitter | Linkedin | GitHub | Discord | YouTube | Newsletter