What’s Fresh in TypeScript 5.3: Let’s Dive In!

Evelyn Taylor
7 min readDec 12, 2023

Imagine you’re working with JavaScript, which is a flexible but sometimes tricky language.

TypeScript comes into play as a sort of assistant that helps catch mistakes before you even run your code.

It does this by adding a way to specify the types of things (like variables and functions) in your code.

So, besides preventing common errors, TypeScript also brings some cool features to the table. Think of it like having a smart friend who not only points out when you’re about to make a typo or forget to check for certain things but also helps you out while you’re writing code.

If you’ve ever used Visual Studio or VS Code, the magic that happens with auto-complete, code navigation, and making your code look cleaner — that’s TypeScript working behind the scenes.

If you’re curious and want to give it a shot, here’s how you can dive into using TypeScript:

To kick things off with TypeScript, you can grab it using NuGet, or if you’re more into the npm world, just toss in this command:

npm install -D typescript

Import Attributes

In TypeScript 5.3, there’s a cool new feature called import attributes. They’re like little tags you can add to your imports to tell the runtime specific details about how to handle them.

For example, if you’re importing a JSON file and you want to make sure it’s treated as JSON and not as executable JavaScript, you can do something like this:

import obj from "./something.json" with { type: "json" };

These attributes won’t be checked by TypeScript itself; they’re more like notes for the browser or runtime to follow. So, you can get creative with them, but be careful not to confuse your browser by using nonsense types:

import * as foo from "./foo.js" with { type: "fluffy bunny" };

Dynamic imports, those made with import(), can also use these import attributes:

const obj = await import("./something.json", {
with: { type: "json" }
});

The cool thing is that TypeScript is moving away from an older feature called “import assertions” to these new import attributes.

So, instead of using the assert keyword, you now use with. If you've got old code with assertions, it's recommended to update it to use the new syntax with with.

Stable Support resolution-mode in Import Types

It’s like a setting you can use when importing types to decide whether you want to handle it like a traditional require or a modern import.

For example, in your code, if you want to treat a type import like an old-school require(), you can do something like this:

import type { TypeFromRequire } from "pkg" with {
"resolution-mode": "require"
};

And if you prefer the new-school import way, you can go for:

import type { TypeFromImport } from "pkg" with {
"resolution-mode": "import"
};

But here’s the cool part. This feature wasn’t initially available for import assertions, which are like hints about how a module should be handled.

Now, with TypeScript 5.3, they’ve extended this “resolution-mode” attribute to import types as well. So, you can use it not only in regular imports but also when pulling in types dynamically using import().

Here’s a more practical example:

export type TypeFromRequire =
import("pkg", { with: { "resolution-mode": "require" } }).TypeFromRequire;

export type TypeFromImport =
import("pkg", { with: { "resolution-mode": "import" } }).TypeFromImport;

export interface MergedType extends TypeFromRequire, TypeFromImport {}

resolution-mode Supported in All Module Modes

Now, in the past, you could only use this resolution-mode magic under specific moduleResolution options like node16 and nodenext.

But guess what? TypeScript 5.3 is making life simpler. You can now use resolution-mode with all the other moduleResolution options, like bundler, node10, and even the classic one.

No more errors popping up just because you want to tweak how you’re handling types.

TypeScript is getting more chill about it, letting you do your thing in whatever module mode you fancy. Cool, right?

switch (true) Narrowing

Now, when you’re dealing with a switch statement set to true, TypeScript can smartly narrow down the type based on conditions in each case. Let me break it down for you.

So, say you have this function:

function f(x: unknown) {
switch (true) {
case typeof x === "string":
// 'x' is a 'string' here
console.log(x.toUpperCase());
// falls through...

case Array.isArray(x):
// 'x' is a 'string | any[]' here.
console.log(x.length);
// falls through...

default:
// 'x' is 'unknown' here.
// ...
}
}

What’s happening here is, depending on the conditions in each case, TypeScript is getting smarter about figuring out the type of ‘x’.

For example, if ‘x’ is a string, it knows it and lets you do string things like toUpperCase(). If it's an array, it understands that too.

Narrowing On Comparisons to Booleans

when you’re doing direct comparisons with true or false. It’s those times when you’re explicitly checking if something is true or false, maybe for style reasons or to handle JavaScript quirks.

Before, TypeScript wasn’t catching on to these comparisons, but now it’s got your back. Let me show you with an example:

interface A {
a: string;
}

interface B {
b: string;
}

