TypeScript: Static Typing for Large-Scale JavaScript Applications

Type System Fundamentals and Strict Mode

TypeScript adds static type checking to JavaScript: declare variable type (string, number, boolean, object), compiler verifies type usage. Basic types: const name: string = 'John', const age: number = 30, const active: boolean = true. Type inference: const name = 'John' automatically inferred as string (no annotation needed). Strict mode: tsconfig.json { "strict": true } enforces strict checking. Strict mode enables: noImplicitAny (any type banned unless explicit), strictNullChecks (null/undefined must be handled), strictFunctionTypes (parameter/return types checked). Without strict: let x; x = 5; x = 'hello'; (allowed, confusing). With strict: error unless explicitly typed (union type). Benefit: prevents 30-50% of runtime errors (type-related bugs). Performance: compile-time checking (0 runtime overhead), TypeScript erased from final JavaScript.

Interfaces and Type Aliases

Interfaces: describe object shape. interface User { name: string; age: number; email?: string }. Optional property (email?) can be undefined. Functions: function getUser(id: number): User { ... } returns User object. Type checking: calling getUser(123) returns object with name/age/email (compile-time verified). Inheritance: interface Admin extends User { role: 'admin' | 'moderator' }. Type aliases: type Point = { x: number; y: number }. Difference: interfaces are "open-ended" (can be re-declared, merged), types are "closed" (single declaration). Both support: readonly properties (modify detection), methods, computed properties. Advanced: interface Document { [key: string]: any } allows any property (flexible).

Generics and Type Reusability

Generics: write reusable functions with type variables. function identity(arg: T): T { return arg; }. Caller specifies type: identity('hello') or inferred by compiler. Example: Array (generic type): let numbers: Array = [1, 2, 3] (typed array). Generics in functions: function first(arr: Array): T { return arr[0]; }. Type constraints: function maxLength(obj: T, max: number): T { ... } (T must have length property). Use case: only strings/arrays/objects with length property accepted. Generics in classes: class Box { contents: T; get(): T; set(value: T): void; }. Utility types: Partial (all properties optional), Readonly (all immutable), Pick (select properties), Record (key-value mapping).

Type Narrowing and Type Guards

Type narrowing: reduce type from broader to narrower. Union types: function process(input: string | number) { ... }. Without narrowing: input.toUpperCase() throws error (number lacks toUpperCase). Type guards: if (typeof input === 'string') { input.toUpperCase() } (narrowed to string). Instanceof checks: if (error instanceof Error) { error.message } (narrow to Error class). Type predicates: function isUser(obj: any): obj is User { return obj.name && obj.age } (custom guard). Truthiness narrowing: if (value) { ... } eliminates null/undefined/falsy values. Exhaustive checking: type Status = 'pending' | 'complete' | 'error'; switch(status) { case 'pending': ...; case 'complete': ...; case 'error': ...; default: const impossible: never = status } (compile error if case missed).

Advanced Features and Performance

Decorators: @deprecated, @readonly modify class behavior (experimental). Example: @memoize function caches results (functional decoration). Enums: enum Direction { Up = 0, Down = 1, Left = 2, Right = 3 }. Reverse mapping: Direction[0] === 'Up' (allows name lookup). String enums: enum Color { Red = 'red', Green = 'green' } (more readable). Conditional types: type IsString = T extends string ? true : false. IsString<'hello'> evaluates to true. Mapped types: type ReadonlyUser = Readonly (all properties immutable). Type assertion: (value as string).toUpperCase() forces type (use rarely). Non-null assertion: value!.property skips null check (unsafe).

Integration with Tools and Best Practices

IDE support: VSCode provides real-time type checking, autocomplete suggestions (50+ milliseconds lag eliminated vs JavaScript debugging). Linting: ESLint + typescript-eslint catches style/type issues. Testing: Jest + @types/jest type-safe test cases. Compilation: tsc (TypeScript compiler) generates JavaScript. Watch mode: tsc --watch recompiles on file change (<100ms). Build optimization: incremental builds cache intermediate results (30% faster rebuilds). Source maps: sourcemap: true maps compiled JS back to TS for debugging. Production: ship JavaScript (no TS runtime), tree-shake unused types (0 bundle overhead). Library publishing: @types/package provides types for untyped JS libraries. Breaking changes: version bump signals type compatibility (semver for types).

Common Pitfalls and Anti-Patterns

any type: defeats type checking (avoid). Instead use unknown (must narrow before use) or generic types. Example: function process(data: any) { ... } allows bug (misspelled property passes). Better: function process(data: T): T { ... } (T must match actual data). Implicit any: let x; x = 5 (x: any implicitly). Fix: strict mode enforces annotation. Type assertion: (data as User).name bypasses checks (unsafe casting). Correct: use type guards (if instanceof/typeof checks). Forget readonly: let obj: { readonly id: number } = { id: 1 }; obj.id = 2 (TypeScript error - good). Circular dependencies: A imports B, B imports A (module resolution fails). Solution: extract shared types to separate file. Performance: don't use complex conditional types excessively (compilation time increases). Production deployment: verify types before deploy (tsc --noEmit in CI/CD). Debug types: type DebugType = T prints to compiler output (for understanding complex types).