Error Handling and Debugging Techniques
1. TypeScript Compiler Error Messages
| Error Code | Message Pattern | Common Cause | Solution |
|---|---|---|---|
| TS2322 | Type 'X' is not assignable to type 'Y' | Type mismatch in assignment | Check types, add type assertion, or fix source type |
| TS2345 | Argument of type 'X' is not assignable to parameter of type 'Y' | Wrong function argument type | Pass correct type or update function signature |
| TS2339 | Property 'X' does not exist on type 'Y' | Accessing non-existent property | Add property to type, use optional chaining, or type guard |
| TS2571 | Object is of type 'unknown' | Using unknown type without narrowing | Add type guard or type assertion |
| TS2769 | No overload matches this call | Function call doesn't match any signature | Check argument types and count |
| TS2740 | Type 'X' is missing properties from type 'Y' | Incomplete object initialization | Add missing properties or use Partial<T> |
| TS7006 | Parameter 'X' implicitly has an 'any' type | Missing type annotation with noImplicitAny | Add explicit type annotation |
| TS2304 | Cannot find name 'X' | Undefined variable/type, missing import | Import symbol or install @types package |
Example: Common compiler errors and fixes
// TS2322 - Type mismatch
// ❌ Error
const num: number = '42'; // Type 'string' is not assignable to type 'number'
// ✅ Fix
const num: number = 42;
// TS2345 - Wrong argument type
// ❌ Error
function greet(name: string) { }
greet(42); // Argument of type 'number' is not assignable to parameter of type 'string'
// ✅ Fix
greet('Alice');
// TS2339 - Property doesn't exist
// ❌ Error
interface User { name: string; }
const user: User = { name: 'Alice' };
console.log(user.age); // Property 'age' does not exist on type 'User'
// ✅ Fix 1 - Add to interface
interface User { name: string; age?: number; }
// ✅ Fix 2 - Type guard
if ('age' in user) {
console.log(user.age);
}
// TS2571 - Unknown type
// ❌ Error
function process(data: unknown) {
console.log(data.name); // Object is of type 'unknown'
}
// ✅ Fix - Type guard
function process(data: unknown) {
if (typeof data === 'object' && data !== null && 'name' in data) {
console.log((data as { name: string }).name);
}
}
// TS2769 - No matching overload
// ❌ Error
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any) { return a + b; }
add(1, '2'); // No overload matches this call
// ✅ Fix - Use compatible types
add(1, 2); // OK
add('1', '2'); // OK
// TS2740 - Missing properties
// ❌ Error
interface Config {
host: string;
port: number;
secure: boolean;
}
const config: Config = { host: 'localhost' }; // Missing 'port' and 'secure'
// ✅ Fix - Provide all properties
const config: Config = { host: 'localhost', port: 3000, secure: true };
// TS7006 - Implicit any
// ❌ Error (with noImplicitAny)
function process(data) { // Parameter 'data' implicitly has an 'any' type
console.log(data);
}
// ✅ Fix - Add type annotation
function process(data: unknown) {
console.log(data);
}
Example: Understanding error messages
// Complex error message breakdown
// Error:
// Type '{ name: string; age: number; }' is not assignable to type 'User'.
// Property 'email' is missing in type '{ name: string; age: number; }'
// but required in type 'User'.
interface User {
name: string;
age: number;
email: string; // Required property
}
const user: User = { // ❌ Error
name: 'Alice',
age: 30
// Missing: email
};
// Reading the error:
// 1. Main message: type mismatch
// 2. Specific issue: missing 'email' property
// 3. Location: 'email' is required in User interface
// Error with complex type
type Handler<T> = (data: T) => void;
const handler: Handler<string> = (data: number) => { // ❌ Error
console.log(data);
};
// Error: Type '(data: number) => void' is not assignable to type 'Handler<string>'.
// Types of parameters 'data' and 'data' are incompatible.
// Type 'string' is not assignable to type 'number'.
// Understanding:
// 1. Handler expects string parameter
// 2. Function defined with number parameter
// 3. Parameter types are incompatible
// Use --explainFiles flag for more details
tsc --explainFiles
// Use --noErrorTruncation for full error messages
tsc --noErrorTruncation
2. Type Error Diagnosis and Resolution
| Technique | Method | When to Use | Benefit |
|---|---|---|---|
| Hover for Type Info | Hover over symbol in IDE | Understand inferred types, signatures | See what TypeScript thinks the type is |
| Go to Definition | F12 on symbol | Find type definitions, interfaces | Understand structure and constraints |
| Type Annotations | Add explicit types to narrow inference | Disambiguate complex types | Control type inference, catch errors early |
| @ts-expect-error | Suppress error, mark as intentional | Known type issue, waiting for fix | Document known limitations |
| @ts-ignore | Suppress next line error (avoid if possible) | Last resort for unfixable issues | Bypass type checker (dangerous) |
| Type Assertion | Use as Type to override inference |
You know more than compiler | Tell compiler the actual type |
| Isolate Problem | Extract to separate file/function | Complex type errors | Reduce complexity, test in isolation |
| --noEmit | Type check without generating output | Fast error checking | Quick validation in CI/CD |
Example: Diagnosis techniques
// 1. Use type annotations to narrow inference
// Problem: Generic type too broad
const data = []; // Inferred as: never[]
data.push(1); // ❌ Error
// Solution: Add type annotation
const data: number[] = [];
data.push(1); // ✅ OK
// 2. Break down complex types
// Problem: Complex union causing issues
type ComplexType =
| { type: 'a'; value: string }
| { type: 'b'; value: number }
| { type: 'c'; value: boolean };
function handle(data: ComplexType) {
// Hard to debug all at once
}
// Solution: Test each branch separately
function handleA(data: Extract<ComplexType, { type: 'a' }>) {
console.log(data.value.toUpperCase()); // string methods available
}
// 3. Use @ts-expect-error for known issues
// Third-party library has incorrect types
// @ts-expect-error - Library types are wrong, will be fixed in v2.0
const result = someLibraryFunction('param');
// 4. Type assertions when you know better than compiler
const canvas = document.getElementById('canvas'); // HTMLElement | null
// You know it's a canvas element
const ctx = (canvas as HTMLCanvasElement).getContext('2d');
// 5. Diagnostic types to inspect complex types
type Inspect<T> = { [K in keyof T]: T[K] };
type Complex = Partial<Omit<User, 'id'>> & { active: boolean };
type Simplified = Inspect<Complex>; // Hover to see resolved type
// 6. Check assignability with conditional types
type IsAssignable<T, U> = T extends U ? true : false;
type Test1 = IsAssignable<string, string | number>; // true
type Test2 = IsAssignable<number, string>; // false
// 7. Use --traceResolution to debug module resolution
// tsc --traceResolution | grep "my-module"
// 8. Enable verbose errors in tsconfig
{
"compilerOptions": {
"noErrorTruncation": true // Show full error messages
}
}
Example: Debugging type inference
// Helper type to see inferred types
type Debug<T> = { [K in keyof T]: T[K] } & {};
// Example: Understanding generic inference
function map<T, U>(array: T[], fn: (item: T) => U): U[] {
return array.map(fn);
}
// Hover over each to see inferred types
const numbers = [1, 2, 3];
const strings = map(numbers, n => n.toString());
// T = number, U = string, result = string[]
// Use satisfies to validate without changing type
const config = {
host: 'localhost',
port: 3000
} satisfies Record<string, string | number>;
config.host; // Still inferred as 'localhost' literal type
// Debugging discriminated unions
type Result<T> =
| { success: true; data: T }
| { success: false; error: string };
function unwrap<T>(result: Result<T>): T {
if (result.success) {
// Hover over result.data to see narrowing
return result.data; // TypeScript knows: { success: true; data: T }
}
throw new Error(result.error); // TypeScript knows: { success: false; error: string }
}
// Debug utility to force type errors (see full type)
type ForceError<T> = T extends never ? T : never;
// Uncomment to see full type structure
// type DebugMyType = ForceError<ComplexType>;
// Check if types match
type Equals<X, Y> =
(<T>() => T extends X ? 1 : 2) extends
(<T>() => T extends Y ? 1 : 2) ? true : false;
type Test = Equals<{ a: number }, { a: number }>; // true
3. Source Map Configuration and Debugging
| Option | Configuration | Purpose | Use Case |
|---|---|---|---|
| sourceMap | "sourceMap": true |
Generate .js.map files - map compiled to source | Debug TypeScript in browser/Node.js |
| inlineSourceMap | "inlineSourceMap": true |
Embed source maps in .js files | Single-file distribution |
| inlineSources | "inlineSources": true |
Include TypeScript source in map | No separate .ts files needed for debugging |
| declarationMap | "declarationMap": true |
Generate .d.ts.map for declaration files | Go to source from .d.ts files |
| sourceRoot | "sourceRoot": "./src" |
Specify root for source files in maps | Correct source paths in debugger |
| mapRoot | "mapRoot": "./maps" |
Specify location of map files | Separate map file deployment |
| VS Code Debugging | launch.json configuration | Debug TypeScript directly in VS Code | Breakpoints in .ts files |
Example: Source map configuration
// tsconfig.json for debugging
{
"compilerOptions": {
// Source map generation
"sourceMap": true, // Generate .js.map files
"declarationMap": true, // Generate .d.ts.map files
// Inline options (for bundlers)
"inlineSourceMap": false, // Don't inline (separate files)
"inlineSources": false, // Don't embed TypeScript source
// Source paths
"sourceRoot": "", // Relative to source map
"mapRoot": "", // Where to find maps
// Output settings
"outDir": "./dist",
"rootDir": "./src",
// Keep for better debugging
"removeComments": false,
"preserveConstEnums": true
}
}
// For production (smaller bundles)
{
"compilerOptions": {
"sourceMap": false, // No source maps
"removeComments": true, // Strip comments
"declaration": false // No .d.ts files
}
}
// For development (best debugging)
{
"compilerOptions": {
"sourceMap": true,
"declarationMap": true,
"inlineSources": true, // Embed sources (easier debugging)
"removeComments": false
}
}
// Webpack source map options
module.exports = {
mode: 'development',
devtool: 'inline-source-map', // Best for development
// devtool: 'source-map', // Separate files (production)
// devtool: 'eval-source-map', // Fast rebuild (development)
// devtool: 'cheap-source-map', // Faster, less accurate
};
Example: VS Code debugging configuration
// .vscode/launch.json - Node.js debugging
{
"version": "0.2.0",
"configurations": [
{
"type": "node",
"request": "launch",
"name": "Debug TypeScript",
"program": "${workspaceFolder}/src/index.ts",
"preLaunchTask": "tsc: build - tsconfig.json",
"outFiles": ["${workspaceFolder}/dist/**/*.js"],
"sourceMaps": true,
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",
"name": "Debug with ts-node",
"runtimeArgs": ["-r", "ts-node/register"],
"args": ["${workspaceFolder}/src/index.ts"],
"skipFiles": ["<node_internals>/**"],
"console": "integratedTerminal"
},
{
"type": "node",
"request": "launch",
"name": "Debug Jest Tests",
"program": "${workspaceFolder}/node_modules/.bin/jest",
"args": ["--runInBand", "--no-cache"],
"console": "integratedTerminal",
"internalConsoleOptions": "neverOpen"
}
]
}
// .vscode/launch.json - Chrome debugging (React, etc.)
{
"version": "0.2.0",
"configurations": [
{
"type": "chrome",
"request": "launch",
"name": "Launch Chrome",
"url": "http://localhost:3000",
"webRoot": "${workspaceFolder}/src",
"sourceMapPathOverrides": {
"webpack:///src/*": "${webRoot}/*"
}
}
]
}
// .vscode/tasks.json - Build before debugging
{
"version": "2.0.0",
"tasks": [
{
"type": "typescript",
"tsconfig": "tsconfig.json",
"option": "watch",
"problemMatcher": ["$tsc-watch"],
"group": "build",
"label": "tsc: watch - tsconfig.json"
}
]
}
4. Runtime Error Handling with Type Safety
| Pattern | Implementation | Type Safety | Use Case |
|---|---|---|---|
| Result Type | Discriminated union for success/failure | Force error handling at compile time | Avoid throwing exceptions, functional approach |
| Try-Catch with Typing | Type the error with type guards | Handle specific error types safely | API calls, file operations |
| Custom Error Classes | Extend Error with additional properties | Discriminate error types | Domain-specific errors |
| Error Boundary Pattern | Wrap risky operations with error handling | Isolate errors, prevent propagation | React error boundaries, middleware |
| Validation Libraries | Zod, io-ts, Yup for runtime validation | Runtime and compile-time safety | API responses, user input |
| Never Type | Functions that never return (throw/exit) | Document unreachable code | Error handlers, infinite loops |
| Option/Maybe Type | Represent nullable values explicitly | Force null checking | Functional programming patterns |
Example: Result type pattern
// Result type for error handling without exceptions
type Result<T, E = Error> =
| { success: true; value: T }
| { success: false; error: E };
// Usage
function divide(a: number, b: number): Result<number, string> {
if (b === 0) {
return { success: false, error: 'Division by zero' };
}
return { success: true, value: a / b };
}
// Consumer must handle both cases
const result = divide(10, 2);
if (result.success) {
console.log(result.value); // TypeScript knows: value exists
} else {
console.error(result.error); // TypeScript knows: error exists
}
// Helper functions
function ok<T>(value: T): Result<T, never> {
return { success: true, value };
}
function err<E>(error: E): Result<never, E> {
return { success: false, error };
}
// Async version
async function fetchUser(id: string): Promise<Result<User, string>> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
return err(`Failed to fetch user: ${response.status}`);
}
const user = await response.json();
return ok(user);
} catch (error) {
return err(`Network error: ${error}`);
}
}
// Chain results
function map<T, U, E>(
result: Result<T, E>,
fn: (value: T) => U
): Result<U, E> {
return result.success ? ok(fn(result.value)) : result;
}
const doubled = map(divide(10, 2), n => n * 2);
Example: Custom error classes and type guards
// Custom error hierarchy
class AppError extends Error {
constructor(message: string) {
super(message);
this.name = this.constructor.name;
Object.setPrototypeOf(this, new.target.prototype);
}
}
class ValidationError extends AppError {
constructor(
message: string,
public field: string,
public value: unknown
) {
super(message);
}
}
class NetworkError extends AppError {
constructor(
message: string,
public statusCode: number,
public url: string
) {
super(message);
}
}
class NotFoundError extends AppError {
constructor(
public resource: string,
public id: string
) {
super(`${resource} with id ${id} not found`);
}
}
// Type guards for error handling
function isValidationError(error: unknown): error is ValidationError {
return error instanceof ValidationError;
}
function isNetworkError(error: unknown): error is NetworkError {
return error instanceof NetworkError;
}
function isNotFoundError(error: unknown): error is NotFoundError {
return error instanceof NotFoundError;
}
// Usage with proper typing
async function getUserData(id: string): Promise<User> {
try {
const response = await fetch(`/api/users/${id}`);
if (!response.ok) {
if (response.status === 404) {
throw new NotFoundError('User', id);
}
throw new NetworkError(
'Failed to fetch user',
response.status,
response.url
);
}
const data = await response.json();
if (!data.email) {
throw new ValidationError(
'Email is required',
'email',
data.email
);
}
return data;
} catch (error) {
// Type-safe error handling
if (isValidationError(error)) {
console.error(`Validation failed for ${error.field}:`, error.value);
} else if (isNetworkError(error)) {
console.error(`Network error ${error.statusCode} at ${error.url}`);
} else if (isNotFoundError(error)) {
console.error(`${error.resource} not found: ${error.id}`);
} else if (error instanceof Error) {
console.error('Unexpected error:', error.message);
} else {
console.error('Unknown error:', error);
}
throw error;
}
}
// Generic error handler
function handleError(error: unknown): never {
if (error instanceof AppError) {
console.error(`[${error.name}] ${error.message}`);
} else if (error instanceof Error) {
console.error(error.message);
} else {
console.error('Unknown error:', error);
}
process.exit(1);
}
5. Type-only Imports for Debugging
| Feature | Syntax | Purpose | Benefit |
|---|---|---|---|
| Type-only Import | import type { T } from './module' |
Import only for type checking - removed at runtime | No runtime overhead, clear intent |
| Inline Type Import | import { type T, value } from './module' |
Mix type and value imports | Cleaner syntax, single import statement |
| Type-only Export | export type { T } from './module' |
Re-export only types | Clear API boundaries |
| importsNotUsedAsValues | "verbatimModuleSyntax": true |
Preserve or remove unused imports | Smaller bundles, clearer semantics |
| isolatedModules | "isolatedModules": true |
Ensure each file can transpile independently | Required for esbuild, SWC |
| preserveValueImports | "preserveValueImports": true |
Keep imports even if only for types | Side-effect preservation |
Example: Type-only imports
// types.ts
export interface User {
id: string;
name: string;
}
export class UserService {
getUser(id: string): User {
// ...
}
}
export const API_URL = 'https://api.example.com';
// main.ts - Different import styles
// 1. Type-only import (TS 3.8+)
import type { User } from './types';
// Removed at runtime, only for type checking
// 2. Mixed import (TS 4.5+ inline style)
import { type User, UserService, API_URL } from './types';
// User is type-only, UserService and API_URL are values
// 3. Separate imports (clear separation)
import type { User } from './types';
import { UserService, API_URL } from './types';
// 4. Type-only export
// api.ts
export type { User } from './types'; // Re-export only type
export { UserService } from './types'; // Re-export value
// Benefits:
// - Smaller bundles (types removed)
// - Clear intent (type vs value)
// - Better tree shaking
// - Required for isolatedModules
// tsconfig.json
{
"compilerOptions": {
// Enforce type-only imports where possible
"verbatimModuleSyntax": true, // TS 5.0+ (replaces below)
// Or legacy options:
"importsNotUsedAsValues": "error",
"preserveValueImports": false,
"isolatedModules": true
}
}
// ESLint rule to enforce
{
"rules": {
"@typescript-eslint/consistent-type-imports": ["error", {
"prefer": "type-imports",
"fixStyle": "inline-type-imports"
}]
}
}
Example: Debugging with type imports
// When debugging, type-only imports don't affect runtime
// Before optimization
import { User, ApiClient, ValidationError } from './library';
// User is only used for types
function processUser(user: User) { }
// After optimization
import type { User } from './library'; // Type-only
import { ApiClient, ValidationError } from './library'; // Runtime values
function processUser(user: User) { } // User type available at compile time
// Helps identify dead code
// If you see a regular import only used for types,
// convert it to type-only import
// Example: Debugging circular dependencies
// file-a.ts
import type { TypeB } from './file-b'; // Safe - type-only
export interface TypeA {
b: TypeB;
}
// file-b.ts
import type { TypeA } from './file-a'; // Safe - type-only
export interface TypeB {
a: TypeA;
}
// Type-only imports don't cause runtime circular dependency
// Debugging bundle size
// Before:
import { LargeLibrary } from 'some-package';
type Config = LargeLibrary.Config; // Imports entire library!
// After:
import type { Config } from 'some-package'; // Only type, no runtime import
// Use source-map-explorer to verify:
npm run build
source-map-explorer dist/*.js
6. Conditional Compilation and Environment Types
| Technique | Implementation | Purpose | Use Case |
|---|---|---|---|
| Environment Variables | process.env.NODE_ENV with types |
Different behavior per environment | Development vs production features |
| Type Declarations | Declare global types for env variables | Type safety for process.env | Avoid runtime undefined errors |
| Const Assertions | const config = { ... } as const |
Narrow types to literal values | Type-safe configuration |
| Dead Code Elimination | Bundler removes unreachable code | Smaller production bundles | Debug logging, dev tools |
| Multiple tsconfig | Different configs per environment | Different types/settings | Test vs production builds |
| Conditional Types | Types based on environment | Different APIs per environment | Mock vs real implementations |
| Feature Flags | Type-safe feature toggles | Enable/disable features safely | A/B testing, gradual rollouts |
Example: Environment-specific types
// env.d.ts - Type environment variables
declare global {
namespace NodeJS {
interface ProcessEnv {
NODE_ENV: 'development' | 'production' | 'test';
API_URL: string;
DEBUG?: 'true' | 'false';
DATABASE_URL: string;
PORT?: string;
}
}
}
export {};
// Now process.env is fully typed
const apiUrl: string = process.env.API_URL; // Type-safe
const port: string = process.env.PORT ?? '3000';
// Conditional compilation with dead code elimination
const isDevelopment = process.env.NODE_ENV === 'development';
if (isDevelopment) {
// This code is removed in production builds by bundlers
console.log('Debug mode enabled');
enableDevTools();
}
// Better: Use function for tree-shaking
function debug(message: string) {
if (process.env.NODE_ENV === 'development') {
console.log('[DEBUG]', message);
}
}
// Even better: Replace at build time
// webpack.DefinePlugin or Vite define
const DEBUG = __DEV__; // Replaced with true/false at build time
if (DEBUG) {
console.log('Development mode');
}
// Type-safe config based on environment
type Config<T extends 'development' | 'production'> = T extends 'development'
? { debug: true; logLevel: 'verbose'; apiUrl: string }
: { debug: false; logLevel: 'error'; apiUrl: string; cdnUrl: string };
function getConfig<T extends 'development' | 'production'>(
env: T
): Config<T> {
if (env === 'development') {
return {
debug: true,
logLevel: 'verbose',
apiUrl: 'http://localhost:3000'
} as Config<T>;
} else {
return {
debug: false,
logLevel: 'error',
apiUrl: 'https://api.example.com',
cdnUrl: 'https://cdn.example.com'
} as Config<T>;
}
}
const devConfig = getConfig('development');
devConfig.debug; // true (type-safe)
Example: Feature flags and conditional types
// Feature flag system with type safety
type FeatureFlags = {
newUI: boolean;
betaFeatures: boolean;
experimentalAPI: boolean;
};
const flags: FeatureFlags = {
newUI: process.env.ENABLE_NEW_UI === 'true',
betaFeatures: process.env.NODE_ENV === 'development',
experimentalAPI: false
};
// Type-safe feature check
function isFeatureEnabled<K extends keyof FeatureFlags>(
feature: K
): boolean {
return flags[feature];
}
// Conditional API based on flags
type API<T extends FeatureFlags> = T['experimentalAPI'] extends true
? ExperimentalAPI
: StableAPI;
interface StableAPI {
fetch(url: string): Promise<Response>;
}
interface ExperimentalAPI extends StableAPI {
fetchWithCache(url: string): Promise<Response>;
preload(urls: string[]): Promise<void>;
}
// Multiple tsconfig for different environments
// tsconfig.base.json - Shared config
{
"compilerOptions": {
"target": "ES2020",
"module": "ESNext",
"strict": true
}
}
// tsconfig.json - Development
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"sourceMap": true,
"declaration": true,
"types": ["node", "jest"]
},
"include": ["src/**/*", "test/**/*"]
}
// tsconfig.prod.json - Production
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
"sourceMap": false,
"removeComments": true,
"declaration": false
},
"include": ["src/**/*"],
"exclude": ["test/**/*", "**/*.test.ts"]
}
// Build scripts
{
"scripts": {
"build:dev": "tsc",
"build:prod": "tsc -p tsconfig.prod.json"
}
}
// Webpack/Vite define plugin
// vite.config.ts
export default defineConfig({
define: {
'__DEV__': JSON.stringify(process.env.NODE_ENV === 'development'),
'__VERSION__': JSON.stringify(process.env.npm_package_version),
'process.env.API_URL': JSON.stringify(process.env.API_URL)
}
});
// globals.d.ts
declare const __DEV__: boolean;
declare const __VERSION__: string;
// Usage - dead code eliminated in production
if (__DEV__) {
console.log(`Running version ${__VERSION__}`);
enableDebugTools();
}
Note: Error handling and debugging best practices:
- Compiler errors - Read messages carefully, use hover for type info, check error codes
- Diagnosis - Break down complex types, use type annotations, @ts-expect-error for known issues
- Source maps - Enable for debugging, use declarationMap for libraries, configure VS Code launch.json
- Runtime errors - Use Result types, custom error classes with type guards, validation libraries
- Type imports - Use type-only imports for smaller bundles, enable verbatimModuleSyntax
- Environment - Type process.env, use feature flags, conditional compilation for optimizations
Error Handling and Debugging Summary
- Compiler errors - Understand common error codes (TS2322, TS2339, TS2571), read full messages
- Diagnosis - Use hover, go to definition, type annotations, isolation techniques to debug type errors
- Source maps - Enable sourceMap and declarationMap, configure VS Code for debugging TypeScript
- Runtime safety - Result types, custom errors with type guards, validation libraries (Zod, io-ts)
- Type imports - Use import type for compile-time only imports, smaller bundles, clearer intent
- Environments - Type process.env, use feature flags, multiple tsconfig files, dead code elimination