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
exportsfield 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
.jsextensions (even for.tsfiles) - ✅ 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
typeimports 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