TypeScript 5.3 What's New

technology5 min

byLucas Santos

This page was machine translated. Read original / Suggest a fix

As usual, I want to bring you the main TypeScript news as it comes out! Recently, I learned that the TypeScript team is working on a new version of the language. To do this, they create something called an iteration plan.

An iteration plan is not a document that shows exactly what will be released in the language, but rather what might possibly be in upcoming versions. Basically it’s a document of the team’s intentions to implement and improve the compiler.

The beta version of TS 5.3 is expected to be released in September (a bit before the launch of the first module of Formação TS) and the final version should be out in November!

Matt from Total TypeScript compiled some of what’s in the document in that other article, and I’ll take the opportunity to summarize and explain it while linking to the JavaScript documentation.

Import Attributes#

This is a proposal that’s been open for a while, and the main goal is to let you specify validation options for module imports. Right now we’re only talking about validating the module type, for example, to verify that a module is a JSON file:

import json from './foo.json' with { type: 'json' };

This is useful from a security perspective, for example, if a file is expected to be JSON but is actually JS.

You can use these attributes in dynamic imports:

import("foo.json", { with: { type: "json" } });

Or even export a module with another validated type:

export { val } from './foo.js' with { type: "javascript" };

This also applies to WebAssembly or different workers:

new Worker("foo.wasm", {
type: "module",
with: { type: "webassembly" },
});

Using with followed by type is a way to keep this feature open because it will be possible to include more properties later. Imagine being able to import a package by one of the properties of package.json or even from the file itself.

Throw expressions#

This is a feature I’ll discuss in a future video, but it’s something that’s been missing from JS for a long time. The proposal for this is still at stage 2, which is unusual for TS, since it usually implements most features when they’re at stage 3.

What might happen is that, as they mention in the plan, the team will be “championing” the proposal, which is a way of saying they’ll actively work on it and push it forward to reach stage 3 and 4 faster.

The idea is that you can use throw outside of a specific statement, for example, in a variable declaration:

const userName = user.name || throw new Error('Name is required')

Today this isn’t possible.

Isolated Declarations#

There’s a problem when working with monorepos in almost all languages, but in TypeScript it’s worse. Because when you have packages that depend on other packages, it inevitably generates an absurd amount of complexity, especially for TypeScript.

If you have 10 levels of packages depending on each other, TS needs to infer all the types of all packages itself, starting from the bottom up and generating the declarative files for each package to import into the package above. And that’s very slow.

And since there’s no faster way to do this without changing the compiler, because other tools like esbuild or even swc aren’t smart enough for that, TypeScript has to infer everything, and it’s not necessarily the most performant compiler out there.

This proposal adds a new configuration to TSConfig called isolatedDeclarations.

{
"compilerOptions": {
"isolatedDeclarations": true
}
}

What it does is enable a stricter mode, where you need to, for example, add type annotations on function returns, especially for exported functions, so TS doesn’t have to infer everything.

Narrowing generics in functions#

There’s a latent problem in TypeScript where it won’t perform type narrowing on a generic type inside a function when we want to return something based on that type.

Complicated? Let’s see. Imagine this:

interface F {
"t": number,
"f": boolean,
}
function depLikeFun<T extends "t" | "f">(str: T): F[T] {
if (str === "t") {
return 1;
} else {
return true;
}
}
depLikeFun("t"); // number
depLikeFun("f"); // boolean

That’s exactly what we’re talking about. What we want is that if we pass t, the return type is number, if it’s f it’s boolean. To achieve this we need a type T that’s passed as a generic of the function, and then we return the key of the interface F based on T.

But if we do this now, we’ll get an error saying our returns aren’t of type never. This is because TS doesn’t narrow our generic type T. Actually, what it’s doing, according to this issue, is that when we pass F[T] we’re saying “write F at key T”, so it will check if F[T] has a type that intersects both of its possibilities (in this case number and boolean), so number & boolean is never.

Autocomplete in string types#

An interesting hack—that I discovered recently—is that you can create a union type of strings and string & {}, this way you get autocomplete from the compiler, plus you can put any other string in its place:

type IconSize =
| "small"
| "medium"
| "large"
| (string & {});

So this would be completely valid:

const icons: IconSize[] = [
"small",
"medium",
"large",
"extra-large",
];

In version 5.3 it might be possible to remove the & {} and use just string to get the same result:

type IconSize =
| "small"
| "medium"
| "large"
| string;

What’s coming next?#

This is a foundational article, we don’t know if these features will actually be added to TS or not, because the document is a work plan and not a guide of what will be included.

I hope some of these features, especially union types and type narrowing, get added because it will be helpful for most people.