Typescript 5.0 Released: New Features and Improvements!
Typescript, which developers mocked earlier, is now the most efficient programming language for building production-level applications. It was released in 2012, and it has been constantly evolving and releasing new features to improve the development experience since then. Microsoft released Typescript 5.0 a few months ago, with many new and exciting features as well as a few breaking changes.
Let’s take a closer look at what new Typescript 5.0 has to offer and how you can start using it in your existing or new projects 🚀
Decorators
Decorators are not unique to Typescript; they are a general programming concept used in many languages, such as Python. These are not yet natively supported by ECMAScript (JavaScript), but there is a proposal, currently in stage 3, to add Decorators to ECMAScript.
However, Decorators have been a part of Typescript for a long time, but now these are revamped and include the ECMAScript Stage 3 Decorator Proposal.
You can read more about the proposal here.
But what exactly are Decorators?
Decorators, as the name implies, are used to decorating something. This something can be a function, a class, or the methods of a class.
Let us consider a simple example: suppose you have the following class:
class Dog {
name: string;
constructor(name: string) {
this.name = name;
}
bark() {
return 'Woof!';
}
}
Code language: TypeScript (typescript)
Now, if you create an object of this class and call the bark
method on it, you’ll get the output shown below:
const dog = new Dog('Jack');
console.log(dog.bark()); // Woof!
Code language: TypeScript (typescript)
Assume you want to log something before and after calling the function; you’d write something like this:
console.log('Function started execution');
console.log(dog.bark());
console.log('Function finished execution');
Code language: TypeScript (typescript)
But what if you want to do the same thing for all methods or methods from a different class?
console.log('Function started execution');
console.log(dog.bark());
console.log('Function finished execution');
console.log('Function started execution');
console.log(dog.run());
console.log('Function finished execution');
console.log('Function started execution');
console.log(dog.sit());
console.log('Function finished execution');
Code language: TypeScript (typescript)
It’s redundant and ineffective. There is a better way to accomplish this, and it is called Decorators.
Decorators are functions that modify (or decorate) another function, class, or method and return the modified version.
Let’s talk code now 👨💻
We’ll write a log
function that accepts an argument from another function. It returns another function, which is a modified version of the one received.
function log(originalFunction: any, context: any) {
return function () {
console.log('Function started execution');
const result = originalFunction.apply(this, arguments);
console.log('Function finished execution');
return result;
};
}
Code language: TypeScript (typescript)
We’re not doing anything fancy to modify the original function. Simply put, we’re logging something before and after the original function is called.
Before defining the class methods, we’ll use the @function
syntax to modify the bark
function.
@log
bark() {
return 'Woof!';
}
Code language: TypeScript (typescript)
Calling the bark
method now will produce the following results:
const dog = new Dog('Jack');
console.log(dog.bark());
// Output:
// Function started execution
// Woof!
// Function finished execution
Code language: TypeScript (typescript)
The log
the function that we just created, is called a Decorator.
See, how simple that was!
These decorators can be as complex as you want, and you can use multiple decorators for a single method:
@log
@decorator1
@decorator2
bark() {
return 'Woof!';
}
Code language: TypeScript (typescript)
So, give decorators a shot; you’ll fall in love with how it simplifies complex problems.
The const Type Parameters
Typescript will infer the object’s type to be its general type by default. As an example:
type Cities = { names: string[] };
function getCities<T extends Cities>(arg: T): T['names'] {
return arg.names;
}
// Inferred type: string[]
const names = getCities({ names: ['Banglore', 'Jhansi', 'Jaipur'] });
Code language: TypeScript (typescript)
But suppose we have a more specific use case, such as a read-only string array:
type Cities = { readonly names: string[] };
Code language: TypeScript (typescript)
Assume we’re going to use this read-only array in the getCities
function:
type Cities = { readonly names: string[] };
function getCities<T extends Cities>(arg: T): T['names'] {
return arg.names;
}
// Inferred type: string[]
const names = getCities({ names: ['Banglore', 'Jhansi', 'Jaipur'] });
Code language: TypeScript (typescript)
The return value of the getCities
function shows that we got the incorrect inferred type string[]
, rather than the readonly string[]
.
To solve this problem, Typescript 5.0 gives us the option to use the const
type parameter on the function declaration as follows:
type Cities = { readonly names: string[] };
function getCities<const T extends Cities>(arg: T): T['names'] {
return arg.names;
}
// Inferred type: readonly string[]
const names = getCities({ names: ['Banglore', 'Jhansi', 'Jaipur'] });
Code language: TypeScript (typescript)
However, keep in mind that the const
type modifier is only applicable when the objects are written directly within the function call:
func([...]);
Code language: TypeScript (typescript)
There will be no change in behavior if arguments cannot be modified with as const
modifier.
const cities = { names: ['Banglore', 'Jhansi', 'Jaipur'] };
// Inferred type: string[]
const names = getCities(cities);
Code language: TypeScript (typescript)
Extending from multiple configuration files
Typescript 5.0 now allows you to extend configuration from multiple other configuration files, which was previously limited to just one. You can now easily manage and share configuration files across projects.
To extend from a different file, use the extends
option in tsconfig.json
to pass an array of file paths.
{
"extends": ["file1", "file2", "file3"],
"compilerOptions": {
// ... other options here ...
}
}
Code language: JSON / JSON with Comments (json)
It will combine the configuration settings from all files from which you extend. You can also override them by modifying the current configuration to include the new values.
All enums Are Union enums
Typescript 5.0 now treats all enums as union enums. That is, each member can have a distinct type, and members can be referred to by their respective types. It also implies that an enum member can now be initialized via a function call.
enum Demo {
A = 1,
B = 'Codedamn',
C = 12 + 5,
D = Math.random(),
}
Code language: TypeScript (typescript)
bundler Module Resolution Strategy
Earlier versions of Typescript used node16
and nodenext
options for module resolution strategies. These options were excellent, but there was one flaw: they imposed restrictions that some tools did not require.
Consider the ESM imports in Node.js. When attempting to import a file, you must specify the extension.
import { something } from './something.ts';
Code language: TypeScript (typescript)
However, when using module bundlers such as Vite, Webpack, and others, you do not need to add an extension. As a result, having to use options like node16
or nodenext
to work with different module bundlers was very painful for developers.
Keeping this in mind, Typescript 5.0 now implements the new hybrid bundler
module resolution strategy, which works well with various bundlers to provide hassle-free builds.
To configure it, use the moduleResolution
option in the compilerOptions
section of tsconfig.json
file:
{
"compilerOptions": {
// ... other options
"moduleResolution": "bundler"
}
}
Code language: JSON / JSON with Comments (json)
Module Resolution Customization Flags
As previously stated, the new bundler
module resolution strategy is best suited for hybrid module resolution in projects that use multiple tools. However, these tools may support different module resolutions, which might be problematic.
Keeping this in mind, Typescript 5.0 allows you to enable or disable features and have more control over your configuration.
Here is a list of the new module resolution flags added in Typescript 5.0.
--allowImportingTsExtensions
: You can use this flag to import Typescript extension files such as.ts
,.tsx
, and so on. Note that this flag is only available when the--noEmit
or--emitDeclarationOnly
flag is set to true.-resolvePackageJsonExports
: This flag is useful when you want Typescript to do exports based on the toexports
field in thepackage.json
file.--resolvePackageJsonImports
: This flag, like the--resolvePackageJsonExports
flag, is used when we want Typescript to do lookups, starting with#
having its parent directory containing apackage.json
file, according to thepackage.json
file’simports
field.--customConditions
: This option is useful when you want a custom condition to be followed when resolving files based on theexports
andimports
fields of thepackage.json
file.
Persist the Module Syntax During Builds
Typescript will drop your type
imports in the emitted JavaScript files. This is done by default and is called import elision. For example:
// This type import might be removed entirely in the emitted JavaScript
import { Dog } from './dog';
Code language: TypeScript (typescript)
To prevent this, Typescript 5.0 adds the --verbatimModuleSyntax
and deprecates older solutions, such as --importsNotUsedAsValues
and --preserveValueImports
.
When using the --verbatimModuleSyntax
flag, the following rules apply: any imports or exports without a type
modifier won’t be affected. However, anything that makes use of the type
modifier is removed completely.
// Removed completely
import type { Dog } from 'dog';
// Rewritten to 'import { cat, cow } from "animal";'
// The type lion is removed
import { cat, type lion, cow } from 'animal';
Code language: TypeScript (typescript)
Support for export type *
Exporting types from another module was a pain in an earlier version of Typescript. You were not permitted to export types using the export type *
syntax.
// File: animals.ts
export type Dog;
export type Cat;
// File: index.js
// Won't work in older versions of TypeScript
export type * from './animals';
Code language: TypeScript (typescript)
However, with Typescript 5.0, you can use the export type *
syntax to export your types from a different module.
// File: animals.ts
export type Dog;
export type Cat;
// File: index.js
// Works with TypeScript 5.0
export type * from './animals';
// Even this works
export type * as Animals from './animals';
Code language: TypeScript (typescript)
@satisfies Support in JSDoc
Typescript 5.0 now supports the @satisfies
tag in JSDoc. You can use this feature to specify whether an expression is compatible with a type or interface. This is the same as the satisfies
operator introduced in Typescript 4.9.
interface Speak {
speak(dogName: string): string;
}
/**
* @satisfies {Speak}
*/
function Bark(dogName: string) {
return `Woof! My name is ${dogName}`;
}
Code language: TypeScript (typescript)
@overload Support in JSDoc
Function overloading occurs when a single function has several variants based on the type and number of arguments it accepts. For example:
function adder(arg1, arg2) {
return arg1 + arg2;
}
Code language: TypeScript (typescript)
If two numbers are passed to the adder
function, it will return their sum; however, if two strings are passed, it will return the concatenation of the strings.
With Typescript 5.0, we can now specify different function overloads in JSDoc using the @overload
tag. It informs the compiler about the various versions of the function. Let us return to our previous example:
/**
* @overload
* @param {string} arg1
* @param {string} arg2
* @return {string}
*/
/**
* @overload
* @param {number} arg1
* @param {number} arg2
* @return {number}
*/
/**
* @param {string | number} arg1
* @param {string | number} arg2
*/
function adder(arg1, arg2) {
return arg1 + arg2;
}
Code language: TypeScript (typescript)
The compiler is aware that there are now two overloads of the function, one of which accepts numbers and other strings as arguments.
Passing Emit-Specific Flags Under --build
When building your application, you can now include emit flags under the --build
mode. It gives you more granular control over the build configuration when working on different project environments that require different build configurations.
Here are the flags available to use in Typescript 5.0:
--declaration
: Generates declaration and JavaScript files for the project.--emitDeclarationOnly
: Only the declaration files are generated; no JavaScript files are generated.--declarationMap
: Generates a source map for the declarations.--sourceMap
: Generates a source map for the JavaScript files.--inlineSourceMap
: Put the generated source map inside the JavaScript files.
Case-Insensitive Import Sorting in Editors
Sorting imports in code editors like VSCode, etc., was very basic in earlier versions of Typescript and was case-sensitive. As a result, you may have noticed some incorrect sorting, as shown below.:
import { Dog, cat, lion } from './animals';
Code language: TypeScript (typescript)
Because uppercase letters are placed before lowercase letters in the ASCII representation, they appear first in the sorted import list.
This is no longer the case because Typescript 5.0 now sorts imports without considering the case (case-insensitive), so you won’t see any weird sorting in the editor.
Exhaustive switch/case Completions
Dealing with switch
statements are now easier with improved code completion in Typescript 5.0. Typescript 5.0 can now detect the referencing type within the switch
statements and display suggestions for all possible cases.
type Animal =
| { type: 'dog' }
| { type: 'cow' }
| { type: 'cat' }
| { type: 'lion' };
function speak(animal: Animal): string {
switch (animal.type) {
/**
* Typescript will suggest to add cases for 'dog, 'cow', 'cat', and 'lion'
*
* case 'dog':
* case 'cow':
* case 'cat':
* case 'lion':
*/
}
}
Code language: TypeScript (typescript)
It comes in handy when dealing with a long list of cases because it ensures that none of the cases are missed.
Speed, Memory, and Package Size Optimizations
Typescript is now significantly faster, more memory efficient, and much smaller in size. It improves the overall development experience with Typescript.
Here are some charts comparing the new and old Typescript versions.
There is a significant difference in the size of Typescript’s npm package. The performance enhancements are also significant, resulting in a more smooth and more efficient experience.
Summary
Typescript is the most effective programming language for developing production-level applications. Since its initial release in 2012, it has been constantly evolving and releasing new features to improve the development experience.
Typescript 5.0 was released a few months ago, and it included a lot of new and exciting features, as well as a few breaking changes. Aside from that, several performance enhancements have resulted in a significant increase in performance, memory efficiency, and smaller package size.
It is, in my opinion, a worthwhile upgrade. You can read the entire announcement blog for more information on all of the listed Typescript additions here.
This article hopefully provided you with some new information. Share it with your friends if you enjoy it. Also, please provide your feedback in the comments section.
Thank you so much for reading 😄
Sharing is caring
Did you like what Varun Tiwari wrote? Thank them for their work by sharing it on social media.
No comments so far
Curious about this topic? Continue your journey with these coding courses: