9.1 tsconfig.json Deep Dive — strict Mode and Project References
The 9 Sub-options of strict Mode
"strict": true is actually a bundle of 9 compiler options. Understanding each allows fine-grained control.
{
"compilerOptions": {
"strict": true
// Same as setting all 9 options below to true
}
}
1. strictNullChecks
Treats null and undefined as separate types. The most important option.
// strictNullChecks: false (dangerous)
let name: string = null; // ✅ Allowed — dangerous!
name.toUpperCase(); // Runtime error!
// strictNullChecks: true (safe)
let name: string = null; // ❌ null not assignable to string
let safeName: string | null = null; // ✅ Explicitly allows null
if (safeName) {
safeName.toUpperCase(); // ✅ Safe after null check
}
2. strictFunctionTypes
Function type parameters are checked contravariantly.
type Handler = (event: MouseEvent) => void;
// strictFunctionTypes: false
const handler: Handler = (event: Event) => { }; // Allowed (unsafe)
// strictFunctionTypes: true
const handler: Handler = (event: MouseEvent) => { }; // ✅
const handler2: Handler = (event: Event) => { }; // ❌ Must be more specific
3. strictBindCallApply
Strictly checks types for bind, call, and apply methods.
function greet(name: string, age: number): string {
return `${name} is ${age}`;
}
// strictBindCallApply: false
greet.call(null, 'Alice', '30'); // ❌ Allowed (string instead of number)
// strictBindCallApply: true
greet.call(null, 'Alice', 30); // ✅
greet.call(null, 'Alice', '30'); // ❌ Error: number expected, string given
4. strictPropertyInitialization
Checks that class properties are initialized in the constructor.
class User {
// strictPropertyInitialization: true
name: string; // ❌ Not initialized
email: string = ''; // ✅ Has default value
role: string; // ❌
constructor(name: string) {
this.name = name; // ✅ Initialized in constructor
// this.role missing — error
}
}
// Solutions
class UserFixed {
name: string;
email: string = '';
role!: string; // ! = guaranteed to be assigned later
optionalField?: string; // Optional property
constructor(name: string) {
this.name = name;
}
}
5. noImplicitAny
Errors on implicit any type usage.
// noImplicitAny: false
function process(data) { // data: any (implicit)
return data.toUpperCase(); // Potential runtime error
}
// noImplicitAny: true
function process(data: string): string { // ✅ Explicit type
return data.toUpperCase();
}
6. noImplicitThis
Errors when this type is implicitly any.
// noImplicitThis: true
function greet(this: { name: string }) {
console.log(`Hello, ${this.name}`); // ✅
}
const obj = { name: 'Alice', greet };
obj.greet(); // ✅
7. alwaysStrict
Adds "use strict" to the output JavaScript.
8. useUnknownInCatchVariables (TS 4.4+)
Changes the catch variable type from any to unknown.
try {
await fetchData();
} catch (error) {
// useUnknownInCatchVariables: false: error is any
// useUnknownInCatchVariables: true: error is unknown
if (error instanceof Error) {
console.error(error.message); // ✅ Safe after type guard
}
}
9. exactOptionalPropertyTypes (TS 4.4+)
Prevents explicitly assigning undefined to optional properties.
interface Config {
timeout?: number; // May be absent (different from undefined)
}
// exactOptionalPropertyTypes: true
const config: Config = { timeout: undefined }; // ❌ Cannot explicitly set undefined
const config2: Config = {}; // ✅ Property absent
const config3: Config = { timeout: 5000 }; // ✅
Common Additional Options
{
"compilerOptions": {
// Safety enhancements
"noUncheckedIndexedAccess": true, // Includes undefined for index access
"noImplicitOverride": true, // Requires explicit override keyword
"noPropertyAccessFromIndexSignature": true, // Index signatures require [] access
// Error detection
"noUnusedLocals": true, // Error on unused local variables
"noUnusedParameters": true, // Error on unused parameters
"noFallthroughCasesInSwitch": true, // Error on switch fallthrough
// Output control
"noEmitOnError": true, // No JS output when errors exist
"removeComments": true, // Remove comments
"sourceMap": true // Generate source maps
}
}
noUncheckedIndexedAccess Example
// noUncheckedIndexedAccess: true
const arr: string[] = ['a', 'b', 'c'];
const first = arr[0]; // Type: string | undefined (safe!)
if (first) {
console.log(first.toUpperCase()); // ✅
}
const obj: Record<string, number> = { a: 1 };
const val = obj['b']; // Type: number | undefined
Project References
A feature to speed up TypeScript compilation in large monorepos.
Basic Structure
workspace/
├── packages/
│ ├── shared/
│ │ ├── src/
│ │ └── tsconfig.json
│ ├── api/
│ │ ├── src/
│ │ └── tsconfig.json
│ └── web/
│ ├── src/
│ └── tsconfig.json
└── tsconfig.json
shared Package Configuration
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true, // Required for project references
"declaration": true, // Required: generates .d.ts files
"declarationMap": true, // Map for source navigation
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"include": ["src"]
}
api Package Configuration (references shared)
// packages/api/tsconfig.json
{
"compilerOptions": {
"composite": true,
"outDir": "./dist",
"rootDir": "./src",
"strict": true
},
"references": [
{ "path": "../shared" } // Reference shared package
],
"include": ["src"]
}
Root tsconfig.json
// tsconfig.json (root)
{
"files": [], // No files compiled at root
"references": [
{ "path": "packages/shared" },
{ "path": "packages/api" },
{ "path": "packages/web" }
]
}
Build Commands
# Build with project references (only rebuilds changed packages)
tsc --build # or tsc -b
tsc --build --clean # Delete build outputs
tsc --build --force # Force complete rebuild
tsc --build --watch # Watch mode
What composite Means
Setting composite: true:
- Forces
declaration: true - Enables
incremental: true - Forces
rootDirto tsconfig.json location - Generates
.tsbuildinfofile
incremental Builds
{
"compilerOptions": {
"incremental": true, // Enable incremental builds
"tsBuildInfoFile": ".tsbuildinfo" // Build cache file location
}
}
Only recompiles changed files, dramatically improving build speed.
Real-World tsconfig Composition Patterns
Separating Base Configuration
// tsconfig.base.json
{
"compilerOptions": {
"target": "ES2022",
"strict": true,
"esModuleInterop": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}
}
// tsconfig.json (app config)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler",
"jsx": "react-jsx",
"noEmit": true
},
"include": ["src"]
}
// tsconfig.node.json (build tools config)
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"module": "CommonJS",
"moduleResolution": "node"
},
"include": ["vite.config.ts", "scripts/**/*.ts"]
}
Pro Tips
Option recommendations table
| Option | New Projects | Legacy Migration |
|---|---|---|
strict | true | Enable gradually |
noUncheckedIndexedAccess | true | Add later |
noUnusedLocals | true | Optional |
exactOptionalPropertyTypes | true | Handle carefully |
skipLibCheck | true | true |