TypeScript release

TypeScript 4.0 for JavaScript users

  • Released 20 August 2020
  • Examples run with TypeScript 5.9

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

ReleaseTypeScript 4.0
Released20 August 2020, the day of its announcement on the TypeScript blog
SourceTypeScript 4.0 release notes
ExamplesEvery example runs through TypeScript 5.9.3 when this site is built.
Next releaseTypeScript 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.

totals.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @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]);
Marking a function as deprecated.
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.

retry.js TypeScript 5.9.3 TypeScript 5.9.3, run 21 October 2026: No errors. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. No errors. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. No errors. checked 21 October 2026
/**
 * @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;
}
??= and ||= fill in defaults, and the checker follows the assignment.
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.

tuples.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: tuples.js(22,14): 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. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. tuples.js(22,14): 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. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. tuples.js(22,14): 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. checked 21 October 2026
/** @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.
A variadic tuple type in @returns keeps every element of both arguments.
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.

jsconfig.json
{
  "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).

user.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: user.js(11,10): error TS2790: The operand of a 'delete' operator must be optional. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. user.js(11,10): error TS2790: The operand of a 'delete' operator must be optional. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. user.js(11,10): error TS2790: The operand of a 'delete' operator must be optional. checked 21 October 2026
/**
 * @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.
}
Under strictNullChecks, delete needs an optional property.
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.

shapes.js Error confirmed 5.9.3 TypeScript 5.9.3, run 21 October 2026: shapes.js(9,3): error TS2610: 'sides' is defined as an accessor in class 'Shape', but is overridden here in 'Square' as an instance property. 6.0.3 TypeScript 6.0.3, run 21 October 2026: the same as 5.9.3. shapes.js(9,3): error TS2610: 'sides' is defined as an accessor in class 'Shape', but is overridden here in 'Square' as an instance property. 7.0.2 TypeScript 7.0.2, run 21 October 2026: the same as 5.9.3. shapes.js(9,3): error TS2610: 'sides' is defined as an accessor in class 'Shape', but is overridden here in 'Square' as an instance property. checked 21 October 2026
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.
}
A class field cannot replace a getter from the base class.
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 .ts file. In a .js file, TypeScript 5.9 reads the same annotation written as catch (/** @type {unknown} */ e) (the release notes do not date that form); TypeScript 4.4 added useUnknownInCatchVariables to make every catch variable unknown.
  • Class property inference from constructor assignments came to .ts files under noImplicitAny. A checked .js file already declares a class property from this.name = … in the constructor.

Upgrade notes

From the breaking changes:

  • delete on a required property is an error under strictNullChecks.
  • A field may not override an accessor, nor an accessor a field.
  • The DOM types dropped document.origin; use self.origin.

Gotchas

  • @deprecated on one of several @overload signatures produced no suggestion in this site's tests, so mark the whole function.

Sources

Last reviewed 20 October 2026.