Emmanuel Akinfulubi

tsconfig reference

There are well over a hundred compiler options. About twenty matter. This page is the twenty, grouped by whether you should turn them on.

The configuration this course is checked with

Printed from the object the build actually uses, so it cannot drift from what verified the samples.

The real compilerOptions behind every panel on this site

{
  "compilerOptions": {
    "strict": true,
    "target": "es2022",
    "module": "esnext",
    "moduleResolution": "bundler",
    "moduleDetection": "force",
    "lib": [
      "es2023",
      "dom"
    ],
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "noImplicitOverride": true,
    "noFallthroughCasesInSwitch": true,
    "skipLibCheck": true,
    "noEmitOnError": false,
    "removeComments": false
  }
}

The skipLibCheck is a concession to build speed. The moduleDetection: "force" is unusual and specific to how this site compiles hundreds of independent samples in one pass — it makes every file a module so the samples cannot collide. You would not normally need it, though it is harmless.

The strict family

What "strict": true turns on. All eight, individually nameable, and all worth having.
FlagWhat it catchesLesson
strictNullChecksnull and undefined become real types you must include on purpose. The highest-value check in the language.08
noImplicitAnyA parameter or variable the compiler cannot type. Forces annotations where inference has nothing to work from.01
strictFunctionTypesFunction parameters checked contravariantly, so a narrower callback cannot be substituted. Does not apply to method syntax.06
strictPropertyInitializationA class field declared and never assigned.11
strictBindCallApplybind, call and apply arguments are type-checked rather than accepted blindly.—
useUnknownInCatchVariablesA caught value is unknown rather than any, because throw accepts anything.12
noImplicitThisthis with an unclear type in a standalone function.06
alwaysStrictEmits "use strict". Modules are strict anyway, so this is mostly free.—

Worth adding on top

Not in strict, and worth turning on anyway. This course uses all four.
FlagWhat changesCostLesson
noUncheckedIndexedAccessarr[i] becomes T | undefined, which is the truth.Indexed loops need a check or a !. Iteration is unaffected, so preferring for...of avoids most of it.07
exactOptionalPropertyTypesx?: T means absent or T, and not “or explicitly undefined”.Object spreads and partial updates sometimes need restructuring.08
noImplicitOverrideoverride becomes mandatory when replacing an inherited member.Almost none. Adding the keyword is mechanical and most editors will do it.11
noFallthroughCasesInSwitchA case that falls into the next without breaking is an error.None, unless you deliberately use fallthrough, which is rare.18

Worth considering

Worth considering, and reasonable to leave off.
FlagWhat it doesWhy you might not
verbatimModuleSyntaxRequires explicit import type, so what you write is exactly what is emitted.Nothing much. Turn it on for a new project; it needs a mechanical pass on an existing one.
noUnusedLocalsErrors on unused variables.Actively hostile while you are mid-thought. An ESLint rule handles this better because it can be configured per case.
noUnusedParametersErrors on unused parameters.Same, plus it fights callbacks where you only need the second argument.
noPropertyAccessFromIndexSignatureForces obj['key'] rather than obj.key for index signatures.Verbose, and the distinction it draws rarely matters in application code.
isolatedModulesRejects code that cannot be compiled one file at a time.Required by most bundlers anyway, so usually already on. Bans const enum across files.
erasableSyntaxOnlyRejects the constructs that cannot be erased — enum, parameter properties, namespace.Only useful if you want files runnable directly by Node. Then it is exactly right.

Output and resolution

Where the output goes, and what it looks like.
FlagMeaningTypical value
targetWhat syntax is emitted. A low target makes the compiler rewrite classes, async and optional chaining into much longer equivalents."es2022" or later
moduleWhat import syntax is emitted."preserve" with a bundler, "nodenext" for Node
moduleResolutionHow import paths are resolved, and whether file extensions are required."bundler" or "nodenext"
libWhich built-ins the compiler believes exist. Ships no code.["es2023", "dom"]
outDirWhere emitted JavaScript is written."dist"
rootDirThe input root, which fixes the output folder structure."src"
noEmitCheck only, write nothing. Correct when a bundler builds your code.true for apps
declarationEmit .d.ts files. Required to publish a typed library.true for libraries
sourceMapEmit source maps, so stack traces point at your TypeScript.true
skipLibCheckDo not type-check .d.ts files in dependencies. A build-speed concession, and a widely accepted one.true
resolveJsonModuleAllows importing .json files with real types.true if you need it
allowJs / checkJsInclude .js files, and check them. The migration pair.see lesson 18

Flags that cost you something

Flags that make the compiler less useful. Know what they cost before using them.
FlagWhat it gives up
"strict": falseAlmost everything. Every check in the first table, including strictNullChecks. A codebase with this off has types that routinely lie.
suppressImplicitAnyIndexErrorsSilences TS7053 project-wide rather than fixing the one place. Deprecated, and for good reason.
ignoreDeprecationsPostpones dealing with a removal. Fine as a temporary measure with a note; a problem when it becomes permanent.
"allowUnreachableCode": trueDead code stops being reported. Rarely what you want.
"noImplicitAny": false aloneThe most common partial retreat. It sounds modest and means every un-annotated parameter silently becomes any, which spreads.

Starting points

An application built with a bundler or framework

  • "strict": true, "target": "es2022", "lib": ["es2023", "dom", "dom.iterable"]
  • "module": "preserve", "moduleResolution": "bundler", "noEmit": true
  • The four extras: noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitOverride, noFallthroughCasesInSwitch
  • Plus verbatimModuleSyntax, isolatedModules, skipLibCheck

Code Node runs directly

  • The same, but "module": "nodenext" and "moduleResolution": "nodenext"
  • Write .js extensions in your imports, even for .ts files
  • Add erasableSyntaxOnly so the compiler rejects what Node cannot strip — no enum, no parameter properties
  • @types/node as a dev dependency, or process and friends will be unknown

A library you intend to publish

  • Drop noEmit; add "declaration": true and "declarationMap": true
  • "outDir": "dist", "rootDir": "src", "sourceMap": true
  • Point types or exports in package.json at the emitted .d.ts
  • Consider turning skipLibCheck off here, since your published types are somebody else's dependency

An existing JavaScript project

  • "allowJs": true, "checkJs": false, "strict": true from the start
  • Then // @ts-check at the top of one file at a time, with JSDoc types
  • Then rename leaf-first, entry points last
  • Then one extra flag per pull request. Lesson 18 has the full sequence

Two commands worth knowing

npx tsc --noEmit checks everything and writes nothing. This is your continuous integration gate, and it is the most valuable line in a project's CI configuration — because most bundlers strip types without checking them, a green build otherwise tells you nothing about your types.

npx tsc --showConfig prints the fully resolved configuration, including everything inherited through extends and everything implied by strict. When you are not sure what is actually switched on, this answers it definitively rather than by reading.

Newsletter

New writing, when it lands.

Occasional notes on shipping software with AI agents. No schedule, no filler, unsubscribe in one click.