TypeScript · Lesson 10 of 12
tsconfig, Strict Mode and Tooling
Configure tsconfig.json: what strict mode enables, extra safety flags, module settings, running TypeScript with Node, and type-aware linting.
- Intermediate
- 16 min read
- 3 objectives
Before this lessonLesson 9: Typing Async Code, fetch and API Responses
What you will learn
- Explain what each strict flag catches
- Pick module and target settings for Node or a bundler
- Run and type-check TypeScript in development and CI
Your Progress
0 of 12 lessons 0%
- Lessons0 / 12
- Completed0
- Est. time left~ 3 hours
Create a free account to keep your progress on every device.
Tip: pressing Next marks this lesson complete automatically.
Two projects can both say "we use TypeScript" and get completely different levels of safety, because nearly all of TypeScript's checking is controlled by tsconfig.json. The earlier real-world lesson showed a starter config. This one explains what the important options do, so you can read any project's config and tighten it deliberately.
Creating a config
Run npx tsc --init in a project to generate a commented tsconfig.json. Recent versions produce a lean, modern default:
{
"compilerOptions": {
"module": "nodenext",
"target": "esnext",
"types": [],
"sourceMap": true,
"declaration": true,
"declarationMap": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"strict": true,
"jsx": "react-jsx",
"verbatimModuleSyntax": true,
"isolatedModules": true,
"noUncheckedSideEffectImports": true,
"moduleDetection": "force",
"skipLibCheck": true
}
}Since TypeScript 6.0, strict also defaults to true even when a config does not mention it. Older projects may still set it to false explicitly, which is the first thing to look for when a codebase feels unsafe.
What strict turns on
"strict": true is a bundle of flags. The two that matter most day to day:
- noImplicitAny: if TypeScript cannot infer a type, it errors instead of silently using
any. This is what forces you to type function parameters. - strictNullChecks:
nullandundefinedare separate types, sostring | undefinedmust be checked before calling string methods. Without it, the billion-dollar mistake is back. - strictFunctionTypes, strictBindCallApply, strictPropertyInitialization (class fields must be set), useUnknownInCatchVariables (
catch (e)isunknown), alwaysStrict and noImplicitThis.
// With strict: all three lines are errors
function total(items) { } // noImplicitAny: 'items' implicitly has an 'any' type
function shout(name: string | undefined) {
return name.toUpperCase(); // strictNullChecks: 'name' is possibly 'undefined'
}
class Job { id: string; } // strictPropertyInitialization: no initializerExtra safety beyond strict
A few valuable checks are not in the strict bundle because they are noisier. The default config above already enables the first two.
// noUncheckedIndexedAccess: indexing may return undefined
const scores: Record<string, number> = { ada: 10 };
const s = scores["linus"]; // number | undefined, not number
const first = [1, 2, 3][0]; // number | undefined
// exactOptionalPropertyTypes: "optional" is not the same as "may be undefined"
interface Prefs { theme?: "light" | "dark"; }
// const p: Prefs = { theme: undefined }; // Error under exactOptionalPropertyTypesWhy does noUncheckedIndexedAccess matter? Because plain JavaScript returns undefined for missing keys and out-of-range indexes, and without the flag TypeScript pretends it cannot happen:
const scores = { ada: 10 };
const list = [1, 2, 3];
console.log(scores["linus"]);
console.log(list[5]);
console.log(scores["linus"] + 1);undefined undefined NaN
Other flags worth turning on: noImplicitOverride (require override in subclasses), noFallthroughCasesInSwitch, and noImplicitReturns. noUnusedLocals and noUnusedParameters are useful but often left to the linter instead.
Module, target and resolution
These options decide what JavaScript is emitted and how imports are found. Pick based on who runs your code:
- Node.js app or library:
"module": "nodenext"(resolution follows automatically). Imports use.jsextensions and respect"type": "module"inpackage.json. - Bundled front end (Vite, Next.js, webpack):
"module": "esnext","moduleResolution": "bundler"and"noEmit": true, because the bundler produces the output andtsconly type-checks. - target controls which syntax is downlevelled. Modern runtimes handle
es2022or newer; old targets likees5are deprecated. - lib sets which built-in APIs exist (
DOMfor browsers). types limits global@typespackages; add"node"for Node projects.
Use extends to share a base config across packages in a monorepo, and include to say which files belong to the project:
{
"extends": "../../tsconfig.base.json",
"compilerOptions": {
"outDir": "dist",
"rootDir": "src",
"types": ["node"]
},
"include": ["src"],
"exclude": ["**/*.test.ts"]
}Running TypeScript
There are two separate jobs: running code, and type-checking it. Modern tools deliberately split them, because stripping types is fast while checking needs the whole program.
# Type-check only (run in CI and before commits)
npx tsc --noEmit
# Watch mode while developing
npx tsc --noEmit --watch
# Node.js 22.18+ and 24 run .ts files directly by stripping types
node src/server.ts
# tsx is a popular runner that also handles enums and path aliases
npx tsx watch src/server.tsNode's built-in type stripping does not check types at all; it just removes them. A file full of type errors runs happily. That is fine for fast feedback, as long as tsc --noEmit runs in CI. TypeScript 7, the native Go port of the compiler, makes that check roughly ten times faster on large projects.
Type-aware linting
The compiler checks types; a linter checks patterns. typescript-eslint adds rules that use type information to catch bugs the compiler allows, such as un-awaited promises and unnecessary conditions.
// eslint.config.js (flat config)
import tseslint from "typescript-eslint";
export default tseslint.config(
tseslint.configs.strictTypeChecked,
{
languageOptions: { parserOptions: { projectService: true } },
rules: {
"@typescript-eslint/no-floating-promises": "error",
"@typescript-eslint/no-explicit-any": "error",
},
},
);Biome and Oxlint are faster Rust-based alternatives that are adding type-aware rules too. Whatever you pick, run it in CI next to tsc --noEmit.
Recap
strictbundlesnoImplicitAny,strictNullChecksand more; it defaults to on since TypeScript 6.0.- Add
noUncheckedIndexedAccessandnoImplicitOverridefor extra safety. - Use
nodenextfor Node,bundlerresolution plusnoEmitfor Vite or Next.js. - Running (Node type stripping, tsx) and checking (
tsc --noEmit) are separate steps; CI must do the check. - Pair the compiler with type-aware linting to catch floating promises and stray
any.
// Write your solution here
Finished reading? Mark this lesson complete to track your progress.
