TypeScript 4.5 was announced on 17 November 2021. It has a section written for JavaScript files: a const assertion can be written as a JSDoc cast, /** @type {const} */ (value), and a @template tag can give its type parameter a default.
The release also added the Awaited type, the module: es2022 setting for top-level await, and checks for #field in obj, a JavaScript feature for testing whether an object has a private field.
At a glance
| Release | TypeScript 4.5 |
|---|---|
| Released | 17 November 2021, the day of its announcement on the TypeScript blog |
| Source | TypeScript 4.5 release notes |
| Examples | Every example runs through TypeScript 5.9.3 when this site is built. |
| Previous release | TypeScript 4.4 |
| Next release | TypeScript 4.6 |
What changed for JavaScript
const assertions in JSDoc
In a .ts file, as const keeps literal values and makes the result read-only. The JavaScript spelling is a cast to const: /** @type {const} */ ({ ... }), with the parentheses that every JSDoc cast needs.
export const ROUTES = /** @type {const} */ ({
home: "/",
cart: "/cart",
account: "/account",
});
/** @typedef {(typeof ROUTES)[keyof typeof ROUTES]} Path */
/** @param {Path} path */
export function go(path) {
location.assign(path);
}
go(ROUTES.cart);
go("/checkout");error TS2345: Argument of type '"/checkout"' is not assignable to parameter of type 'Path'.export const ROUTES = {
home: "/",
cart: "/cart",
account: "/account",
} as const;
type Path = (typeof ROUTES)[keyof typeof ROUTES];
export function go(path: Path) {
location.assign(path);
}
go(ROUTES.cart);
go("/checkout");error TS2345: Argument of type '"/checkout"' is not assignable to parameter of type 'Path'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} Default type arguments in @template
A type parameter in square brackets with = gets a default: @template {string | number} [T=number]. The default applies when nothing else sets the type, as it does for type Box<T = number> in TypeScript.
/**
* @template [T=unknown]
* @typedef {{ ok: true, data: T } | { ok: false, error: string }} Result
*/
/** @type {Result<number>} */
const count = { ok: true, data: 3 };
/** @type {Result} */
const anything = { ok: true, data: "whatever" };
/** @param {Result<number>} result */
export function show(result) {
return result.ok ? result.data.toFixed(0) : result.error;
}
show(count);
export { anything };type Result<T = unknown> = { ok: true; data: T } | { ok: false; error: string };
const count: Result<number> = { ok: true, data: 3 };
const anything: Result = { ok: true, data: "whatever" };
export function show(result: Result<number>) {
return result.ok ? result.data.toFixed(0) : result.error;
}
show(count);
export { anything };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} The Awaited type
Awaited<T> is the type await produces: it unwraps promises, nested ones included. Promise.all and other built-ins use it, which fixes several inference problems with mixed promises and plain values.
/** @param {string} id */
export async function loadAccount(id) {
return { id, name: "Jane", orders: /** @type {string[]} */ ([]) };
}
/** @typedef {Awaited<ReturnType<typeof loadAccount>>} Account */
/** @param {Account} account */
export function greeting(account) {
return `Hello, ${account.name}`;
}
loadAccount("u_1").then(greeting);export async function loadAccount(id: string) {
return { id, name: "Jane", orders: [] as string[] };
}
type Account = Awaited<ReturnType<typeof loadAccount>>;
export function greeting(account: Account) {
return `Hello, ${account.name}`;
}
loadAccount("u_1").then(greeting);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} #field in obj
JavaScript can test whether an object has a private field with #cents in other. TypeScript 4.5 checks the test and narrows other to the class, so other.#cents is allowed after it.
export class Money {
#cents;
/** @param {number} cents */
constructor(cents) {
this.#cents = cents;
}
/** @param {unknown} other */
equals(other) {
return typeof other === "object" && other !== null && #cents in other && this.#cents === other.#cents;
}
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} Replacing the DOM types with a package
TypeScript looks for @typescript/lib-dom (and the other @typescript/lib-* names) in node_modules before using its own lib files, so a project can pin its DOM types with a package alias and upgrade TypeScript without new DOM errors. TypeScript 5.8 added libReplacement to switch the lookup off, and TypeScript 6.0 turns it off by default.
{
"devDependencies": {
"@typescript/lib-dom": "npm:@types/web"
}
} Import assertions
import data from "./data.json" assert { type: "json" } tells the runtime what kind of module to expect, and TypeScript 4.5 accepts it without checking the value. The syntax did not last: TypeScript 5.3 replaced it with import attributes (with), and 5.8 rejects assert under nodenext.
import settings from "./settings.json" assert { type: "json" };error TS2880: Import assertions have been replaced by import attributes. Use 'with' instead of 'assert'.
export const port = settings.port;jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} What did not change
as const itself is still TypeScript syntax and an error in a .js file (TS8016); the JSDoc cast is the way to write it.
Upgrade notes
The breaking changes are small:
- Inference that now goes through
Awaitedcan produce different types in generic code. - A
jsconfig.jsonortsconfig.jsonwith compiler options at the top level and nocompilerOptionsobject is now an error, which catches options that were silently ignored.
Gotchas
- A JSDoc cast needs parentheses around the value;
/** @type {const} */ { a: 1 }without them is not a cast.
Sources
- TypeScript 4.5 release notes · TypeScript handbook
- Announcing TypeScript 4.5 · TypeScript blog
Last reviewed 20 October 2026.