TypeScript Strict Mode
Modern TypeScript configuration with strict type checking for maximum safety and developer experience. This guide focuses on TypeScript 5.x best practices for 2025.
Core Expertise
What is Strict Mode?
-
Type safety: Catch more bugs at compile time
-
Better IDE experience: Improved autocomplete and refactoring
-
Maintainability: Self-documenting code with explicit types
-
Modern defaults: Align with current TypeScript best practices
Key Capabilities
-
Strict null checking
-
Strict function types
-
No implicit any
-
No unchecked indexed access
-
Proper module resolution
-
Modern import/export syntax enforcement
Recommended tsconfig.json (2025)
Minimal Production Setup
{ "compilerOptions": { // Type Checking "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true,
// Modules
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": false,
"verbatimModuleSyntax": true,
// Emit
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"removeComments": false,
"noEmit": false,
// Interop
"isolatedModules": true,
"allowJs": false,
"checkJs": false,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}, "include": ["src/**/*"], "exclude": ["node_modules", "dist", "build"] }
Vite/Bun Project (Bundler)
{ "compilerOptions": { // Type Checking - Maximum strictness "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "noFallthroughCasesInSwitch": true, "noUnusedLocals": true, "noUnusedParameters": true, "allowUnreachableCode": false, "allowUnusedLabels": false, "exactOptionalPropertyTypes": true,
// Modules (Bundler for Vite/Bun)
"module": "ESNext",
"moduleResolution": "Bundler",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": true,
"esModuleInterop": false,
"verbatimModuleSyntax": true,
// Emit (Vite handles bundling)
"target": "ES2022",
"lib": ["ES2023", "DOM", "DOM.Iterable"],
"jsx": "preserve", // Vite handles JSX
"noEmit": true, // Vite handles emit
// Interop
"isolatedModules": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true,
// Path Mapping
"baseUrl": ".",
"paths": {
"@/*": ["src/*"],
"@components/*": ["src/components/*"]
}
}, "include": ["src/**/*", "vite.config.ts"], "exclude": ["node_modules", "dist"] }
Node.js Library (NodeNext)
{ "compilerOptions": { // Type Checking "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true,
// Modules (NodeNext for Node.js)
"module": "NodeNext",
"moduleResolution": "NodeNext",
"resolveJsonModule": true,
"allowSyntheticDefaultImports": false,
"esModuleInterop": true,
"verbatimModuleSyntax": true,
// Emit (Node.js library)
"target": "ES2022",
"lib": ["ES2023"],
"outDir": "dist",
"declaration": true,
"declarationMap": true,
"sourceMap": true,
// Interop
"isolatedModules": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
}, "include": ["src/**/*"], "exclude": ["node_modules", "dist"] }
Strict Flags Explained
strict: true (Umbrella Flag)
Enables all strict type-checking options:
{ "strict": true // Equivalent to: // "noImplicitAny": true, // "strictNullChecks": true, // "strictFunctionTypes": true, // "strictBindCallApply": true, // "strictPropertyInitialization": true, // "noImplicitThis": true, // "alwaysStrict": true }
Always enable strict: true for new projects.
noImplicitAny
Disallows variables with implicit any type.
// ❌ Error with noImplicitAny function add(a, b) { // ^ Error: Parameter 'a' implicitly has an 'any' type return a + b; }
// ✅ Correct function add(a: number, b: number): number { return a + b; }
strictNullChecks
null and undefined are distinct from other types.
// ❌ Error with strictNullChecks const name: string = null; // ^^^^ Error: Type 'null' is not assignable to type 'string'
// ✅ Correct const name: string | null = null;
// ✅ Correct (handle null explicitly)
function greet(name: string | null): string {
if (name === null) {
return 'Hello, stranger!';
}
return Hello, ${name}!;
}
strictFunctionTypes
Function parameter types are checked contravariantly.
type Logger = (msg: string | number) => void;
// ❌ Error with strictFunctionTypes const log: Logger = (msg: string) => console.log(msg); // ^^^^^^^^^^^^^ Error: Type '(msg: string) => void' is not assignable
// ✅ Correct const log: Logger = (msg: string | number) => console.log(msg);
strictBindCallApply
Check that bind , call , and apply are invoked correctly.
function greet(name: string, age: number) {
console.log(${name} is ${age} years old);
}
// ❌ Error with strictBindCallApply greet.call(undefined, 'John', '25'); // ^^^^ Error: Argument of type 'string' is not assignable to 'number'
// ✅ Correct greet.call(undefined, 'John', 25);
strictPropertyInitialization
Class properties must be initialized.
class User { // ❌ Error with strictPropertyInitialization name: string; // ^^^^^^ Error: Property 'name' has no initializer
// ✅ Correct (initialize in constructor) name: string; constructor(name: string) { this.name = name; }
// ✅ Correct (default value) age: number = 0;
// ✅ Correct (definitely assigned assertion) id!: number; }
noImplicitThis
Disallow this with implicit any type.
// ❌ Error with noImplicitThis function logName() { console.log(this.name); // ^^^^ Error: 'this' implicitly has type 'any' }
// ✅ Correct (explicit this parameter) function logName(this: { name: string }) { console.log(this.name); }
Additional Strict Flags (Recommended for 2025)
noUncheckedIndexedAccess (Essential)
Index signatures return T | undefined instead of T .
// Without noUncheckedIndexedAccess const users: Record<string, User> = {}; const user = users['john']; // Type: User (wrong - might be undefined)
// ✅ With noUncheckedIndexedAccess const users: Record<string, User> = {}; const user = users['john']; // Type: User | undefined (correct)
if (user) { console.log(user.name); // Type narrowed to User }
Always enable this flag to prevent runtime errors.
noImplicitOverride
Require override keyword for overridden methods.
class Base { greet() { console.log('Hello'); } }
class Derived extends Base { // ❌ Error with noImplicitOverride greet() { // ^^^^^ Error: This member must have an 'override' modifier console.log('Hi'); }
// ✅ Correct override greet() { console.log('Hi'); } }
noPropertyAccessFromIndexSignature
Force bracket notation for index signatures.
type User = { name: string; [key: string]: string; };
const user: User = { name: 'John', email: 'john@example.com' };
// ❌ Error with noPropertyAccessFromIndexSignature console.log(user.email); // ^^^^^ Error: Property 'email' comes from index signature, use bracket notation
// ✅ Correct console.log(user['email']);
// ✅ Also correct (explicit property) console.log(user.name);
noFallthroughCasesInSwitch
Prevent fallthrough in switch statements.
function getDiscount(role: string): number { switch (role) { case 'admin': return 0.5; // ❌ Error with noFallthroughCasesInSwitch case 'user': // ^^^^ Error: Fallthrough case in switch console.log('User discount'); case 'guest': return 0.1; } }
// ✅ Correct function getDiscount(role: string): number { switch (role) { case 'admin': return 0.5; case 'user': console.log('User discount'); return 0.2; // Explicit return case 'guest': return 0.1; default: return 0; } }
exactOptionalPropertyTypes
Optional properties cannot be set to undefined explicitly.
type User = { name: string; age?: number; // Type: number | undefined (implicit) };
// ❌ Error with exactOptionalPropertyTypes const user: User = { name: 'John', age: undefined }; // ^^^^^^^^^ Error: Type 'undefined' is not assignable
// ✅ Correct (omit property) const user: User = { name: 'John' };
// ✅ Correct (assign a value) const user2: User = { name: 'Jane', age: 25 };
Module Resolution
moduleResolution: "Bundler" (Vite/Bun)
Use for projects with bundlers (Vite, Webpack, Bun).
{ "compilerOptions": { "module": "ESNext", "moduleResolution": "Bundler" } }
Features:
-
✅ No file extensions required in imports
-
✅ JSON imports without assertions
-
✅ Package.json exports field support
-
✅ Optimized for bundlers
// ✅ Works with Bundler import config from './config.json'; import { add } from './utils'; // No .ts extension
moduleResolution: "NodeNext" (Node.js)
Use for Node.js libraries and servers.
{ "compilerOptions": { "module": "NodeNext", "moduleResolution": "NodeNext" } }
Features:
-
✅ Respects package.json type: "module"
-
✅ Requires explicit .js extensions (even for .ts files)
-
✅ Supports conditional exports
-
✅ Aligned with Node.js ESM behavior
// ✅ Works with NodeNext (note .js extension) import { add } from './utils.js'; import config from './config.json' assert { type: 'json' };
package.json:
{ "type": "module", "exports": { ".": { "import": "./dist/index.js", "types": "./dist/index.d.ts" } } }
verbatimModuleSyntax (Recommended for 2025)
Prevents TypeScript from rewriting imports/exports.
{ "compilerOptions": { "verbatimModuleSyntax": true } }
Benefits:
-
✅ Explicit type imports required
-
✅ Prevents unintended side effects
-
✅ Better tree-shaking
-
✅ Aligns with future JavaScript standards
// ❌ Error with verbatimModuleSyntax import { User } from './types'; // ^^^^^^ Error: 'User' is a type and must be imported with 'import type'
// ✅ Correct import type { User } from './types';
// ✅ Correct (value import) import { fetchUser } from './api';
// ✅ Correct (mixed import) import { fetchUser, type User } from './api';
Replaces:
-
importsNotUsedAsValues (deprecated)
-
preserveValueImports (deprecated)
Path Mapping
{ "compilerOptions": { "baseUrl": ".", "paths": { "@/": ["src/"], "@components/": ["src/components/"], "@utils/": ["src/utils/"] } } }
Usage:
// Path aliases keep imports clean import { Button } from '@components/Button';
Vite/Bun configuration:
// vite.config.ts import { defineConfig } from 'vite'; import path from 'path';
export default defineConfig({ resolve: { alias: { '@': path.resolve(__dirname, './src'), '@components': path.resolve(__dirname, './src/components'), }, }, });
Migrating to Strict Mode
Step 1: Enable Gradually
{ "compilerOptions": { // Start with these "noImplicitAny": true, "strictNullChecks": false, // Enable later "strictFunctionTypes": true, "strictBindCallApply": true, "strictPropertyInitialization": false, // Enable later "noImplicitThis": true, "alwaysStrict": true } }
Step 2: Fix Errors Incrementally
Check errors without emitting
bunx tsc --noEmit
Fix files one at a time
bunx tsc --noEmit src/utils.ts
Step 3: Enable Remaining Flags
{ "compilerOptions": { "strict": true, // Enable all at once "noUncheckedIndexedAccess": true } }
Step 4: Optional Strict Flags
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true, "noImplicitOverride": true, "noPropertyAccessFromIndexSignature": true, "exactOptionalPropertyTypes": true } }
Common Patterns
Handling Null/Undefined
// Non-null assertion (use sparingly) const element = document.getElementById('app')!;
// Optional chaining const name = user?.profile?.name;
// Nullish coalescing const displayName = user?.name ?? 'Anonymous';
// Type guard function isUser(value: unknown): value is User { return typeof value === 'object' && value !== null && 'name' in value; }
Index Signature Safety
// ❌ Unsafe const value = obj[key]; // Type: T (wrong)
// ✅ Safe with noUncheckedIndexedAccess const value = obj[key]; // Type: T | undefined
if (value !== undefined) { // Type: T console.log(value); }
// ✅ Safe with assertion const value = obj[key]; if (value === undefined) throw new Error('Key not found'); // Type: T (narrowed)
Troubleshooting
Too Many Errors
Enable flags gradually
Start with noImplicitAny, then add others
Use @ts-expect-error for temporary fixes
// @ts-expect-error - TODO: Fix this type const value: string = null;
Library Types Missing
Install type definitions
bun add --dev @types/node @types/react
Skip type checking for libraries
{ "compilerOptions": { "skipLibCheck": true } }
Module Resolution Errors
// Bundler: No extension needed import { add } from './utils';
// NodeNext: Requires .js extension import { add } from './utils.js';
// Check moduleResolution setting bunx tsc --showConfig | grep moduleResolution
References
-
TypeScript Handbook: https://www.typescriptlang.org/docs/handbook/intro.html
-
TSConfig Reference: https://www.typescriptlang.org/tsconfig
-
Strict Mode Guide: https://www.typescriptlang.org/tsconfig#strict
-
Module Resolution: https://www.typescriptlang.org/docs/handbook/module-resolution.html
-
Best Practices: https://www.typescriptlang.org/docs/handbook/declaration-files/do-s-and-don-ts.html