pool.ts within the infrastructure folder does not require updating import statements across the codebase. It also improves IDE navigation and reduces merge conflicts in large teams.
2. Dead Code Elimination
Unused variables and parameters indicate incomplete refactoring, typos, or dead logic. Enforcing their removal keeps the codebase lean and prevents confusion about architectural intent.
Configuration:
{
"compilerOptions": {
"noUnusedLocals": true,
"noUnusedParameters": true
}
}
Implementation Example:
// Triggers error: 'retryCount' is declared but never read.
function executeRequest(url: string, retryCount: number, timeout: number): Promise<Response> {
const controller = new AbortController();
const timer = setTimeout(() => controller.abort(), timeout);
return fetch(url, { signal: controller.signal }).finally(() => clearTimeout(timer));
}
Rationale:
noUnusedLocals flags variables that are declared but never accessed. noUnusedParameters flags function arguments that are not used within the function body. This forces developers to either use the value, remove it, or explicitly mark it as unused (e.g., _retryCount), ensuring every piece of code serves a purpose.
3. Control Flow Integrity
JavaScript and TypeScript have syntax features that can lead to silent failures or unintended behavior. These settings enforce strict control flow rules and catch syntax anomalies.
Configuration:
{
"compilerOptions": {
"noFallthroughCasesInSwitch": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false
}
}
Implementation Examples:
Switch Fallthrough Prevention:
enum LogLevel { Debug, Info, Error }
function logMessage(level: LogLevel, msg: string) {
switch (level) {
case LogLevel.Debug:
console.debug(msg);
// Error: Fallthrough case in switch.
case LogLevel.Info:
console.info(msg);
break;
}
}
Fix: Group cases intentionally or add break statements.
switch (level) {
case LogLevel.Debug:
case LogLevel.Info:
console.log(msg);
break;
case LogLevel.Error:
console.error(msg);
break;
}
Unreachable Code Detection:
function validateInput(data: string): boolean {
if (!data) return false;
console.log('Validation passed'); // Error: Unreachable code detected.
return true;
}
Unused Label Detection:
function initConfig() {
const settings = {
port: 8080
};
// Error: Unused label. Intended as property, but creates JS label.
host: 'localhost';
}
Rationale:
noFallthroughCasesInSwitch prevents accidental execution of subsequent switch cases, a common source of logic bugs. allowUnreachableCode: false removes dead execution blocks that can never run, cleaning up the code. allowUnusedLabels: false catches a specific JavaScript quirk where a colon can be misinterpreted as a label instead of an object property, preventing subtle typos.
4. Runtime Type Fidelity
TypeScript's default type inference can diverge from runtime behavior, particularly with index access and optional properties. These settings align compile-time types with actual runtime possibilities.
Configuration:
{
"compilerOptions": {
"noUncheckedIndexAccess": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true
}
}
Implementation Examples:
Index Access Safety:
const activeSessions: string[] = ['sess_001', 'sess_002'];
// Without flag: Type is 'string'. Runtime crash if index out of bounds.
// With flag: Type is 'string | undefined'.
const currentSession = activeSessions[99];
// Error: Object is possibly 'undefined'.
console.log(currentSession.length);
// Fix: Handle undefined explicitly.
if (currentSession) {
console.log(currentSession.length);
}
Index Signature Property Access:
type FeatureFlags = {
version: string;
[flagName: string]: boolean;
};
const flags: FeatureFlags = { version: '1.0', darkMode: true };
// Error: Property access must use bracket notation for index signatures.
const isDark = flags.darkMode;
// Fix: Use bracket notation for dynamic keys.
const isDark = flags['darkMode'];
Exact Optional Properties:
interface UserPayload {
name?: string;
age?: number;
}
// Error: Type '{ name: undefined }' is not assignable to type 'UserPayload'.
// Missing property 'name' is distinct from property 'name' with value undefined.
const payload: UserPayload = { name: undefined };
// Fix: Omit the property or assign a valid value.
const validPayload: UserPayload = {};
Rationale:
noUncheckedIndexAccess appends undefined to index access results, forcing developers to handle missing elements and preventing runtime crashes. noPropertyAccessFromIndexSignature requires bracket notation for index signature properties, preventing typos on dot notation access for dynamic keys. exactOptionalPropertyTypes distinguishes between a missing property and a property set to undefined, which is critical for API contracts and configuration objects where omission has different semantics than an explicit undefined value.
5. Side-Effect Imports and Inheritance Integrity
Side-effect imports and class inheritance patterns require explicit verification to ensure correctness and maintainability.
Configuration:
{
"compilerOptions": {
"noUncheckedSideEffectImports": true,
"noImplicitOverride": true
}
}
Implementation Examples:
Side-Effect Verification:
// Error: Cannot find module './polyfills/global' or its corresponding type declarations.
// The compiler now verifies the file exists on disk.
import './polyfills/global';
Explicit Override:
abstract class DataAdapter {
abstract connect(): Promise<void>;
}
class CsvAdapter extends DataAdapter {
// Error: This member must have an 'override' modifier.
connect(): Promise<void> {
return Promise.resolve();
}
}
// Fix: Add override keyword.
class CsvAdapter extends DataAdapter {
override connect(): Promise<void> {
return Promise.resolve();
}
}
Rationale:
noUncheckedSideEffectImports ensures that imports without bindings (like polyfills or CSS) resolve to actual files, preventing silent deployment failures. noImplicitOverride requires the override keyword when a subclass method overrides a parent method, making inheritance intent explicit and catching errors when parent signatures change.
Pitfall Guide
-
The baseUrl Blindspot
- Explanation: Configuring
paths without baseUrl causes TypeScript to fail resolution. paths relies on baseUrl as the anchor point.
- Fix: Always set
baseUrl to "." or "./src" when using paths. Ensure your build tool (Webpack, Vite, etc.) is also configured to resolve these aliases.
-
Index Access Noise in Legacy Code
- Explanation: Enabling
noUncheckedIndexAccess on a large codebase can generate hundreds of errors, tempting teams to disable the flag.
- Fix: Do not disable the flag. Use optional chaining (
arr[idx]?.prop) or type guards to fix errors incrementally. This flag is critical for runtime safety.
-
Interface Parameter Pollution
- Explanation:
noUnusedParameters can flag parameters in interface implementations that are required by the contract but unused in the specific implementation.
- Fix: Prefix unused parameters with an underscore (e.g.,
_context) or configure argsIgnorePattern in tsconfig to allow underscore-prefixed parameters.
-
Optional Property API Mismatch
- Explanation:
exactOptionalPropertyTypes may break code that sends { key: undefined } to APIs expecting the key to be omitted.
- Fix: Refactor payload construction to omit keys with
undefined values. Use utility functions to clean objects before serialization.
-
Intentional Fallthrough Confusion
- Explanation:
noFallthroughCasesInSwitch blocks legitimate fallthrough logic if not structured correctly.
- Fix: Group cases without bodies for shared logic. If fallthrough is intentional, add a
// falls through comment to suppress the error and document intent.
-
Side-Effect Resolution in Monorepos
- Explanation:
noUncheckedSideEffectImports may fail in monorepo setups where side-effect files are in different packages or generated during build.
- Fix: Ensure TypeScript project references are configured correctly. For generated files, verify that the build order produces the files before type-checking runs.
-
Override Keyword Overhead
- Explanation:
noImplicitOverride adds boilerplate to class hierarchies, which some developers find verbose.
- Fix: Accept the verbosity as a trade-off for safety. The
override keyword prevents silent bugs when parent method signatures change, making it invaluable in large OOP codebases.
Production Bundle
Action Checklist
Decision Matrix
| Scenario | Recommended Approach | Why | Cost Impact |
|---|
| Greenfield Project | Enable all hardened flags immediately. | No legacy debt; establishes best practices from day one. | Low (Setup only) |
| Legacy Migration | Enable flags incrementally; use // @ts-expect-error sparingly. | Reduces risk of breaking changes; allows phased adoption. | Medium (Refactor effort) |
| High-Risk Runtime | Prioritize noUncheckedIndexAccess and noFallthroughCasesInSwitch. | Targets most common runtime crash vectors. | Low (Targeted fixes) |
| API-Heavy Service | Prioritize exactOptionalPropertyTypes. | Ensures payload contracts match runtime behavior. | Low (Payload adjustment) |
Configuration Template
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"strict": true,
"baseUrl": ".",
"paths": {
"@core/*": ["src/core/*"],
"@infra/*": ["src/infrastructure/*"],
"@ui/*": ["src/components/*"]
},
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true,
"allowUnreachableCode": false,
"allowUnusedLabels": false,
"noUncheckedIndexAccess": true,
"noPropertyAccessFromIndexSignature": true,
"exactOptionalPropertyTypes": true,
"noUncheckedSideEffectImports": true,
"noImplicitOverride": true,
"skipLibCheck": true,
"forceConsistentCasingInFileNames": true
},
"include": ["src/**/*.ts"],
"exclude": ["node_modules", "dist"]
}
Quick Start Guide
- Backup Configuration: Copy your existing
tsconfig.json to tsconfig.backup.json.
- Apply Flags: Add the hardened flags from the template to your
compilerOptions.
- Run Build: Execute
tsc --noEmit to identify errors introduced by new flags.
- Refactor: Fix errors incrementally, starting with index access and unused code.
- Verify: Run tests and linting to ensure no regressions. Commit changes.