AniUI Academy

Configuring tsconfig.json and Strictness

What the strict flag actually turns on, the individual checks that make it up, and the other tsconfig options that affect real projects most: target, module, and skipLibCheck.

11 min read

tsconfig.json was introduced briefly, early in this track, as "the file that configures the compiler." This lesson is about the options that actually matter most in a real project — above all, what strict mode is really doing, since it's the single setting responsible for most of the safety this whole track has been teaching.

strict is a bundle, not one switch

"strict": true doesn't turn on one check called "strict mode" — it's shorthand for enabling a whole family of individually-named flags at once. The two most consequential are worth knowing by name:

  • noImplicitAny — an un-annotated parameter or variable the compiler can't infer a type for becomes an error, instead of silently falling back to any. This is what makes function greet(name) (no annotation) actually catch missing types, rather than quietly opting that parameter out of checking.
  • strictNullChecksnull and undefined become genuinely distinct from every other type, rather than being silently assignable everywhere. Without it, a function typed to return string could actually return null at runtime with no compile error at all, which defeats a large part of the point of typing it in the first place.

The rest of the family, briefly:

  • strictFunctionTypes — checks function parameter types more precisely (contravariantly, if you want the formal term) when comparing function types.
  • strictBindCallApply — checks the arguments passed to .call(), .apply(), and .bind() against the function's actual signature.
  • strictPropertyInitialization — a class property without a definite assignment (in the constructor or at declaration) is an error, catching a field that's typed as always present but might actually start as undefined.
  • noImplicitThis — an untyped this inside a function is an error rather than silently any.
  • alwaysStrict — emits "use strict" and parses in strict mode, same as plain JavaScript's own strict mode.
  • useUnknownInCatchVariable — a caught error (catch (error) {}) is typed unknown rather than any, which is why instanceof Error narrowing, covered earlier in this track, was necessary in the first place.
{
  "compilerOptions": {
    "strict": true
  }
}

Turning individual flags off after enabling strict is possible ("strictNullChecks": false alongside "strict": true), but doing this piecemeal in a real project quietly reintroduces exactly the bugs strict exists to prevent — it's rarely the right call outside a specific, deliberate migration.

target: what JavaScript version comes out

target controls how modern the emitted JavaScript is allowed to be — newer syntax gets rewritten into older equivalents below the chosen target:

{
  "compilerOptions": {
    "target": "es2020"
  }
}

A low target (es5) maximizes compatibility with very old runtimes at the cost of larger, less efficient output (classes rewritten as functions, for instance); a modern target (es2020 or later) keeps most contemporary syntax as-is, assuming a reasonably current runtime. For most projects targeting current browsers or Node.js, a recent target is the right default.

module and moduleResolution

module controls what module syntax is emitted (commonjs's require, or ES modules' import/export), and moduleResolution controls how the compiler looks up what a given import path actually resolves to. Modern projects using a bundler typically use:

{
  "compilerOptions": {
    "module": "esnext",
    "moduleResolution": "bundler"
  }
}

This tells TypeScript to leave import/export syntax untouched (the bundler handles the actual module format) and to resolve imports the way modern bundlers do, rather than following Node.js's older CommonJS resolution rules.

skipLibCheck: a pragmatic trade-off

skipLibCheck skips type-checking inside .d.ts declaration files — overwhelmingly, ones from node_modules, not your own source:

{
  "compilerOptions": {
    "skipLibCheck": true
  }
}

Without it, tsc type-checks every declaration file pulled in by every dependency, including ones you have no control over and can't fix if they're wrong — which is also simply slow on a project with a realistic number of dependencies. skipLibCheck trades a small amount of theoretical safety (an actually broken third-party .d.ts might go unnoticed) for meaningfully faster builds, and is enabled in most real-world projects for exactly that reason.

A sensible modern baseline

{
  "compilerOptions": {
    "target": "es2020",
    "module": "esnext",
    "moduleResolution": "bundler",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  }
}

esModuleInterop and forceConsistentCasingInFileNames are two further options worth this default's inclusion: the first smooths over interoperability between CommonJS and ES module import styles; the second catches a file imported with inconsistent casing (./Utils vs ./utils), which works on case-insensitive filesystems (macOS, Windows) but silently breaks on case-sensitive ones (Linux, and most CI and production servers).

Try it yourself

This playground already runs with strict-equivalent checking. Notice the difference an un-annotated parameter would make.

Try it yourself
Loading playground...

What to remember

  • strict bundles several individually-named checks; noImplicitAny and strictNullChecks are the two doing the most work, and this whole track assumes both are on.
  • target controls the JavaScript version emitted; module/moduleResolution control the module syntax and how imports resolve.
  • skipLibCheck skips checking dependency .d.ts files, trading a small amount of safety for meaningfully faster builds — standard in most real projects.
  • Disabling individual strict-family flags after turning strict on is possible, but usually reintroduces exactly the bugs it exists to catch.

Check yourself

4 questions · pass 3/4 to unlock as const and satisfies

up to 50
  1. 1.What is the practical effect of setting "strict": true in tsconfig.json?

  2. 2.What does noImplicitAny specifically catch?

  3. 3.What does strictNullChecks change about how null and undefined behave?

  4. 4.What does skipLibCheck do, and why do many real projects enable it?

4 left to answer