TypeScript 4.1 was announced on 19 November 2020. For JavaScript projects its most visible change is a small one: checkJs now implies allowJs, so a tsconfig.json needs only one of them. (A jsconfig.json already defaulted allowJs to true.)
It also added noUncheckedIndexedAccess, template literal types and key remapping, editor support for @see, and a breaking change that reaches most JavaScript code that creates promises: resolve() now needs a value unless the promise is a Promise<void>.
At a glance
| Release | TypeScript 4.1 |
|---|---|
| Released | 19 November 2020, the day of its announcement on the TypeScript blog |
| Source | TypeScript 4.1 release notes |
| Examples | Every example runs through TypeScript 5.9.3 when this site is built. |
| Previous release | TypeScript 4.0 |
| Next release | TypeScript 4.2 |
What changed for JavaScript
checkJs implies allowJs
Setting "checkJs": true alone now brings .js files into the program and checks them. This site's tests check the rule against TypeScript 5.9; the checkJs page has the details.
const user = { name: "Ada", role: "admin" };
let label = user.name;
label = label.toUppercase();error TS2551: Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?
export { label };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} resolve() needs a value
The resolve function of new Promise takes a value that is no longer optional, so resolve() with nothing is an error unless the promise is typed Promise<void>. In a .js file the message says so (TS2810: new Promise() needs a JSDoc hint). The simplest hint is @returns {Promise<void>} on the function that returns the promise.
/** @param {number} ms */
export function waitLoose(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve(), ms);error TS2810: Expected 1 argument, but got 0. 'new Promise()' needs a JSDoc hint to produce a 'resolve' that can be called without arguments.
});
}
/**
* @param {number} ms
* @returns {Promise<void>}
*/
export function wait(ms) {
return new Promise((resolve) => {
setTimeout(() => resolve(), ms);
});
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} noUncheckedIndexedAccess
A new option, outside strict, makes a lookup by index or key include undefined, so items[0] has to be checked before use.
const names = ["Ada", "Grace"];
const third = names[2];
console.log(third.toUpperCase());error TS18048: 'third' is possibly 'undefined'.
/** @type {Record<string, number>} */
const stock = { tea: 12 };
console.log(stock.cake.toFixed(0));error TS18048: 'stock.cake' is possibly 'undefined'.
for (const name of names) console.log(name.toUpperCase());jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": [],
"noUncheckedIndexedAccess": true
},
"include": [
"*.js"
]
} Template literal types and key remapping
Type syntax gained template literal types (`on${Capitalize<E>}`) and an as clause in mapped types. Both work inside JSDoc braces exactly as in a .ts file.
/**
* @template T
* @typedef {{ [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K] }} Getters
*/
/** @type {Getters<{ sku: string, price: number }>} */
const product = {
getSku: () => "TEE-01",
getPrice: () => 19.5,
};
product.getPrice().toFixed(2);
export { product };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["*.js"]
} any and unknown in && expressions
In value && expr, an unknown (or any) value now makes the whole expression unknown (or any), because the falsy value may be what comes out. A function typed @returns {boolean} that returns such an expression fails; !!value && … fixes it.
/**
* @param {unknown} value
* @returns {boolean}
*/
export function isRecordLoose(value) {
return value && typeof value === "object";error TS2322: Type 'unknown' is not assignable to type 'boolean'.
}
/**
* @param {unknown} value
* @returns {boolean}
*/
export function isRecord(value) {
return !!value && typeof value === "object";
}jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": [
"*.js"
]
} paths without baseUrl
paths no longer needs baseUrl: without it, each path is resolved from the folder of the config file. Older jsconfig.json files often set baseUrl only for this, and TypeScript 6.0 deprecates baseUrl.
import { formatPrice } from "@lib/format.js";
console.log(formatPrice("19.99"));error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": [],
"paths": {
"@lib/*": [
"./src/lib/*"
]
}
},
"include": [
"src/**/*.js"
]
} What did not change
A jsconfig.json checked the same files before and after: it already defaulted allowJs on.
@seewas already read as documentation; 4.1 adds go to definition on the names it mentions.
Upgrade notes
New errors in checked JavaScript usually come from:
resolve()without a value innew Promise(TS2810 in.jsfiles).x && …with anunknownoranyxin boolean-returning functions.- Conditional spreads that now produce optional properties.
Gotchas
- Passing
resolveitself, as insetTimeout(resolve, ms), raises no error: the timer calls it with no arguments at run time, but TypeScript checks only direct calls such asresolve().
Sources
- TypeScript 4.1 release notes · TypeScript handbook
- Announcing TypeScript 4.1 · TypeScript blog
Last reviewed 20 October 2026.