type MyType = A | B;

function isA(x: MyType): x is A {
return "a" in x;
}

function someFn(x: MyType) {
if (isA(x) === true) {
console.log(x.a); // TypeScript gets it now!
}
}

See that isA(x) === true? TypeScript now understands that and is smart enough to narrow down the type inside the if block.

So, you can confidently do things like console.log(x.a) without TypeScript complaining.

instanceof Narrowing Through Symbol.hasInstance

when you use instanceof, it's smarter and takes into account methods defined with [Symbol.hasInstance]. This means you can be more precise in narrowing down types using your own custom type guards.

Let me break it down with an example:

Imagine you have a class called Point. Now, TypeScript lets you define a special method for it using [Symbol.hasInstance]. In that method, you can put your own logic to check if something is like a Point.

class Point {
static [Symbol.hasInstance](val: unknown): val is PointLike {
// Your custom type guard logic goes here
}
}

Then, when you use instanceof in your code, TypeScript is smart enough to recognize and use your custom type guard. For instance:

function f(value: unknown) {
if (value instanceof Point) {
// Now, you can access properties defined in PointLike,
// but you won't have access to specific Point methods or properties.
}
}

So, this feature basically lets TypeScript really understand your custom type checks and narrow down types more accurately.

Checks for super Property Accesses on Instance Fields

when you’re dealing with classes, you can use the super keyword to access a method from a base class. It's a way of saying, "Hey, let me grab that method from the class I'm inheriting from."

For example:

class Base {
someMethod() {
console.log("Base method called!");
}
}

class Derived extends Base {
someMethod() {
console.log("Derived method called!");
super.someMethod();
}
}

new Derived().someMethod();
// Prints:
// Derived method called!
// Base method called!

So, you can see that super.someMethod() is a way of specifically getting the method from the base class.

Now, here’s the tricky part. If you’re working with class fields (basically, properties of an instance), using super in the same way might not work as expected. TypeScript 5.3 is smart enough to catch this and give you a heads up.

Here’s an example:

class Base {
someMethod = () => {
console.log("someMethod called!");
}
}

class Derived extends Base {
someOtherMethod() {
super.someMethod(); // This will throw an error now!
}
}

new Derived().someOtherMethod();
// 💥
// Doesn't work because 'super.someMethod' is 'undefined'.

So, TypeScript 5.3 is making sure that when you’re using super to access something, it checks if that something is a class field. If it is, and you're trying to access it using super, TypeScript will stop you before you run into a runtime error. It's like having a friend double-checking your code to prevent those sneaky bugs!

Settings to Prefer type Auto-Imports

when it automatically adds imports for types, it used to do it based on your settings. For instance, if you had a type like Person:

export let p: Person;

TypeScript would usually add an import like:

import { Person } from "./types";

export let p: Person;

But it could vary depending on your settings. For example, with certain settings like verbatimModuleSyntax, it might add the type modifier:

import { type Person } from "./types";

export let p: Person;

Now, TypeScript is giving you more control. If you have specific preferences or your codebase can’t use certain options, you can set it up to always go for explicit type imports when possible. It’s like saying, “Hey TypeScript, I want it done this way, no surprises.” So, it’s all about making TypeScript work just the way you like it.

In TypeScript 5.3, some more optimizations have been introduced:

First off, they’re skipping some JSDoc parsing. This means TypeScript doesn’t spend unnecessary time and memory on JSDoc, making compilation speedier.

This is especially noticeable in watch mode, where changes are frequent. Plus, this improvement isn’t just for TypeScript itself — tools like typescript-eslint and Prettier can also benefit from these speed and memory enhancements.

Then, there are optimizations in how TypeScript handles intersections. It’s like TypeScript is getting better at comparing types, especially in unions and intersections.

By looking at the original intersection form, it can now do checks faster, making your type assessments more efficient.

Lastly, TypeScript is doing a bit of spring cleaning by consolidating its library files, tsserverlibrary.js and typescript.js.

They’re combining them to reduce duplication and make things more consistent.

This not only streamlines how the API is used but also helps cut down on resource usage. It’s like tidying up the house to make sure everything is in its right place and working smoothly.

For a detailed rundown of all the changes, head over to the complete release log available here.

Connect with me on Medium ✍ : https://medium.com/@Evelyn.Taylor

--

--

Evelyn Taylor

A front-end enthusiast and dedicated development engineer, eager to expand knowledge on development techniques and collaborate with others.