Guide · Adoption · 21 October 2026 · 9 min read

Choosing a strictness level for checked JavaScript

Relaxed, recommended or strict: what each level of jsconfig.json strictness catches, what it costs in an existing codebase, and an order to turn options on.

  • Examples checked with TypeScript 5.9

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.

LevelWhat it setsCatchesCost in an existing codebase
Relaxedstrict: falseWrong arguments to typed functions, typos on known types, wrong JSDocLow: only mistakes, no missing types
Recommendedstrict: trueAlso untyped parameters, null and undefined, unknown properties on any objectMedium to high: every untyped exported function, every unchecked lookup
Strictstrict plus noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitReturns, noFallthroughCasesInSwitchAlso missing array and record entries, undefined in optional properties, missing returns, switch fall-throughHigh: 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:

orders.js Error confirmed · TypeScript 5.9.3 · checked 21 October 2026
/**
 * @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'?
Relaxed: wrong arguments to typed functions and typos on known types are caught.
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:

orders.js TypeScript 5.9.3 · checked 21 October 2026
const orders = [{ id: "A-100", totalCents: 4250 }];

export function totalOf(id) {
  const order = orders.find((o) => o.id === id);
  return order.totalCent;
}

totalOf(100);
Relaxed: an untyped parameter, a missing null check and a typo on an untyped object pass.
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 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:

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:

report.js Error confirmed · TypeScript 5.9.3 · checked 21 October 2026
/** @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 };
Strict: index access, optional properties and missing returns.
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"]
}
report.js TypeScript 5.9.3 · checked 21 October 2026
/** @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 };
Strict: the same code, written to pass.
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:

  1. checkJs file by file or folder by folder, with strict: false.
  2. noImplicitAny: types for parameters and for objects TypeScript cannot follow.
  3. strictNullChecks: handle null and undefined.
  4. The rest of strict: usually few errors; strictPropertyInitialization is the one that can be large in class-heavy code, and you can leave it off ("strictPropertyInitialization": false next to "strict": true) for a while.
  5. noImplicitReturns and noFallthroughCasesInSwitch: small and quick.
  6. exactOptionalPropertyTypes, then noUncheckedIndexedAccess, 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.js

count-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

Last reviewed 21 October 2026.