Exploring šŸšµ the ExcitingšŸ’ƒ Features and Enhancements šŸ¦¾ in TypeScript 5.2 Beta Version: A Comprehensive Overview šŸ“

Israel
7 min readJul 2, 2023

Introduction:

TypeScript, the popular programming language known for its static typing and advanced tooling, continues to evolve with the release of its eagerly anticipated 5.2 Beta Version. Building upon the strengths of its predecessors, TypeScript 5.2 brings forth a range of exciting features and enhancements that elevate the development experience to new heights. Whether youā€™re a seasoned TypeScript enthusiast or just getting started, understanding the latest advancements in this beta release is key to staying at the forefront of modern web development.

To get started using the beta, you can get it through NuGet, or through npm with the following command:

Table of Content

  • Using Declarations and Explicit Resource Management.
  • Decorator Metadata
  • Named and Anonymous Tuple Elements.
  • Easier Method Usage for Unions of Arrays.
  • Consistent Export Checking for Merged Symbols

Prerequisites

Knowledge of TypeScript and JavaScript basics with nodejs installed on your machine.

Using Declarations and Explicit Resource Management

Itā€™s common to need to do some sort of "clean-up" after creating an object. For example, you might need to close network connections, delete temporary files, or just free up some memory.

Letā€™s imagine a function that creates a temporary file, reads and writes to it for various operations, and then closes and deletes it.

import * as fs from "fs";

export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");

// use file...

// Close the file and delete it.
fs.closeSync(file);
fs.unlinkSync(path);
}

This is fine, but what happens if we need to perform an early exit?

export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");

// use file...
if (someCondition()) {
// do some more work...

// Close the file and delete it.
fs.closeSync(file);
fs.unlinkSync(path);
return;
}

// Close the file and delete it.
fs.closeSync(file);
fs.unlinkSync(path);
}

Weā€™re starting to see some duplication of clean-up which can be easy to forget. Weā€™re also not guaranteed to close and delete the file if an error gets thrown. This could be solved by wrapping this all in a try/finally block.

export function doSomeWork() {
const path = ".some_temp_file";
const file = fs.openSync(path, "w+");

try {
// use file...

if (someCondition()) {
// do some more work...
return;
}
}
finally {
// Close the file and delete it.
fs.closeSync(file);
fs.unlinkSync(path);
}
}

That brings us to the first star of the feature: using declarations! using is a new keyword that lets us declare new fixed bindings, kind of like const. The key difference is that variables declared with using get their Symbol.dispose method called at the end of the scope!

So we could simply have written our code like this:

export function doSomeWork() {
using file = new TempFile(".some_temp_file");

// use file...

if (someCondition()) {
// do some more work...
return;
}
}

Check it outā€Šā€”ā€Šno try/finally blocks! At least, none that we see. Functionally, thatā€™s exactly what using declarations will do for us, but we donā€™t have to deal with that.

Decorator Metadata

TypeScript 5.2 implements an upcoming ECMAScript feature called decorator metadata.

The key idea of this feature is to make it easy for decorators to create and consume metadata on any class theyā€™re used on or within.

Whenever decorator functions are used, they now have access to a new metadata property on their context object. The metadata property just holds a simple object. Since JavaScript lets us add properties arbitrarily, it can be used as a dictionary that is updated by each decorator. Alternatively, since every metadata object will be identical for each decorated portion of a class, it can be used as a key into a Map. After all decorators on or in a class get run, that object can be accessed on the class via Symbol.metadata.

interface Context {
name: string;
metadata: Record<PropertyKey, unknown>;
}

function setMetadata(_target: any, context: Context) {
context.metadata[context.name] = true;
}

class SomeClass {
@setMetadata
foo = 123;

@setMetadata
accessor bar = "hello!";

@setMetadata
baz() { }
}

const ourMetadata = SomeClass[Symbol.metadata];

console.log(JSON.stringify(ourMetadata));
// { "bar": true, "baz": true, "foo": true }

This can be useful in a number of different scenarios. Metadata could possibly be attached for lots of uses like debugging, serialization, or performing dependency injection with decorators. Since metadata objects are created per decorated class, frameworks can either privately use them as keys into a Map or WeakMap, or tack properties on as necessary.

Named and Anonymous Tuple Elements

Tuple types have supported optional labels or names for each element.

type Pair<T> = [first: T, second: T];

