TypeScript Compiler Configuration
TypeScript compiler configuration — tsconfig.json structure, strict-mode flags and what each one catches, module resolution strategies (node/bundler/nodenext), and diagnosing/fixing slow type-checking. Use when bootstrapping a tsconfig.json for a new project, deciding which strictness flags to enable beyond `strict: true`, debugging a module-resolution error, or a `tsc --noEmit` run that has gotten too slow.
npx skills add github:pantheon-org/tekhne --skill development--typescript-advanced--compiler-configThe tsconfig.json options that decide what the compiler catches and how fast it catches it.
Mindset
Keep strict: true as the non-negotiable baseline and treat every additional strictness flag (noUncheckedIndexedAccess, exactOptionalPropertyTypes, noImplicitReturns) as a default-on decision, not an opt-in one — the cost of a flag is some extra narrowing at the call sites it affects, and the cost of not having it is a defect the compiler could have caught but didn’t. Never disable a strict-mode finding with @ts-ignore or strict: false; the defect it surfaced is still there, you’ve only removed the compiler’s ability to tell you about it. When type-checking is slow, profile before guessing — tsc --extendedDiagnostics and project references usually explain the slowdown faster than trimming include patterns at random.
When to Apply
Use this skill when:
- Bootstrapping a
tsconfig.jsonfor a new project - Deciding which strictness flags to enable beyond the
strictbundle - Debugging a module-resolution error (
Cannot find module, wrong resolution strategy for ESM/bundler setups) - Diagnosing and fixing slow
tsc --noEmitruns
Use When
- “What should my tsconfig.json look like for a new Node/ESM project?”
- “Which strictness flags should I turn on beyond
strict: true?” - “Why is TypeScript not resolving this module the way I expect?”
- “Why is type-checking so slow, and how do I speed it up?”
Scope
In Scope
tsconfig.jsonstructure and common option choices.- Strict-mode flags and what each one catches.
- Module resolution strategies (
node,bundler,node16/nodenext) and their trade-offs. - Diagnosing and improving type-checking performance.
Out of Scope
- Type-system design (generics, conditional types, unions) — see the sibling
typescript-type-systemskill. - Runtime validation and type guards — see the sibling
typescript-type-guardsandtypescript-practicesskills. - Build-pipeline or bundler configuration unrelated to the TypeScript compiler itself.
When NOT to Use
Do not use this skill for end-to-end test framework setup, CI pipeline configuration, or bundler configuration that isn’t a tsconfig.json compiler option. Do not use it as a substitute for running tsc --noEmit — this skill explains what a setting does, but the compiler is the source of truth on whether the project actually type-checks under it.
Quick Commands
Type Check
npx tsc --noEmit
Diagnose Slow Type-Checking
npx tsc --noEmit --extendedDiagnostics
Generate a tsconfig.json
npx tsc --init
Anti-Patterns
NEVER disable a strict-mode finding instead of fixing the underlying type model
WHY: the defect strict mode caught is still present in the code; @ts-ignore and strict: false only remove the compiler’s ability to report it.
BAD:
{ "compilerOptions": { "strict": false } }
GOOD:
{ "compilerOptions": { "strict": true, "noUncheckedIndexedAccess": true } }
NEVER set strict: true and stop there when the project can afford more
WHY: strict is a floor, not a ceiling — flags like noUncheckedIndexedAccess and exactOptionalPropertyTypes catch real classes of bugs strict alone leaves open.
BAD:
{ "compilerOptions": { "strict": true } }
GOOD:
{
"compilerOptions": {
"strict": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true
}
}
NEVER guess at a performance fix before profiling
WHY: trimming include patterns or splitting files at random can miss the actual cause (often deep conditional-type recursion or an oversized project graph) while adding maintenance overhead for no measured gain.
BAD: Randomly excluding directories from include because “it feels slow.”
GOOD: Run tsc --noEmit --extendedDiagnostics first, then act on what it reports (files checked, instantiation count, memory).
References
| File | Covers |
|---|---|
references/tsconfig.md |
tsconfig.json structure and options |
references/strict-mode.md |
Strict-mode flags and what each one catches |
references/module-resolution.md |
Module resolution strategies |
references/performance.md |
Diagnosing and improving type-checking speed |
Explicit NEVER/WHY/consequence triplets with side-by-side bad/good examples. Generic warnings score low — concrete failure modes with real impact (security, correctness, performance) score high.
Runtime validation via eval scenarios proving the skill actually changes agent behaviour. Requires evals/ directory, instructions.json, ≥3 scenarios, and ≥80% instruction coverage in summary.json.
Appropriate rigidity for the skill type: strict rules for critical/security skills, balanced steps for process skills, flexible options for tool skills. Mismatched calibration (too rigid or too loose) reduces score.
Expert-only knowledge the AI couldn't derive on its own — domain-specific patterns, production gotchas, decision frameworks, and non-obvious tool behaviour. High scores mean <5% redundancy with what the model already knows.
A clear philosophy plus numbered step-by-step workflows with explicit entry/exit points. Should answer: what is the core principle, how to execute it, and when (and when not) to apply it.
Rich activation keywords and trigger scenarios in the description so the skill fires when needed. Best descriptions read like an exhaustive list of the tasks and concepts that should invoke this skill.
Copy-paste-ready code, complete runnable examples, real file paths and commands. Pseudocode and abstract descriptions score low — concrete, executable content scores high.
SKILL.md as a <100-line navigation hub pointing to focused references/ files. Content should be lazy-loaded on demand, not front-loaded. Includes actionable 'When to Use' conditions per reference.
Proper frontmatter, single-task focus, activation keywords, and cross-harness portability. Skills must work across 40+ agentic harnesses without hardcoded paths or agent-specific references.
No eval scenarios available.
Module Resolution in TypeScript
Module Resolution Strategies
TypeScript uses different strategies to locate module files.
Node16 / NodeNext
For Node.js projects with ESM support:
{
"compilerOptions": {
"module": "NodeNext",
"moduleResolution": "NodeNext"
}
}
Behavior:
- Respects
package.json"type": "module"or"type": "commonjs" - Requires file extensions in imports:
.js,.mjs,.cjs - Follows Node.js ESM resolution algorithm
// Must include .js extension (not .ts)
import { helper } from "./utils.js";
import type { User } from "./types.js";
Bundler
For projects using bundlers (Vite, esbuild, Webpack):
{
"compilerOptions": {
"module": "ESNext",
"moduleResolution": "bundler"
}
}
Behavior:
- No file extensions required
- Supports package.json
"exports"field - Allows mixing ESM and CJS
- Optimized for build tools
// Extensions optional
import { helper } from "./utils";
import { Component } from "my-library";
Node (Classic)
Legacy Node.js resolution (not recommended for new projects):
{
"compilerOptions": {
"moduleResolution": "node"
}
}
Path Mapping
baseUrl and paths
Map module names to locations:
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@components/*": ["./components/*"],
"@utils/*": ["./utils/*"],
"@types": ["./types/index.ts"]
}
}
}
Usage:
import { Button } from "@components/Button";
import { formatDate } from "@utils/date";
import type { User } from "@types";
Important: Build tools need separate configuration
- Vite:
vite.config.tswithresolve.alias - Webpack:
webpack.config.jswithresolve.alias - Jest:
jest.config.jswithmoduleNameMapper
rootDirs
Treat multiple directories as a single root:
{
"compilerOptions": {
"rootDirs": ["src", "generated"]
}
}
Use case: Import from generated code as if it’s in source
Module Types
ES Modules (ESM)
Modern JavaScript modules:
// Export
export const value = 42;
export function helper() {}
export default class MyClass {}
// Import
import MyClass, { value, helper } from "./module";
import * as Module from "./module";
import type { MyType } from "./module";
CommonJS (CJS)
Node.js traditional modules:
// Export
exports.value = 42;
module.exports = { value: 42 };
// Import
const { value } = require("./module");
const Module = require("./module");
Type-Only Imports/Exports
Explicitly mark type imports for better tree-shaking:
// Type-only import
import type { User, Post } from "./types";
import { type User, fetchUser } from "./api"; // Mixed
// Type-only export
export type { User, Post };
export { type User, fetchUser }; // Mixed
Benefits:
- Clearer intent
- Better dead code elimination
- Required with
isolatedModulesandverbatimModuleSyntax
Package.json Exports
Modern packages use "exports" field:
{
"name": "my-library",
"type": "module",
"exports": {
".": {
"types": "./dist/index.d.ts",
"import": "./dist/index.js",
"require": "./dist/index.cjs"
},
"./utils": {
"types": "./dist/utils.d.ts",
"import": "./dist/utils.js"
}
}
}
Import paths:
import { main } from "my-library";
import { helper } from "my-library/utils";
Declaration Files
Generating Declarations
{
"compilerOptions": {
"declaration": true,
"declarationMap": true,
"emitDeclarationOnly": false
}
}
declaration: Generate.d.tsfilesdeclarationMap: Generate.d.ts.mapfor “Go to Definition”emitDeclarationOnly: Only emit declarations (useful with Babel)
Consuming Declarations
TypeScript looks for types in this order:
package.json"types"or"typings"fieldindex.d.tsin package root@types/<package>package
{
"name": "my-library",
"types": "./dist/index.d.ts",
"main": "./dist/index.js"
}
Monorepo Configuration
Project References
// packages/shared/tsconfig.json
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"outDir": "dist"
}
}
// packages/app/tsconfig.json
{
"compilerOptions": {
"composite": true
},
"references": [
{ "path": "../shared" }
]
}
// packages/app/src/index.ts
import { helper } from "@my-org/shared";
Benefits:
- Incremental builds
- Type checking across packages
- Enforced dependency graph
- Better IDE performance
Best Practices
- Use
bundlerfor bundled apps - Most flexible - Use
NodeNextfor Node.js libraries - Correct ESM/CJS handling - Always use path mapping - Cleaner imports
- Use type-only imports - Better tree-shaking
- Enable
isolatedModules- Ensures transpiler compatibility - Use project references - For monorepos
- Configure build tools - Match TypeScript paths
- Use
"exports"in package.json - For libraries
Common Pitfalls
- Missing file extensions with NodeNext - Required for ESM
- Path mapping not configured in build tool - Import errors at runtime
- Wrong moduleResolution - Causes resolution failures
- Not using project references - Slow monorepo builds
- Mixing ESM and CJS incorrectly - Runtime errors
- Using relative paths everywhere - Hard to refactor
TypeScript Compiler Performance
Performance Optimization Strategies
Incremental Compilation
Enable caching for faster rebuilds:
{
"compilerOptions": {
"incremental": true,
"tsBuildInfoFile": ".tsbuildinfo"
}
}
Benefits:
- Only recompile changed files
- 2-5x faster on subsequent builds
- Minimal disk usage (~few KB)
Project References
For monorepos and large projects:
{
"compilerOptions": {
"composite": true,
"declaration": true
},
"references": [
{ "path": "../shared" }
]
}
Benefits:
- Parallel compilation
- Incremental builds across projects
- Better IDE performance
- Enforced dependency boundaries
Build with:
tsc --build
tsc --build --watch # Watch mode
tsc --build --force # Clean rebuild
Skip Type Checking for Libraries
Skip checking node_modules:
{
"compilerOptions": {
"skipLibCheck": true
}
}
Trade-offs:
- Faster compilation (~30-50%)
- May miss errors in type definitions
- Safe if dependencies are well-typed
Reduce Checked Files
{
"include": ["src/**/*"],
"exclude": [
"node_modules",
"dist",
"**/*.test.ts",
"**/*.spec.ts"
]
}
Alternative: Separate test configuration
// tsconfig.build.json (for production)
{
"extends": "./tsconfig.json",
"exclude": ["**/*.test.ts", "**/*.spec.ts"]
}
Limit Type Instantiation Depth
Prevent runaway type computation:
{
"compilerOptions": {
"noErrorTruncation": false, // Truncate long error messages
"maxNodeModuleJsDepth": 0 // Don't infer types from .js files
}
}
Type System Performance
Avoid Deep Nesting
// Slow: Deep recursion
type DeepPartial<T> = {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P]>
: T[P];
};
// Limit recursion depth
type DeepPartial<T, Depth extends number = 5> = Depth extends 0
? T
: {
[P in keyof T]?: T[P] extends object
? DeepPartial<T[P], Prev[Depth]>
: T[P];
};
type Prev = [never, 0, 1, 2, 3, 4, 5];
Simplify Union Types
// Slow: Large union (100s of members)
type Color = "red" | "blue" | "green" | ... // 100+ values
// Fast: Use string index
type Color = string;
const COLORS = {
red: "#ff0000",
blue: "#0000ff",
// ...
} as const;
type Color = keyof typeof COLORS;
Cache Complex Types
// Slow: Recalculate every time
function process<T>(value: ComplexType<ComplexType<T>>) {}
// Fast: Cache intermediate type
type CachedComplex<T> = ComplexType<ComplexType<T>>;
function process<T>(value: CachedComplex<T>) {}
Use Type Aliases for Reuse
// Recalculates type each time
function a(value: { id: string; name: string; age: number }) {}
function b(value: { id: string; name: string; age: number }) {}
// Fast: Reuse type
type User = { id: string; name: string; age: number };
function a(value: User) {}
function b(value: User) {}
Build Tool Integration
Parallel Type Checking
Use fork-ts-checker-webpack-plugin or similar:
// webpack.config.js
const ForkTsCheckerWebpackPlugin = require('fork-ts-checker-webpack-plugin');
module.exports = {
plugins: [
new ForkTsCheckerWebpackPlugin({
async: true, // Non-blocking
typescript: {
configFile: 'tsconfig.json'
}
})
]
};
Use swc or esbuild for Transpilation
Only use TypeScript for type checking:
# Type check
tsc --noEmit
# Transpile (fast)
esbuild src/index.ts --bundle --outfile=dist/index.js
# or
swc src -d dist
Benefits:
- 10-100x faster transpilation
- TypeScript only for types
- Best of both worlds
Watch Mode Optimization
{
"compilerOptions": {
"assumeChangesOnlyAffectDirectDependencies": true
},
"watchOptions": {
"excludeDirectories": ["**/node_modules", "dist"],
"excludeFiles": ["**/*.test.ts"]
}
}
IDE Performance
Reduce IntelliSense Scope
{
"exclude": [
"node_modules",
"dist",
"build",
"coverage",
".cache"
]
}
Disable Automatic Type Acquisition
{
"compilerOptions": {
"types": [] // Only include explicit @types packages
}
}
Use @ts-nocheck for Problematic Files
// @ts-nocheck
// Large generated file or vendor code
Profiling and Diagnostics
Trace Type Checking
tsc --generateTrace trace
# Analyze with: https://ui.perfetto.dev/
Measure Compilation Time
tsc --diagnostics
# or more detail
tsc --extendedDiagnostics
Output includes:
- Files checked
- Time per phase
- Memory usage
- Cache hits
Find Slow Types
tsc --generateTrace trace --extendedDiagnostics
Look for:
- High instantiation counts
- Deep recursion
- Large union types
Best Practices
- Enable incremental compilation - 2-5x faster rebuilds
- Use project references - For monorepos
- Enable skipLibCheck - Safe for most projects
- Separate type checking from transpilation - Use esbuild/swc
- Limit recursion depth - Prevent runaway types
- Cache complex types - Reuse type aliases
- Exclude tests from production builds - Faster builds
- Use watch mode options - Fewer unnecessary checks
- Profile slow compilations - Use –generateTrace
- Keep dependencies updated - Better typed libraries
Performance Budget
Compilation times to aim for:
- Small project (<100 files): <2s
- Medium project (<500 files): <10s
- Large project (<2000 files): <30s
- Monorepo: <60s with project references
If slower:
- Enable incremental compilation
- Add project references
- Profile with –generateTrace
- Simplify complex types
- Reduce checked files
Common Bottlenecks
- No incremental compilation - Enable
incremental: true - Checking all node_modules - Enable
skipLibCheck: true - No project references - Add for monorepos
- Deep type recursion - Limit recursion depth
- Large unions - Use string index signatures
- Checking test files in production - Separate config
- Overly complex mapped types - Simplify or cache
TypeScript Strict Mode
Overview
"strict": true enables all strict type checking options. It’s the foundation of type-safe TypeScript.
Strict Mode Flags
Core Flags (enabled by strict)
noImplicitAny - Disallow implicit any types
// Error with noImplicitAny
function greet(name) { // Parameter 'name' implicitly has an 'any' type
console.log(`Hello, ${name}`);
}
// Fix: Add explicit type
function greet(name: string) {
console.log(`Hello, ${name}`);
}
strictNullChecks - null and undefined require explicit handling
// Error with strictNullChecks
function getLength(str: string | null) {
return str.length; // Object is possibly 'null'
}
// Fix: Check for null
function getLength(str: string | null) {
return str?.length ?? 0;
}
strictFunctionTypes - Stricter checking of function parameter types
type Handler = (value: string) => void;
type GenericHandler = (value: string | number) => void;
// Error: Type 'GenericHandler' is not assignable to 'Handler'
const handler: Handler = (value: string | number) => {};
strictBindCallApply - Check bind, call, and apply arguments
function greet(name: string, age: number) {
console.log(`${name} is ${age} years old`);
}
greet.call(undefined, "Alice", "30"); // Error: Argument of type 'string' not assignable to 'number'
strictPropertyInitialization - Ensure class properties are initialized
class User {
name: string; // Error: Property 'name' has no initializer
age: number = 0; // OK: Has initializer
email!: string; // OK: Definite assignment assertion
constructor() {
this.name = ""; // OK: Initialized in constructor
}
}
noImplicitThis - Disallow implicit any for this
// Error with noImplicitThis
const obj = {
name: "Alice",
greet() {
return function() {
console.log(this.name); // 'this' implicitly has type 'any'
};
}
};
// Fix: Use arrow function
const obj = {
name: "Alice",
greet() {
return () => {
console.log(this.name); // OK: Arrow function captures 'this'
};
}
};
alwaysStrict - Emit "use strict" in generated JavaScript
- Enables JavaScript strict mode
- Prevents common pitfalls (e.g., accidental globals)
Additional Recommended Flags
noUnusedLocals - Error on unused local variables
function calculate() {
const x = 10; // Error: 'x' is declared but never used
return 5;
}
noUnusedParameters - Error on unused function parameters
function greet(name: string, age: number) { // Error: 'age' is declared but never used
console.log(`Hello, ${name}`);
}
// Fix: Prefix with underscore
function greet(name: string, _age: number) {
console.log(`Hello, ${name}`);
}
noImplicitReturns - Error if not all code paths return a value
function getStatus(value: number): string {
if (value > 0) {
return "positive";
}
// Error: Not all code paths return a value
}
// Fix: Handle all paths
function getStatus(value: number): string {
if (value > 0) {
return "positive";
}
return "non-positive";
}
noFallthroughCasesInSwitch - Error on switch fallthrough
function getDay(day: number): string {
switch (day) {
case 0:
return "Sunday";
case 1: // Error: Fallthrough case in switch
console.log("Monday");
case 2:
return "Tuesday";
}
return "Unknown";
}
noUncheckedIndexedAccess - Index signatures return T | undefined
const colors: Record<string, string> = { red: "#ff0000" };
// Without noUncheckedIndexedAccess
const red = colors["red"]; // Type: string
// With noUncheckedIndexedAccess
const red = colors["red"]; // Type: string | undefined
if (red) {
console.log(red.toUpperCase()); // Safe
}
exactOptionalPropertyTypes - Distinguish undefined from missing
interface Config {
timeout?: number;
}
// Without exactOptionalPropertyTypes
const config: Config = { timeout: undefined }; // OK
// With exactOptionalPropertyTypes
const config: Config = { timeout: undefined }; // Error
const config: Config = {}; // OK
Migration Strategy
Enabling strict mode in an existing codebase:
- Enable flags incrementally
{
"compilerOptions": {
"noImplicitAny": true,
// Fix errors, then enable next flag
"strictNullChecks": true,
// Continue until all strict flags enabled
}
}
- Use
@ts-expect-errorfor temporary suppression
// @ts-expect-error TODO: Fix null handling
const value = maybeNull.property;
-
Fix by priority
- Start with noImplicitAny (easiest)
- Then strictNullChecks (most impactful)
- Finally other flags
-
Use
strictNullChecksmigration helpers
// Option 1: Non-null assertion (use sparingly)
const value = maybeNull!.property;
// Option 2: Optional chaining
const value = maybeNull?.property;
// Option 3: Nullish coalescing
const value = maybeNull ?? defaultValue;
Benefits
- Catch errors at compile time - Before runtime
- Better IDE support - More accurate autocomplete
- Self-documenting code - Types show intent
- Easier refactoring - Type errors guide changes
- Prevent null reference errors - strictNullChecks catches most
Best Practices
- Enable strict mode in new projects - Start with
"strict": true - Enable noUncheckedIndexedAccess - Not included in
strictbut highly recommended - Enable exactOptionalPropertyTypes - Clearer optional semantics
- Use
unknownoverany- Forces type checking - Avoid
!assertions - Prefer explicit null checks - Add JSDoc for complex types - Helps other developers
Common Pitfalls
- Using
as anyto bypass errors - Defeats purpose of strict mode - Overusing
!assertions - Hides potential null errors - Not enabling noUncheckedIndexedAccess - Still allows unsafe index access
- Disabling strict for quick fixes - Technical debt accumulates
TypeScript Compiler Configuration
tsconfig.json Structure
{
"compilerOptions": {
// Type Checking
"strict": true,
"noImplicitAny": true,
"strictNullChecks": true,
"strictFunctionTypes": true,
"strictBindCallApply": true,
"strictPropertyInitialization": true,
"noImplicitThis": true,
"alwaysStrict": true,
// Additional Checks
"noUnusedLocals": true,
"noUnusedParameters": true,
"noImplicitReturns": true,
"noFallthroughCasesInSwitch": true,
"noUncheckedIndexedAccess": true,
"exactOptionalPropertyTypes": true,
// Module Resolution
"moduleResolution": "bundler",
"module": "ESNext",
"target": "ES2022",
"lib": ["ES2022"],
// Emit
"declaration": true,
"declarationMap": true,
"sourceMap": true,
"outDir": "./dist",
"rootDir": "./src",
// Interop Constraints
"esModuleInterop": true,
"allowSyntheticDefaultImports": true,
"isolatedModules": true,
"verbatimModuleSyntax": true,
// Language and Environment
"experimentalDecorators": true,
"emitDecoratorMetadata": true,
// Projects
"composite": true,
"incremental": true
},
"include": ["src/**/*"],
"exclude": ["node_modules", "dist"]
}
Essential Compiler Options
Type Checking Options
strict - Enable all strict type checking options
- Best practice: Always enable in new projects
- Enables: noImplicitAny, strictNullChecks, strictFunctionTypes, etc.
noImplicitAny - Error on expressions and declarations with implied any
- Prevents accidental
anyusage - Forces explicit type annotations
strictNullChecks - null and undefined require explicit handling
- Prevents null reference errors
- Must check for nullish values before use
noUncheckedIndexedAccess - Index signatures return T | undefined
const config: Record<string, string> = {};
const value = config['key']; // string | undefined (not just string)
exactOptionalPropertyTypes - Distinguish between undefined and missing properties
interface User {
name?: string;
}
const user1: User = { name: undefined }; // Error with exactOptionalPropertyTypes
const user2: User = {}; // OK
Module Resolution
moduleResolution - Strategy for resolving module imports
"node16"/"nodenext"- For Node.js ESM/CJS hybrid projects"bundler"- For bundlers (Vite, esbuild, Webpack)
module - Output module system
"ESNext"- Modern ESM"CommonJS"- Node.js CJS"NodeNext"- Node.js with package.json"type": "module"
baseUrl and paths - Path mapping
{
"compilerOptions": {
"baseUrl": "./src",
"paths": {
"@/*": ["./*"],
"@components/*": ["./components/*"]
}
}
}
Emit Options
declaration - Generate .d.ts files
- Required for libraries
- Enables type checking for consumers
declarationMap - Generate source maps for .d.ts files
- Enables “Go to Definition” to source code
sourceMap - Generate .js.map files for debugging
outDir - Output directory for compiled files
rootDir - Root directory of source files (for maintaining structure)
Interop Constraints
isolatedModules - Ensure each file can be safely transpiled independently
- Required for Babel, esbuild, swc
- Prevents re-export of types without
typekeyword
esModuleInterop - Enable CommonJS/ESM interop
- Allows
import React from "react"instead ofimport * as React
verbatimModuleSyntax - Require import type for type-only imports
- More explicit than
isolatedModules - Forces clarity about runtime vs type imports
Project References
For monorepos and large projects:
{
"compilerOptions": {
"composite": true,
"declaration": true,
"declarationMap": true,
"incremental": true
},
"references": [
{ "path": "../shared" },
{ "path": "../utils" }
]
}
Benefits:
- Faster builds with caching
- Enforce dependency graph
- Better IDE performance
Best Practices
- Enable strict mode - Start with
"strict": true - Use noUncheckedIndexedAccess - Safer index access
- Enable exactOptionalPropertyTypes - Clearer optional semantics
- Use project references - For monorepos
- Match runtime target - Set
targetandlibappropriately - Use verbatimModuleSyntax - Explicit type imports
- Enable incremental - Faster subsequent builds
Common Pitfalls
- Disabling strict mode - Loses type safety benefits
- Wrong moduleResolution - Causes import resolution errors
- Missing isolatedModules - Breaks with modern bundlers
- Overly permissive include - Slows down compilation
- Not using project references - Slow monorepo builds