How strict should tsc be about your JavaScript? The jsconfig generator offers three answers, and this guide explains them: what each level catches, what it misses, and what it costs to turn on in code that was written without it. The examples below are the same small project checked at each level.
| Level | What it sets | Catches | Cost in an existing codebase |
|---|---|---|---|
| Relaxed | strict: false | Wrong arguments to typed functions, typos on known types, wrong JSDoc | Low: only mistakes, no missing types |
| Recommended | strict: true | Also untyped parameters, null and undefined, unknown properties on any object | Medium to high: every untyped exported function, every unchecked lookup |
| Strict | strict plus noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitReturns, noFallthroughCasesInSwitch | Also missing array and record entries, undefined in optional properties, missing returns, switch fall-through | High: index access appears everywhere |
Relaxed
With strict off, TypeScript still checks everything it has a type for. A function with a @param type rejects a wrong argument, and a misspelled method on a string is caught, because strings have a known type:
/**
* @param {number} cents
* @returns {string}
*/
function formatCents(cents) {
return "$" + (cents / 100).toFixed(2);
}
console.log(formatCents("4250"));error TS2345: Argument of type 'string' is not assignable to parameter of type 'number'.
console.log(formatCents(4250).toUppercase());error TS2551: Property 'toUppercase' does not exist on type 'string'. Did you mean 'toUpperCase'?jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": false,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} What it misses is everything without a type. An untyped parameter is any, so any argument passes. find() may return undefined, but without strictNullChecks that is not part of the type. And in a .js file, an object without a declared type is open-ended while noImplicitAny is off, so reading a property it never had is not an error either:
const orders = [{ id: "A-100", totalCents: 4250 }];
export function totalOf(id) {
const order = orders.find((o) => o.id === id);
return order.totalCent;
}
totalOf(100);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": false,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} That makes relaxed a starting point rather than a place to stay. It is the right first step for an existing codebase because it reports real mistakes and nothing else, so the first pull request is small. The migration guide starts there.
Recommended
Recommended is strict: true, which turns on noImplicitAny, strictNullChecks and the rest of the strict family. The same code now fails three times: the parameter needs a type, the result of find() may be missing, and the misspelled property is caught on the untyped object too:
const orders = [{ id: "A-100", totalCents: 4250 }];
export function totalOf(id) {error TS7006: Parameter 'id' implicitly has an 'any' type.
const order = orders.find((o) => o.id === id);
return order.totalCent;error TS18048: 'order' is possibly 'undefined'.error TS2551: Property 'totalCent' does not exist on type '{ id: string; totalCents: number; }'. Did you mean 'totalCents'?
}
totalOf(100);jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": []
},
"include": ["**/*.js"]
} Each of those is a bug in waiting, which is why this is the level for new code. In an existing codebase the cost is the number of untyped exported functions plus the number of places that use a value that can be missing. Callbacks typed from context (orders.find((o) => …)) need nothing, so the count is usually smaller than the number of functions.
Strict
The strict level adds four options that strict leaves off:
noUncheckedIndexedAccess:orders[0]andprices[key]may beundefined.exactOptionalPropertyTypes: an optional property may be missing, but may not be set toundefinedunless its type says so.noImplicitReturns: every path through a function that returns a value must return one.noFallthroughCasesInSwitch: a non-emptycasemust end withbreak,returnorthrow.
/** @typedef {{ id: string, note?: string }} Order */
/** @param {Order[]} orders */
export function firstId(orders) {
return orders[0].id;error TS2532: Object is possibly 'undefined'.
}
/** @type {Order} */
const draft = { id: "A-101", note: undefined };error TS2375: Type '{ id: string; note: undefined; }' is not assignable to type 'Order' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties.
Types of property 'note' are incompatible.
Type 'undefined' is not assignable to type 'string'.
/** @param {"paid" | "refunded" | "open"} status @returns {string | undefined} */error TS7030: Not all code paths return a value.
export function label(status) {
if (status === "paid") return "Paid";
if (status === "refunded") return "Refunded";
}
export { draft };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": [],
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["**/*.js"]
} /** @typedef {{ id: string, note?: string }} Order */
/** @param {Order[]} orders */
export function firstId(orders) {
return orders[0]?.id;
}
/** @type {Order} */
const draft = { id: "A-101" };
/**
* @param {"paid" | "refunded" | "open"} status
* @returns {string | undefined}
*/
export function label(status) {
if (status === "paid") return "Paid";
if (status === "refunded") return "Refunded";
return undefined;
}
export { draft };jsconfig.json for this example
{
"compilerOptions": {
"checkJs": true,
"noEmit": true,
"strict": true,
"target": "es2022",
"module": "nodenext",
"types": [],
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
},
"include": ["**/*.js"]
} The last two options are cheap: they report a handful of places, and each one is worth a look. exactOptionalPropertyTypes is moderate. noUncheckedIndexedAccess is the expensive one, because index access is everywhere and a loop with an index (for (let i = 0; …)) does not prove to TypeScript that items[i] exists. Prefer for...of, find() and destructuring, which need no extra checks, and keep the option for code where a missing entry is a real risk.
An order for an existing codebase
Turn options on one at a time, each in its own pull request, in roughly this order of cost:
checkJsfile by file or folder by folder, withstrict: false.noImplicitAny: types for parameters and for objects TypeScript cannot follow.strictNullChecks: handlenullandundefined.- The rest of
strict: usually few errors;strictPropertyInitializationis the one that can be large in class-heavy code, and you can leave it off ("strictPropertyInitialization": falsenext to"strict": true) for a while. noImplicitReturnsandnoFallthroughCasesInSwitch: small and quick.exactOptionalPropertyTypes, thennoUncheckedIndexedAccess, if you want them at all.
To size a step before you take it, run tsc with the option on the command line, which overrides the config for one run, and count the errors per file:
npx tsc -p jsconfig.json --noUncheckedIndexedAccess --pretty false | node count-errors.jscount-errors.js is the short script in the migration guide. A step that reports a few dozen errors is a pull request; one that reports thousands is a project, and it can wait.
Whatever you choose, write it down
Set every option you rely on in the config, even when it matches today’s default. TypeScript 6.0 turns strict on by default and changes other defaults (its release page lists them), and new TypeScript releases add checks to the strict family. A config that says "strict": true means the same thing on every version.
Every example on this page was checked with TypeScript 5.9.
Questions
Which level should a new project use?
Recommended, which is strict: true. It catches untyped parameters and missing null checks from the first line, when they cost nothing to fix. Add the strict level's options if you want index access checked too.
Is relaxed worth anything?
Yes, as a first step in an existing codebase. It still catches wrong arguments to typed functions and typos on known types, with almost no errors to fix up front. It misses untyped code, so do not stay there.
Will upgrading TypeScript change my level?
It can. New releases add checks to strict (strictBuiltinIteratorReturn in 5.6), and TypeScript 6.0 turns strict on by default, so a config without the option means something different there. Write strict explicitly either way.
Sources
- TSConfig Reference: strict · TypeScript
- TSConfig Reference: noUncheckedIndexedAccess · TypeScript
- TSConfig Reference: exactOptionalPropertyTypes · TypeScript
- Type Checking JavaScript Files · TypeScript handbook
Last reviewed 21 October 2026.