TypeScript 4.0 was announced on 20 August 2020. For JavaScript checked with JSDoc it brought @deprecated support in editors, checking for the new &&=, ||= and ??= operators, variadic and labeled tuple types that work inside JSDoc braces, and --incremental together with noEmit, the jsconfig.json default.
Its breaking changes reach .js files too: under strictNullChecks, delete is only allowed on optional properties, and a class field can no longer replace a getter from its base class.
At a glance
| Release | TypeScript 4.0 |
|---|---|
| Released | 20 August 2020, the day of its announcement on the TypeScript blog |
| Source | TypeScript 4.0 release notes |
| Examples | Every example runs through TypeScript 5.9.3 when this site is built. |
| Next release | TypeScript 4.1 |
What changed for JavaScript
@deprecated in editors
TypeScript reads /** @deprecated */ and reports uses of the name as a suggestion: editors strike the name through and show the reason in hovers and completions. It is never an error, and tsc prints nothing.
/**
* @deprecated Use {@link cartTotal}, which takes the line items.
* @param {number[]} prices
* @returns {number}
*/
export function sumPrices(prices) {
return prices.reduce((sum, price) => sum + price, 0);
}
/**
* @param {{ price: number, quantity: number }[]} lines
* @returns {number}
*/
export function cartTotal(lines) {
return lines.reduce((sum, line) => sum + line.price * line.quantity, 0);
}
// tsc reports nothing; editors strike the name through and say it is deprecated.
sumPrices([19.5, 12]);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} &&=, ||= and ??=
The three logical assignment operators are checked in .js files, and the checker follows them: after options.retries ??= 3, options.retries is a number, not number | undefined.
/**
* @typedef {object} RetryOptions
* @property {number} [retries]
* @property {number} [delayMs]
*/
/** @param {RetryOptions} options */
export function totalWait(options) {
options.retries ??= 3;
options.delayMs ||= 250;
// both are number here, not number | undefined
return options.retries * options.delayMs;
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} Variadic and labeled tuple types
A tuple type can spread a generic tuple, [...T, ...U], so a function that joins two tuples can return their exact element types. Tuple elements can also be labeled, [start: number, end: number], which editors show in hovers. Both work in JSDoc.
/** @typedef {[start: number, end: number]} Range */
/**
* @template {unknown[]} T
* @template {unknown[]} U
* @param {[...T]} head
* @param {[...U]} tail
* @returns {[...T, ...U]}
*/
export function concat(head, tail) {
return [...head, ...tail];
}
/** @type {Range} */
const range = [1, 5];
const labeled = concat(range, ["inclusive"]);
/** @type {[number, number, string]} */
export const ok = labeled;
/** @type {[number, number]} */
export const tooShort = labeled;error TS2322: Type '[start: number, end: number, string]' is not assignable to type '[number, number]'.
Source has 3 element(s) but target allows only 2.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} --incremental with noEmit
A jsconfig.json sets noEmit by default, and until 4.0 that ruled out incremental. Now the two combine: tsc writes a .tsbuildinfo file and uses it to recheck only what changed. Checked here with TypeScript 5.9: with "incremental": true, tsc -p jsconfig.json writes jsconfig.tsbuildinfo beside the config, or the path in tsBuildInfoFile.
{
"compilerOptions": {
"checkJs": true,
"strict": true,
"incremental": true,
// keep the cache out of the source folder
"tsBuildInfoFile": ".cache/jsconfig.tsbuildinfo"
},
"include": ["src"]
} delete needs an optional property
Under strictNullChecks, the operand of delete must be optional (or any, unknown or never), because deleting a required property leaves an object that no longer fits its type (TS2790).
/**
* @typedef {object} User
* @property {string} name
* @property {string} [nickname]
*/
/** @param {User} user */
export function anonymize(user) {
delete user.nickname;
delete user.name;error TS2790: The operand of a 'delete' operator must be optional.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} Fields and accessors cannot replace each other
A class field that overrides a getter or setter from the base class, or the other way round, is now always an error (TS2610, TS2611); before 4.0 it was an error only with useDefineForClassFields. JavaScript classes are checked the same way.
class Shape {
get sides() {
return 0;
}
}
export class Square extends Shape {
sides = 4;error TS2610: 'sides' is defined as an accessor in class 'Shape', but is overridden here in 'Square' as an instance property.
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} What did not change
Two 4.0 features were written for .ts files:
catch (e: unknown)annotates a catch variable in a.tsfile. In a.jsfile, TypeScript 5.9 reads the same annotation written ascatch (/** @type {unknown} */ e)(the release notes do not date that form); TypeScript 4.4 addeduseUnknownInCatchVariablesto make every catch variableunknown.- Class property inference from constructor assignments came to
.tsfiles undernoImplicitAny. A checked.jsfile already declares a class property fromthis.name = …in the constructor.
Upgrade notes
From the breaking changes:
deleteon a required property is an error understrictNullChecks.- A field may not override an accessor, nor an accessor a field.
- The DOM types dropped
document.origin; useself.origin.
Gotchas
@deprecatedon one of several@overloadsignatures produced no suggestion in this site's tests, so mark the whole function.
Sources
- TypeScript 4.0 release notes · TypeScript handbook
- Announcing TypeScript 4.0 · TypeScript blog
Last reviewed 20 October 2026.