These labels donā€™t change what youā€™re allowed to do with themā€Šā€”ā€Štheyā€™re solely to help with readability and tooling.

However, TypeScript previously had a rule that tuples could not mix and match between labeled and unlabeled elements. In other words, either no element could have a label in a tuple, or all elements needed one.

// āœ… fine - no labels
type Pair1<T> = [T, T];

// āœ… fine - all fully labeled
type Pair2<T> = [first: T, second: T];

// āŒ previously an error
type Pair3<T> = [first: T, T];
// ~
// Tuple members must all have names
// or all not have names.

This could be annoying for rest elements where weā€™d be forced to just add a label like rest or tail.

// āŒ previously an error
type TwoOrMore_A<T> = [first: T, second: T, ...T[]];
// ~~~~~~
// Tuple members must all have names
// or all not have names.

// āœ…
type TwoOrMore_B<T> = [first: T, second: T, rest: ...T[]];

It also meant that this restriction had to be enforced internally in the type system, meaning TypeScript would lose labels.

type HasLabels = [a: string, b: string];
type HasNoLabels = [number, number];
type Merged = [...HasNoLabels, ...HasLabels];
// ^ [number, number, string, string]
//
// 'a' and 'b' were lost in 'Merged'

In TypeScript 5.2, the all-or-nothing restriction on tuple labels has been lifted. The language can now also preserve labels when spreading into an unlabeled tuple.

Easier Method Usage for Unions of Arrays

In previous versions on TypeScript, calling a method on a union of arrays could end in pain.

declare let array: string[] | number[];

array.filter(x => !!x);
// ~~~~~~ error!
// This expression is not callable.
// Each member of the union type '...' has signatures,
// but none of those signatures are compatible
// with each other.

In this example, TypeScript would try to see if each version of filter is compatible across string[] and number[]. Without a coherent strategy, TypeScript threw its hands in the air and said "I canā€™t make it work".

In TypeScript 5.2, before giving up in these cases, unions of arrays are treated as a special case. A new array type is constructed out of each memberā€™s element type, and then the method is invoked on that.

Taking the above example,Ā string[] | number[]Ā is transformed intoĀ (string | number)[]Ā (orĀ Array<string | number>), andĀ filterĀ is invoked on that type. There is a slight caveat which is thatĀ filterĀ will produce anĀ Array<string | number>Ā instead of aĀ string[] | number[]; but for a freshly produced value there is less risk of something "going wrong".

This means lots of methods likeĀ filter,Ā find,Ā some,Ā every, andĀ reduceĀ should all be invokable on unions of arrays in cases where they were not previously.ead of a string[] | number[]; but for a freshly produced value there is less risk of something "going wrong".

This means lots of methods like filter, find, some, every, and reduce should all be invokable on unions of arrays in cases where they were not previously.

Consistent Export Checking for Merged Symbols

When two declarations merge, they must agree in whether they are both exported. Due to a bug, TypeScript missed specific cases in ambient contexts, like in declaration files or declare module blocks. For example, it would not issue an error on a case like the following, where replaceInFile is declared once as an exported function, and one as an un-exported namespace.

declare module 'replace-in-file' {
export function replaceInFile(config: unknown): Promise<unknown[]>;
export {};

namespace replaceInFile {
export function sync(config: unknown): unknown[];
}
}

In an ambient module, adding an export {Ā ... } or a similar construct like export defaultĀ ... implicitly changes whether all declarations are automatically exported. TypeScript now recognizes these unfortunately confusing semantics more consistently, and issues an error on the fact that all declarations of replaceInFile need to agree in their modifiers, and will issue the following error:

Individual declarations in merged declaration 'replaceInFile' must be all exported or all local.

For more information, see the change here.

Conclusion

Typescript eventually has been improving overtime being one of the most prominent language with minimal bug record if aligned to itā€™s best practices. The any tag typescript developers are not referred to here šŸ˜‚..

Typescript Beta 5.2 is out there for you to try out or play with.

Reference:

This article is heavily dependent on this blog post below and itā€™s more detailed I only just picked out the need to know features.

https://devblogs.microsoft.com/typescript/announcing-typescript-5-2-beta/#inline-variable-refactoring

Find this article helpful? Drop a like or comment.

Gracias šŸ™.

--

--

Israel
Israel

Written by Israel

I'm Isreal a Frontend Engineer with 4+ experience in the space . My love to profer solutions led me to being a technical writer. I hope to make +ve impact here.

No responses yet