Explicitly specify variable type - provides documentation and compile-time checking
let age: number = 25;
Type Inference
let x = value
TypeScript automatically infers type from initial value - reduces verbosity
let name = "John";
Function Return Type
function(): Type
Annotate function return type for clarity and type safety
function getAge(): number
Parameter Annotation
param: Type
Required for function parameters - inference not available
function(x: string)
Contextual Typing
callback context
Type inferred from context where value is used - common in callbacks
arr.map(x => x.length)
Example: Type annotations vs inference
// Explicit annotationlet count: number = 10;let message: string = "Hello";// Type inference (recommended when obvious)let inferredCount = 10; // numberlet inferredMsg = "Hello"; // string// Function with annotationsfunction add(a: number, b: number): number { return a + b;}// Contextual typing in callbacksconst numbers = [1, 2, 3];numbers.map(num => num * 2); // num inferred as number
1.2 Primitive Types
Type
Syntax
Values
Use Case
string
string
Text values, template literals, Unicode characters
Escape hatch - allows any operation without type safety
Migration, third-party libs, quick prototypes
unknown TS 3.0+
Type-safe any
Requires type checking before use - safer than any
User input, API responses, dynamic data
never
Represents impossible
Functions that never return, unreachable code, exhaustive checks
Error throwing, infinite loops, type guards
void
No return value
Function completes but returns nothing meaningful
Event handlers, loggers, side-effect functions
any vs unknown
Feature
any
unknown
Type Safety
None
Full
Operations
All allowed
Requires checks
Assignment
To/from any type
From any, to unknown only
Recommendation
Avoid if possible
Preferred over any
void vs never
Feature
void
never
Return
Function completes
Never completes
Value
undefined
No value possible
Usage
Side effects
Errors, unreachable
Example: any vs unknown
// any - no type safetylet data: any = getUserInput();data.anything(); // No error, dangerous!data.toUpperCase(); // Runtime error if not string// unknown - type-safelet safeData: unknown = getUserInput();// safeData.anything(); // Error! Must check firstif (typeof safeData === "string") { safeData.toUpperCase(); // OK, narrowed to string}
Example: never and void
// void - function completes but returns nothingfunction log(message: string): void { console.log(message); // implicitly returns undefined}// never - function never returnsfunction throwError(message: string): never { throw new Error(message); // execution stops here}function infiniteLoop(): never { while (true) { // never exits }}// never in exhaustive checkstype Shape = "circle" | "square";function assertNever(x: never): never { throw new Error("Unexpected: " + x);}function getArea(shape: Shape): number { switch (shape) { case "circle": return 1; case "square": return 2; default: return assertNever(shape); // Compile error if not exhaustive }}
Warning: Avoid any whenever possible - it defeats TypeScript's purpose. Use
unknown when you need dynamic typing with safety.
2. Advanced Type System Features
2.1 Type Aliases and Interface Declarations
Feature
Syntax
Description
Use Case
Type Alias
type Name = Type
Creates named reference to any type - unions, intersections, primitives
Complex unions, utility compositions
Interface
interface Name { }
Defines object shape - extendable and mergeable
Object contracts, class implementations
Interface Extension
extends Interface
Inherit properties from parent interfaces
Hierarchical type relationships
Type Intersection
Type1 & Type2
Combine multiple type aliases
Mixins, composed types
Declaration Merging
Multiple interface declarations
Interfaces with same name automatically merge - not available for types
Augmenting libraries, modules
Type vs Interface
Feature
Type
Interface
Unions
✓
✗
Intersections
✓
Use extends
Primitives
✓
✗
Tuples
✓
✗
Merging
✗
✓
Computed Props
✓
✗
Example: Type alias
type ID = string | number;type Point = [number, number];type Callback = (data: string) => void;
Example: Interface
interface User { id: number; name: string;}interface Admin extends User { permissions: string[];}
Example: Type aliases vs interfaces
// Type alias - flexible for any typetype StringOrNumber = string | number;type Coordinate = [number, number];type UserID = string;// Interface - best for object shapesinterface Person { name: string; age: number;}interface Employee extends Person { employeeId: string; department: string;}// Declaration merging (interfaces only)interface Window { customProperty: string;}interface Window { anotherProperty: number;}// Window now has both properties// Type compositiontype WithTimestamp = { timestamp: Date };type TrackedPerson = Person & WithTimestamp;
2.2 Enum Types and Const Enums
Type
Syntax
Description
Compiled Output
Numeric Enum
enum Name { A, B }
Auto-incrementing numeric values starting from 0
Object with reverse mapping
String Enum
enum { A = "a" }
Explicit string values - no auto-increment
Object without reverse mapping
Heterogeneous Enum
enum { A = 1, B = "b" }
Mix of numeric and string values - not recommended
Mixed object
Const Enum
const enum Name { }
Completely inlined at compile time - no runtime object
Values inlined, no object
Computed Member
A = expression
Value computed from expression
Evaluated at compile time
Ambient Enum
declare enum Name { }
Describe existing enum from external source
Type-only, no JS output
Example: Enum types
// Numeric enumenum Direction { Up, // 0 Down, // 1 Left, // 2 Right // 3}// String enum (preferred)enum Status { Pending = "PENDING", Active = "ACTIVE", Completed = "COMPLETED"}// Custom numeric valuesenum HttpStatus { OK = 200, NotFound = 404, ServerError = 500}// Const enum (performance optimization)const enum Colors { Red = "#FF0000", Green = "#00FF00", Blue = "#0000FF"}// Usagelet status: Status = Status.Active;let color = Colors.Red; // Inlined to "#FF0000" at compile time
Note: Prefer string enums or union of
string literals over numeric enums for better debugging and runtime safety. Use const enum
for zero-runtime overhead.
2.3 Type Assertions
Syntax
Form
Description
Use Case
as Syntax
value as Type
Preferred modern syntax - works in JSX
All scenarios, especially TSX/JSX files
Angle Bracket
<Type>value
Original syntax - conflicts with JSX
Non-JSX files only
Double Assertion
x as unknown as T
Force assertion through unknown - escape hatch
Incompatible types, migrations
Const Assertion TS 3.4+
as const
Make literal types readonly and narrow to literal
Immutable data, exact types
Non-null Assertion
value!
Assert value is not null/undefined
When you know value exists
Example: Type assertions
// as syntax (preferred)let input = document.getElementById("input") as HTMLInputElement;input.value = "Hello";// Angle bracket syntax (avoid in JSX)let element = <HTMLElement>document.querySelector(".item");// Double assertion (escape hatch)let num = "123" as unknown as number; // Dangerous!// Const assertion - deep readonlylet config = { host: "localhost", port: 3000, flags: ["debug", "verbose"]} as const;// Type: { readonly host: "localhost"; readonly port: 3000; readonly flags: readonly ["debug", "verbose"] }// Array as constlet arr = [1, 2, 3] as const; // Type: readonly [1, 2, 3]// Non-null assertionfunction process(value: string | null) { let length = value!.length; // Assert value is not null}
Warning: Type assertions bypass type checking - use sparingly. Prefer type guards and
narrowing. as const is safe and recommended.
2.4 Type Guards and Narrowing Techniques
Technique
Syntax
Description
Narrows To
typeof Guard
typeof x === "type"
Check primitive types at runtime
string, number, boolean, etc.
instanceof Guard
x instanceof Class
Check if object is instance of class
Class type
in Operator
"prop" in obj
Check property existence
Type with that property
Truthiness Check
if (value) { }
Filters out null, undefined, 0, "", false
Non-nullable type
Equality Check
x === value
Compare with specific value
Literal type
User-defined Guard
is Type
Custom type predicate function
Custom type
Discriminated Union
switch (x.kind)
Use discriminant property to narrow
Specific union member
Example: Built-in type guards
function process(value: string | number) { // typeof guard if (typeof value === "string") { console.log(value.toUpperCase()); // string } else { console.log(value.toFixed(2)); // number }}// instanceof guardfunction handle(x: Date | string) { if (x instanceof Date) { console.log(x.getFullYear()); // Date } else { console.log(x.length); // string }}// in operatortype Fish = { swim: () => void };type Bird = { fly: () => void };function move(animal: Fish | Bird) { if ("swim" in animal) { animal.swim(); // Fish } else { animal.fly(); // Bird }}// Truthiness narrowingfunction print(text: string | null) { if (text) { console.log(text.toUpperCase()); // string }}
Example: User-defined type guards
// Type predicate functionfunction isString(value: unknown): value is string { return typeof value === "string";}function process(input: unknown) { if (isString(input)) { console.log(input.toUpperCase()); // Narrowed to string }}// Complex type guardinterface Cat { meow: () => void; }interface Dog { bark: () => void; }function isCat(animal: Cat | Dog): animal is Cat { return (animal as Cat).meow !== undefined;}function speak(animal: Cat | Dog) { if (isCat(animal)) { animal.meow(); } else { animal.bark(); }}
2.5 Discriminated Unions and Tagged Unions
Component
Description
Example
Discriminant Property
Common property with literal type - acts as type tag
kind: "success" | "error"
Union Members
Each type in union has unique discriminant value
Success | Error
Exhaustiveness Check
Ensure all union cases are handled
default: assertNever(x)
Example: Discriminated unions for safe state management
// Define discriminated uniontype Result<T> = | { status: "loading" } | { status: "success"; data: T } | { status: "error"; error: Error };function handle<T>(result: Result<T>) { switch (result.status) { case "loading": console.log("Loading..."); break; case "success": console.log(result.data); // Type: T break; case "error": console.log(result.error.message); // Type: Error break; }}// Shape discriminated uniontype Shape = | { kind: "circle"; radius: number } | { kind: "rectangle"; width: number; height: number } | { kind: "triangle"; base: number; height: number };function area(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rectangle": return shape.width * shape.height; case "triangle": return 0.5 * shape.base * shape.height; }}// Exhaustiveness checkfunction assertNever(x: never): never { throw new Error("Unexpected: " + x);}function getArea(shape: Shape): number { switch (shape.kind) { case "circle": return Math.PI * shape.radius ** 2; case "rectangle": return shape.width * shape.height; case "triangle": return 0.5 * shape.base * shape.height; default: return assertNever(shape); // Compile error if case missing }}
2.6 Mapped Types and Key Remapping
Pattern
Syntax
Description
Result
Basic Mapped Type
{ [K in Keys]: Type }
Iterate over keys and map to type
New object type with mapped properties
Readonly Mapping
{ readonly [K in Keys] }
Make all properties readonly
Readonly<T>
Optional Mapping
{ [K in Keys]?: Type }
Make all properties optional
Partial<T>
Remove Modifiers
-readonly, -?
Remove readonly or optional modifiers
Required<T>
Key Remapping TS 4.1+
as NewKey
Transform key names during mapping
Renamed property keys
Filter Keys
as K extends X ? K : never
Conditionally include/exclude keys
Filtered property set
Example: Mapped types
// Basic mapped typetype Nullable<T> = { [K in keyof T]: T[K] | null;};type User = { name: string; age: number };type NullableUser = Nullable<User>;// Result: { name: string | null; age: number | null }// Add/remove modifierstype Mutable<T> = { -readonly [K in keyof T]: T[K];};type Required<T> = { [K in keyof T]-?: T[K];};// Key remapping with template literalstype Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};type Person = { name: string; age: number };type PersonGetters = Getters<Person>;// Result: { getName: () => string; getAge: () => number }// Filter properties by typetype StringKeys<T> = { [K in keyof T as T[K] extends string ? K : never]: T[K];};type Data = { id: number; name: string; email: string; count: number };type StringProps = StringKeys<Data>;// Result: { name: string; email: string }
2.7 Conditional Types and Utility Types
Pattern
Syntax
Description
Use Case
Conditional Type
T extends U ? X : Y
Type-level if-else - select type based on condition
Dynamic type selection
infer Keyword TS 2.8+
infer R
Extract type from another type within conditional
Return types, array elements
Distributive
T extends U when T is union
Conditional applied to each union member separately
Union transformations
Non-Distributive
[T] extends [U]
Conditional applied to whole type, not distributed
Exact type matching
Example: Conditional types
// Basic conditional typetype IsString<T> = T extends string ? true : false;type A = IsString<string>; // truetype B = IsString<number>; // false// Extract return type with infertype ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;type Func = () => string;type Result = ReturnType<Func>; // string// Extract array element typetype ElementType<T> = T extends (infer U)[] ? U : never;type Items = ElementType<number[]>; // number// Distributive conditional (union)type ToArray<T> = T extends any ? T[] : never;type Arrays = ToArray<string | number>;// Result: string[] | number[] (distributed)// Non-distributive (tuple)type ToArrayNonDist<T> = [T] extends [any] ? T[] : never;type TupleArray = ToArrayNonDist<string | number>;// Result: (string | number)[] (not distributed)// Complex conditional - function overloadstype UnwrapPromise<T> = T extends Promise<infer U> ? U : T;type Value1 = UnwrapPromise<Promise<string>>; // stringtype Value2 = UnwrapPromise<number>; // number// Recursive conditional typetype Flatten<T> = T extends Array<infer U> ? Flatten<U> : T;type Deep = Flatten<number[][][]>; // number
Key Takeaways
Use interfaces for object shapes and type
aliases for unions/intersections
Prefer string enums or union literals over numeric enums
as const assertions are safe; regular type assertions are dangerous
Type guards enable safe type narrowing at runtime
Discriminated unions with exhaustiveness checks ensure type safety
// Generic classclass Box<T> { private contents: T; constructor(value: T) { this.contents = value; } get(): T { return this.contents; } set(value: T): void { this.contents = value; }}const stringBox = new Box<string>("hello");const numberBox = new Box(42); // Type inferred// Generic class with multiple parametersclass Pair<T, U> { constructor( public first: T, public second: U ) {} swap(): Pair<U, T> { return new Pair(this.second, this.first); }}// Generic collection classclass Collection<T> { private items: T[] = []; add(item: T): void { this.items.push(item); } get(index: number): T { return this.items[index]; } findFirst(predicate: (item: T) => boolean): T | undefined { return this.items.find(predicate); }}
3.3 Generic Constraints with extends Keyword
Constraint Type
Syntax
Description
Use Case
Basic Constraint
T extends Type
T must be assignable to Type - restricts generic
Ensure minimum type requirements
Object Constraint
T extends object
T must be object type - excludes primitives
Object manipulation functions
Interface Constraint
T extends Interface
T must implement interface - ensures properties/methods
Polymorphic functions
keyof Constraint
K extends keyof T
K must be property key of T - type-safe property access
Property getters/setters
Union Constraint
T extends A | B
T must be one of specified types
Limited type options
Multiple Constraints
T extends A & B
T must satisfy multiple constraints simultaneously
Complex requirements
Example: Generic constraints
// Basic constraint - ensure property existsinterface Lengthwise { length: number;}function logLength<T extends Lengthwise>(arg: T): T { console.log(arg.length); // OK - length guaranteed return arg;}logLength("hello"); // OK - string has lengthlogLength([1, 2, 3]); // OK - array has length// logLength(10); // Error - number has no length// keyof constraint for type-safe property accessfunction getProperty<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key];}let person = { name: "Alice", age: 30 };let name = getProperty(person, "name"); // stringlet age = getProperty(person, "age"); // number// let x = getProperty(person, "invalid"); // Error// Multiple constraintsinterface Named { name: string; }interface Aged { age: number; }function describe<T extends Named & Aged>(obj: T): string { return `${obj.name} is ${obj.age} years old`;}// Constructor constraintfunction create<T>(constructor: new () => T): T { return new constructor();}class Person { name = "Unknown";}let person = create(Person); // Person instance
3.4 Default Generic Parameters
Feature
Syntax
Description
Use Case
Default Type
<T = DefaultType>
Fallback type when generic not provided
Optional configuration types
Dependent Default
<T, U = T>
Default depends on another type parameter
Related type pairs
Conditional Default
<T = T extends X ? A : B>
Default computed from conditional type
Smart defaults based on constraints
Example: Default generic parameters
// Basic defaultinterface Container<T = string> { value: T;}let c1: Container = { value: "hello" }; // T defaults to stringlet c2: Container<number> = { value: 42 }; // T explicitly number// Multiple parameters with defaultsclass Request<TData = any, TError = Error> { data?: TData; error?: TError;}// Use all defaultslet req1 = new Request(); // Request<any, Error>// Override first, use second defaultlet req2 = new Request<string>(); // Request<string, Error>// Override bothlet req3 = new Request<string, CustomError>(); // Request<string, CustomError>// Dependent default - U defaults to Tfunction create<T, U = T>(value: T): [T, U] { return [value, value as unknown as U];}// Default from conditionaltype APIResponse<T = never> = T extends never ? { status: "idle" } : { status: "success"; data: T } | { status: "error"; error: string };let response1: APIResponse = { status: "idle" };let response2: APIResponse<string> = { status: "success", data: "hello" };
3.5 Conditional Types with infer Keyword
Pattern
Syntax
Description
Extracts
infer in Conditional
T extends Pattern<infer U>
Extract type from pattern match
Type variable U
Function Return
(...args) => infer R
Extract function return type
Return type
Function Parameters
(infer P) => any
Extract parameter types
Parameter tuple
Array Element
(infer U)[]
Extract array element type
Element type
Promise Value
Promise<infer V>
Extract promise resolved type
Resolved type
Multiple infer
infer A, infer B
Extract multiple types from pattern
Multiple type variables
Example: infer keyword usage
// Extract return typetype GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;type Func1 = () => string;type Return1 = GetReturnType<Func1>; // string// Extract parameter typestype GetParameters<T> = T extends (...args: infer P) => any ? P : never;type Func2 = (a: string, b: number) => void;type Params = GetParameters<Func2>; // [string, number]// Extract array element typetype ElementType<T> = T extends (infer E)[] ? E : never;type Numbers = ElementType<number[]>; // number// Unwrap Promisetype Unpromise<T> = T extends Promise<infer U> ? U : T;type Resolved = Unpromise<Promise<string>>; // stringtype NotPromise = Unpromise<number>; // number// Deep unwrap nested Promisestype DeepUnpromise<T> = T extends Promise<infer U> ? DeepUnpromise<U> : T;type Deep = DeepUnpromise<Promise<Promise<number>>>; // number// Extract first array element typetype First<T extends any[]> = T extends [infer F, ...any[]] ? F : never;type FirstType = First<[string, number, boolean]>; // string// Extract last array element typetype Last<T extends any[]> = T extends [...any[], infer L] ? L : never;type LastType = Last<[string, number, boolean]>; // boolean// Extract constructor parameter typestype ConstructorParams<T> = T extends new (...args: infer P) => any ? P : never;class MyClass { constructor(name: string, age: number) {}}type Params2 = ConstructorParams<typeof MyClass>; // [string, number]
3.6 Variance and Covariance in Generics
Concept
Relationship
Description
Example Context
Covariance
T<Child> ⊆ T<Parent>
Generic preserves subtype relationship - safe for output positions
Return types, readonly arrays
Contravariance
T<Parent> ⊆ T<Child>
Generic reverses subtype relationship - safe for input positions
Function parameters
Invariance
No relationship
Generic allows no subtype substitution - most restrictive
Mutable containers
Bivariance
Both directions
Generic accepts both super and subtypes - least safe
Legacy method signatures
Variance Rules
Position
Variance
Return Type
Covariant
Parameter Type
Contravariant
Readonly Property
Covariant
Writable Property
Invariant
Method (strict)
Contravariant params
Array Variance
Type
Variance
Mutable Array
Covariant (unsafe)
Readonly Array
Covariant (safe)
Tuple
Covariant elements
Example: Covariance - return types
class Animal { name: string = "";}class Dog extends Animal { breed: string = "";}// Covariance in return typestype Producer<T> = () => T;let animalProducer: Producer<Animal> = () => new Animal();let dogProducer: Producer<Dog> = () => new Dog();// Covariant - Dog producer is subtype of Animal produceranimalProducer = dogProducer; // OK - Dog extends Animal// Readonly arrays are covariant (safe)let animals: readonly Animal[] = [];let dogs: readonly Dog[] = [new Dog()];animals = dogs; // OK - readonly, so safe
Example: Contravariance - parameter types
// Contravariance in function parameterstype Consumer<T> = (arg: T) => void;let animalConsumer: Consumer<Animal> = (animal: Animal) => { console.log(animal.name);};let dogConsumer: Consumer<Dog> = (dog: Dog) => { console.log(dog.breed);};// Contravariant - Animal consumer is subtype of Dog consumerdogConsumer = animalConsumer; // OK - can handle Dog since it's an Animal// animalConsumer = dogConsumer; // Error - cannot handle all Animals
Example: Invariance - mutable containers
// Mutable arrays are technically covariant but unsafelet animals: Animal[] = [];let dogs: Dog[] = [new Dog()];animals = dogs; // OK in TypeScript (unsound)// This is why it's unsafe:animals.push(new Animal()); // Runtime error! Added Animal to Dog[]// Solution: Use readonly for true covariancefunction printAnimals(animals: readonly Animal[]) { animals.forEach(a => console.log(a.name));}printAnimals(dogs); // Safe - cannot modify array
Note: TypeScript's array covariance is unsound but pragmatic. Use readonly arrays
for type-safe covariance. Enable strictFunctionTypes for better variance checking in function
parameters.
Generics Best Practices
Use type inference when possible - avoid explicit type arguments
Apply constraints to ensure type safety and enable useful operations
Provide default types for optional flexibility
Use infer for extracting types from complex type patterns
Understand variance for safe generic type relationships
Prefer readonly for covariant collections
4. Utility Types and Built-in Type Helpers
4.1 Partial<T> and Required<T> for Property Modification
Utility Type
Syntax
Description
Use Case
Partial<T>
Partial<Type>
Makes all properties optional - adds ? to each property
Updates, patches, partial configs
Required<T>
Required<Type>
Makes all properties required - removes ? from properties
Validation, complete objects
Readonly<T>
Readonly<Type>
Makes all properties readonly - adds readonly modifier
Note: Interface merging only works with interfaces, not type
aliases. This is one key difference between interfaces and types. Use merging for library augmentation and
extending third-party types.
Explicit type for this context - not a real parameter
Method typing, callbacks
this Type
this: void
Function doesn't use this or requires no specific context
Standalone functions
ThisParameterType<T>
Extract this parameter type
Get the type of this from function type
Type utilities
OmitThisParameter<T>
Remove this from function type
Convert method to standalone function type
Function transformations
noImplicitThis
Compiler flag
Require explicit this types - prevents errors
Strict type checking
Example: Explicit this parameter
// this parameter in functioninterface User { name: string; greet(this: User): void;}const user: User = { name: "Alice", greet() { console.log(`Hello, I'm ${this.name}`); }};user.greet(); // OK - correct context// const greetFn = user.greet;// greetFn(); // Error if noImplicitThis enabled// this in callbacksinterface Database { query(this: Database, sql: string): void;}const db: Database = { query(sql: string) { console.log(`Executing: ${sql}`); }};// Standalone function with thisfunction logThis(this: { value: number }): void { console.log(this.value);}const obj = { value: 42 };logThis.call(obj); // 42// this: void - no this context neededfunction calculate(this: void, a: number, b: number): number { // Cannot use 'this' here return a + b;}// Generic thisinterface Builder<T> { setValue(this: Builder<T>, value: T): this; build(this: Builder<T>): T;}class StringBuilder implements Builder<string> { private value: string = ""; setValue(value: string): this { this.value = value; return this; } build(): string { return this.value; }}
Example: this type utilities
// ThisParameterType - extract this typetype ObjectMethod = (this: { name: string }, age: number) => void;type ThisType = ThisParameterType<ObjectMethod>;// Result: { name: string }// OmitThisParameter - remove this parametertype StandaloneMethod = OmitThisParameter<ObjectMethod>;// Result: (age: number) => void// Practical use caseinterface Context { prefix: string;}function log(this: Context, message: string): void { console.log(`${this.prefix}: ${message}`);}// Extract this type for type safetytype LogContext = ThisParameterType<typeof log>;const context: LogContext = { prefix: "INFO" };log.call(context, "Starting");// Convert to standalonetype StandaloneLog = OmitThisParameter<typeof log>;const standaloneFn: StandaloneLog = (message) => { console.log(message);};// this in class methodsclass Component { private state: number = 0; // Method has implicit 'this: Component' increment(): void { this.state++; } // Bind-safe method with this parameter onClick(this: Component, event: Event): void { this.increment(); }}
7.6 Arrow Functions and Lexical this Binding
Feature
Regular Function
Arrow Function
this Binding
Dynamic - depends on call site
Lexical - captured from enclosing scope
arguments
Has arguments object
No arguments - use rest params
Constructor
Can be used with new
Cannot be constructor
Syntax
function() { }
() => { } or () => expr
prototype
Has prototype property
No prototype
Example: Arrow function this binding
// Arrow functions capture 'this' lexicallyclass Counter { count: number = 0; // Regular method - 'this' is dynamic increment() { this.count++; } // Arrow function - 'this' is lexical (captured) delayedIncrement = () => { setTimeout(() => { this.count++; // 'this' refers to Counter instance }, 1000); }; // Problem with regular function problematicIncrement() { setTimeout(function() { // this.count++; // Error: 'this' is undefined or window }, 1000); }}const counter = new Counter();counter.delayedIncrement(); // Works correctly// Arrow functions in event handlersclass Button { label: string = "Click me"; // Arrow function preserves 'this' handleClick = (event: MouseEvent) => { console.log(`${this.label} was clicked`); }; attachHandler(element: HTMLElement) { element.addEventListener("click", this.handleClick); }}// Array methods with arrow functionsconst numbers = [1, 2, 3, 4, 5];const doubled = numbers.map(n => n * 2);const evens = numbers.filter(n => n % 2 === 0);const sum = numbers.reduce((acc, n) => acc + n, 0);
Example: Arrow function patterns
// Implicit returnconst square = (x: number) => x * x;const greet = (name: string) => `Hello, ${name}!`;// Object literal return (wrap in parens)const makePoint = (x: number, y: number) => ({ x, y });// Generic arrow functionsconst identity = <T>(value: T): T => value;const toArray = <T>(value: T): T[] => [value];// Arrow function as callbacksetTimeout(() => console.log("Done"), 1000);// Arrow with destructuringconst printUser = ({ name, age }: { name: string; age: number }) => { console.log(`${name} is ${age} years old`);};// Async arrow functionconst fetchUser = async (id: number): Promise<User> => { const response = await fetch(`/api/users/${id}`); return response.json();};// Higher-order arrow functionsconst multiply = (x: number) => (y: number) => x * y;const double = multiply(2);console.log(double(5)); // 10// Arrow vs regular in classclass Example { name = "Example"; // Regular method - shared on prototype regularMethod() { return this.name; } // Arrow function - unique per instance arrowMethod = () => { return this.name; };}// Arrow preserves context in callbacksclass DataLoader { data: string[] = []; load(urls: string[]) { // 'this' preserved in arrow function urls.forEach(url => { fetch(url).then(response => { this.data.push(response.url); // Safe to use 'this' }); }); }}
Note: Use arrow functions when you need to preserve
this context (event handlers, callbacks). Use regular functions for
methods that might be overridden or when you need arguments object.
Function Best Practices
Use function type expressions for inline callback types
Apply overloading for multiple valid signatures
Prefer default parameters over optional with undefined checks
Use rest parameters for variable-length argument lists
Add explicit this parameter when context matters
Use arrow functions for callbacks needing lexical this
Enable noImplicitThis for safer this usage
8. Advanced Type Manipulation
8.1 Mapped Types with Key Remapping
Pattern
Syntax
Description
Use Case
Basic Mapped Type
{[K in Keys]: Type}
Transform each property in a type - iterate over union of keys
Make all properties readonly/optional
Key Remapping
{[K in Keys as NewK]: Type}
Transform property keys during mapping using as clause
Prefix/suffix keys, filter properties
Template Remapping
as `prefix${K}`
Use template literal types to transform key names
Add getters/setters prefixes
Conditional Remapping
as K extends Cond ? T : F
Conditionally include/exclude or transform keys
Filter out specific properties
never for Filtering
as K extends Bad ? never : K
Exclude properties by mapping key to never
Remove private/internal properties
Mapped Modifiers
+readonly, -?
Add (+) or remove (-) readonly/optional modifiers
Create mutable/required versions
Example: Basic mapped types and modifiers
// Make all properties optionaltype Partial<T> = { [P in keyof T]?: T[P];};// Make all properties required (remove optional)type Required<T> = { [P in keyof T]-?: T[P];};// Make all properties readonlytype Readonly<T> = { readonly [P in keyof T]: T[P];};// Remove readonly modifiertype Mutable<T> = { -readonly [P in keyof T]: T[P];};
Example: Key remapping with template literals
// Add getter prefix to all propertiestype Getters<T> = { [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];};interface User { name: string; age: number;}// Result: { getName: () => string; getAge: () => number; }type UserGetters = Getters<User>;// Add setter prefixtype Setters<T> = { [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;};// Combine getters and setterstype Accessors<T> = Getters<T> & Setters<T>;
Example: Filtering properties with conditional remapping
// Remove properties starting with underscoretype RemovePrivate<T> = { [K in keyof T as K extends `_${string}` ? never : K]: T[K];};interface Data { name: string; _id: number; _internal: boolean; value: number;}// Result: { name: string; value: number; }type PublicData = RemovePrivate<Data>;// Extract only function propertiestype FunctionProps<T> = { [K in keyof T as T[K] extends Function ? K : never]: T[K];};// Extract only non-function propertiestype DataProps<T> = { [K in keyof T as T[K] extends Function ? never : K]: T[K];};
8.2 Template Literal Types and String Manipulation
Type
Syntax
Description
Example
Template Literal
`${A}${B}`
Combine string literal types - creates union of all combinations
// Deep readonly - make all nested properties readonlytype DeepReadonly<T> = { readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P];};interface NestedData { user: { name: string; address: { street: string; city: string; }; };}type ReadonlyData = DeepReadonly<NestedData>;// All nested properties are readonly// Deep partial - make all nested properties optionaltype DeepPartial<T> = { [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];};// Deep required - make all nested properties requiredtype DeepRequired<T> = { [P in keyof T]-?: T[P] extends object ? DeepRequired<T[P]> : T[P];};
Example: Path string type for nested objects
// Generate all valid property pathstype Paths<T, Prefix extends string = ''> = { [K in keyof T]: K extends string ? T[K] extends object ? `${Prefix}${K}` | Paths<T[K], `${Prefix}${K}.`> : `${Prefix}${K}` : never;}[keyof T];interface Data { user: { name: string; address: { city: string; }; }; count: number;}// Result: 'user' | 'user.name' | 'user.address' | 'user.address.city' | 'count'type DataPaths = Paths<Data>;// Get value type at pathtype PathValue<T, P extends string> = P extends `${infer K}.${infer Rest}` ? K extends keyof T ? PathValue<T[K], Rest> : never : P extends keyof T ? T[P] : never;type CityType = PathValue<Data, 'user.address.city'>; // string
8.4 Higher-Order Types and Type-level Programming
Concept
Description
Pattern
Use Case
Type Constructor
Generic type that takes types as parameters
type Box<T> = { value: T }
Container types, wrappers
Higher-Order Type
Type that takes/returns type constructors
type Apply<F, T>
Generic transformations
Type-level Function
Conditional type that computes new type
type If<C, T, F>
Conditional logic
Type Composition
Combine multiple type transformations
Compose<F, G, T>
Pipeline transformations
Type-level Iteration
Build types through recursion/iteration
Tuple building, counting
Compile-time computation
Example: Type-level utilities
// Identity function at type leveltype Identity<T> = T;// Constant type - always returns same typetype Const<A, B> = A;// Type-level if/elsetype If<Cond extends boolean, Then, Else> = Cond extends true ? Then : Else;// Type equality checktype Equals<X, Y> = (<T>() => T extends X ? 1 : 2) extends (<T>() => T extends Y ? 1 : 2) ? true : false;type Test1 = Equals<string, string>; // truetype Test2 = Equals<string, number>; // false// Type-level ANDtype And<A extends boolean, B extends boolean> = A extends true ? (B extends true ? true : false) : false;// Type-level ORtype Or<A extends boolean, B extends boolean> = A extends true ? true : (B extends true ? true : false);
Example: Tuple and array manipulation
// Tuple lengthtype Length<T extends any[]> = T['length'];// Prepend element to tupletype Prepend<E, T extends any[]> = [E, ...T];// Append element to tupletype Append<T extends any[], E> = [...T, E];// Concat tuplestype Concat<T extends any[], U extends any[]> = [...T, ...U];// Reverse tupletype Reverse<T extends any[]> = T extends [infer First, ...infer Rest] ? [...Reverse<Rest>, First] : [];type Original = [1, 2, 3];type Reversed = Reverse<Original>; // [3, 2, 1]// Build tuple of length Ntype Tuple<N extends number, T = unknown, Acc extends T[] = []> = Acc['length'] extends N ? Acc : Tuple<N, T, [T, ...Acc]>;type FiveItems = Tuple<5, string>; // [string, string, string, string, string]
Example: Type-level arithmetic (simplified)
// Add 1 to tuple length (increment)type Inc<T extends any[]> = [...T, any];// Subtract 1 from tuple length (decrement)type Dec<T extends any[]> = T extends [any, ...infer Rest] ? Rest : [];// Add two numbers using tuplestype Add<A extends number, B extends number> = [...Tuple<A>, ...Tuple<B>]['length'];type Result = Add<3, 5>; // 8 (limited by recursion depth)// Range type - generate union of numberstype Range< From extends number, To extends number, Acc extends number[] = [], Result = never> = Acc['length'] extends To ? Result | To : Range<From, To, Inc<Acc>, Acc['length'] extends From ? Result | Acc['length'] : Result>;type ZeroToFive = Range<0, 5>; // 0 | 1 | 2 | 3 | 4 | 5
8.5 Brand Types and Nominal Typing Patterns
Pattern
Technique
Purpose
Advantage
Brand Property
Add unique symbol property
Distinguish structurally identical types
Type safety for primitives
Phantom Type
Generic parameter not used in structure
Tag type with metadata
No runtime overhead
Opaque Type
Hide implementation details
Prevent direct construction
Encapsulation
Newtype Pattern
Wrapper type around primitive
Domain-specific types
Self-documenting code
Example: Basic branding with symbols
// Brand using unique symboldeclare const UserIdBrand: unique symbol;type UserId = string & { [UserIdBrand]: true };declare const ProductIdBrand: unique symbol;type ProductId = string & { [ProductIdBrand]: true };// Constructor functions ensure type safetyfunction createUserId(id: string): UserId { // Validation logic here return id as UserId;}function createProductId(id: string): ProductId { return id as ProductId;}// Type-safe usagefunction getUser(id: UserId) { /* ... */ }function getProduct(id: ProductId) { /* ... */ }const userId = createUserId('user-123');const productId = createProductId('prod-456');getUser(userId); // ✓ OKgetUser(productId); // ✗ Error: ProductId not assignable to UserIdgetUser('user-789'); // ✗ Error: string not assignable to UserId
Example: Branded primitive types
// Email typetype Email = string & { readonly __brand: 'Email' };function isValidEmail(str: string): str is Email { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);}function sendEmail(to: Email, subject: string) { console.log(`Sending to ${to}: ${subject}`);}const input = 'user@example.com';if (isValidEmail(input)) { sendEmail(input, 'Hello'); // ✓ Type guard narrows to Email}sendEmail(input, 'Hello'); // ✗ Error: string not Email// Positive integer typetype PositiveInt = number & { readonly __brand: 'PositiveInt' };function toPositiveInt(n: number): PositiveInt { if (!Number.isInteger(n) || n <= 0) { throw new Error('Not a positive integer'); } return n as PositiveInt;}function setArraySize(size: PositiveInt) { /* ... */ }
Example: Phantom types for state machines
// State machine with phantom typestype State = 'Initial' | 'Loaded' | 'Processed';interface DataContainer<S extends State> { data: unknown; _state?: S; // Phantom - not used at runtime}// State-specific operationsfunction load(): DataContainer<'Loaded'> { return { data: 'loaded data' };}function process( container: DataContainer<'Loaded'>): DataContainer<'Processed'> { return { data: 'processed' };}function save(container: DataContainer<'Processed'>) { console.log('Saving:', container.data);}// Type-safe workflowconst initial: DataContainer<'Initial'> = { data: null };const loaded = load();const processed = process(loaded);save(processed); // ✓ OKsave(loaded); // ✗ Error: 'Loaded' not assignable to 'Processed'process(processed); // ✗ Error: 'Processed' not assignable to 'Loaded'
Note: Brand types enable nominal typing in TypeScript's
structural type system. Use them for domain primitives (IDs, emails, URLs), validated data, and state machine
transitions. The runtime cost is zero since brands are compile-time only.
8.6 Type-only Imports and Exports
Syntax
Purpose
Behavior
Benefit
import type
Import only for type checking
Erased during compilation - no runtime code
Avoid circular dependencies, smaller bundles
export type
Export only type information
Type-only export, removed at runtime
Clear intent, separate concerns
inline type import
import { type T }
Mix type and value imports
Flexibility, granular control
importsNotUsedAsValues
Compiler option for side effects
Control import preservation
Prevent unwanted side effects
Example: Type-only imports
// types.tsexport interface User { id: string; name: string;}export type UserId = string;export const DEFAULT_USER: User = { id: '0', name: 'Guest'};// user.ts - type-only importimport type { User, UserId } from './types';// DEFAULT_USER not imported - no runtime dependencyfunction processUser(user: User): UserId { return user.id;}// ✗ Error: Cannot use type import as value// const guest = DEFAULT_USER;
Example: Inline type imports (TS 4.5+)
// Mixed imports - types and valuesimport { createUser, type User, type UserId } from './types';// ^^^^^^^^^^ ^^^^ ^^^^^^^^^^^^// value type type// Can use createUser at runtimeconst newUser = createUser();// Can use User and UserId for typingfunction validate(user: User): UserId { return user.id;}// Re-export with inline typeexport { createUser, type User } from './types';
Example: Breaking circular dependencies
// module-a.tsimport type { TypeB } from './module-b';// Only imports type, not runtime codeexport interface TypeA { b: TypeB; name: string;}export function createA(): TypeA { /* ... */ }// module-b.tsimport type { TypeA } from './module-a';// Circular reference OK with type-only importexport interface TypeB { a: TypeA; value: number;}export function createB(): TypeB { /* ... */ }// Without type-only imports, this would cause circular dependency error
Example: Type-only exports
// api-types.ts - pure type definitionsexport type { User, Product, Order } from './models';// Only export types, not any runtime code// Re-export types from multiple sourcesexport type { RequestHandler, Middleware} from './handlers';export type { Config, Environment} from './config';// Can be imported in consuming codeimport type { User, Product, RequestHandler } from './api-types';
Note: Use import type when you only need types for annotations. This is especially
important for:
Breaking circular dependencies between modules
Importing from libraries where you don't want side effects
Reducing bundle size by avoiding unused imports
Making dependencies explicit in type-only contexts
Enable "verbatimModuleSyntax": true in tsconfig for stricter type import checking.
Warning: Type-only imports are erased at runtime. Don't use import type for:
Classes you need to instantiate
Functions you need to call
Values you need to reference
Modules with required side effects (initialization code)
Advanced Type Manipulation Summary
Mapped types transform object properties with key remapping for prefixes,
filters, and transformations
Template literal types enable string manipulation and pattern matching at
the type level
Recursive types handle nested structures like trees, JSON, and deep
transformations (limited by compiler depth)
Higher-order types provide type-level programming with functions,
composition, and computation
Brand types add nominal typing to distinguish structurally identical types
for domain modeling
string, number, boolean, symbol, undefined, object, function
instanceof
x instanceof Class
Check if object is instance of a class
Specific class type
typeof null
typeof x === 'object'
⚠️ Returns 'object' for null (JavaScript quirk)
Need additional null check
typeof array
typeof [] === 'object'
⚠️ Arrays return 'object', use Array.isArray()
Use Array.isArray() instead
Example: typeof type guards
function processValue(value: string | number | boolean) { if (typeof value === 'string') { // Type narrowed to string return value.toUpperCase(); } if (typeof value === 'number') { // Type narrowed to number return value.toFixed(2); } // Type narrowed to boolean return value ? 'yes' : 'no';}// Function type checkfunction callIfFunction(fn: unknown) { if (typeof fn === 'function') { // Type narrowed to Function fn(); }}// Handle null and objectfunction processObject(value: unknown) { if (typeof value === 'object' && value !== null) { // Type narrowed to object (excluding null) console.log('Is object:', value); }}
Example: instanceof type guards
class Dog { bark() { console.log('Woof!'); }}class Cat { meow() { console.log('Meow!'); }}function makeSound(animal: Dog | Cat) { if (animal instanceof Dog) { // Type narrowed to Dog animal.bark(); } else { // Type narrowed to Cat animal.meow(); }}// Built-in classesfunction processDate(value: Date | string) { if (value instanceof Date) { // Type narrowed to Date return value.toISOString(); } // Type narrowed to string return new Date(value).toISOString();}// Error handlingfunction handleError(error: unknown) { if (error instanceof Error) { // Type narrowed to Error console.error(error.message, error.stack); } else { console.error('Unknown error:', error); }}
9.2 User-Defined Type Guards and Predicates
Feature
Syntax
Description
Use Case
Type Predicate
x is Type
Custom function returning type predicate - tells TS about narrowing
Complex validation logic
Return Type
function(x): x is T
Return type must be boolean with is predicate
Type guard function signature
Parameter Name
paramName is Type
Must match parameter name in function signature
Type guard reference
Narrowing Scope
After guard call
Type narrowed only in if/else blocks after guard check
Control flow analysis
Example: Basic type predicates
interface Fish { swim: () => void;}interface Bird { fly: () => void;}// User-defined type guardfunction isFish(animal: Fish | Bird): animal is Fish { return (animal as Fish).swim !== undefined;}function move(animal: Fish | Bird) { if (isFish(animal)) { // Type narrowed to Fish animal.swim(); } else { // Type narrowed to Bird animal.fly(); }}// String validation guardfunction isValidEmail(str: string): str is string { return /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(str);}function sendEmail(email: string) { if (isValidEmail(email)) { // Type still string, but validated console.log(`Sending to ${email}`); } else { throw new Error('Invalid email'); }}
Example: Narrowing unknown types
// Check if value is stringfunction isString(value: unknown): value is string { return typeof value === 'string';}// Check if value is numberfunction isNumber(value: unknown): value is number { return typeof value === 'number' && !isNaN(value);}// Check if value is arrayfunction isArray<T>(value: unknown): value is T[] { return Array.isArray(value);}// Complex object validationinterface User { id: number; name: string; email: string;}function isUser(value: unknown): value is User { return ( typeof value === 'object' && value !== null && 'id' in value && 'name' in value && 'email' in value && typeof (value as any).id === 'number' && typeof (value as any).name === 'string' && typeof (value as any).email === 'string' );}// Usagefunction processData(data: unknown) { if (isUser(data)) { // Type narrowed to User console.log(data.name, data.email); }}
Example: Generic type guards
// Generic array element guardfunction isArrayOf<T>( value: unknown, guard: (item: unknown) => item is T): value is T[] { return Array.isArray(value) && value.every(guard);}// Usage with primitivesconst data: unknown = [1, 2, 3];if (isArrayOf(data, isNumber)) { // Type narrowed to number[] data.forEach(n => console.log(n.toFixed(2)));}// Nullable type guardfunction isNonNullable<T>(value: T): value is NonNullable<T> { return value !== null && value !== undefined;}function processValue(value: string | null | undefined) { if (isNonNullable(value)) { // Type narrowed to string return value.toUpperCase(); }}
9.3 Assertion Functions and asserts Keyword
Feature
Syntax
Description
Behavior
asserts condition
asserts condition
Assert boolean condition - throws if false
Narrows type in rest of scope
asserts x is Type
asserts x is T
Assert parameter is specific type - throws if not
Type narrowing after call
Return Type
void or never
Assertion functions return void or throw (never returns)
Side effect: type narrowing
Throw Behavior
Must throw on failure
Function must throw or process.exit on assertion failure
Compiler assumes success
Example: Basic assertion functions
// Assert condition is truefunction assert(condition: unknown, msg?: string): asserts condition { if (!condition) { throw new Error(msg || 'Assertion failed'); }}function processValue(value: string | null) { assert(value !== null, 'Value cannot be null'); // After assert, type narrowed to string console.log(value.toUpperCase());}// Assert typefunction assertIsString(value: unknown): asserts value is string { if (typeof value !== 'string') { throw new Error('Value must be a string'); }}function handleInput(input: unknown) { assertIsString(input); // After assertion, type narrowed to string console.log(input.trim());}
Example: Non-null assertions
// Assert value is not null or undefinedfunction assertNonNullable<T>( value: T, message?: string): asserts value is NonNullable<T> { if (value === null || value === undefined) { throw new Error(message || 'Value is null or undefined'); }}function getUser(id: string): User | null { // ... database lookup return null;}function displayUser(id: string) { const user = getUser(id); assertNonNullable(user, 'User not found'); // Type narrowed from User | null to User console.log(user.name, user.email);}// Assert array has elementsfunction assertNotEmpty<T>( array: T[], message?: string): asserts array is [T, ...T[]] { if (array.length === 0) { throw new Error(message || 'Array is empty'); }}function processItems(items: string[]) { assertNotEmpty(items); // Type narrowed to [string, ...string[]] (non-empty tuple) const first = items[0]; // Type is string, not string | undefined}
Example: Complex type assertions
interface User { id: number; name: string; email: string;}// Assert object matches interfacefunction assertIsUser(value: unknown): asserts value is User { if ( typeof value !== 'object' || value === null || typeof (value as any).id !== 'number' || typeof (value as any).name !== 'string' || typeof (value as any).email !== 'string' ) { throw new Error('Invalid user object'); }}// API response handlingasync function fetchUser(id: string): Promise<User> { const response = await fetch(`/api/users/${id}`); const data: unknown = await response.json(); assertIsUser(data); // Type narrowed to User after assertion return data;}// Discriminated union assertiontype Success = { status: 'success'; data: string };type Failure = { status: 'error'; error: string };type Result = Success | Failure;function assertSuccess(result: Result): asserts result is Success { if (result.status !== 'success') { throw new Error('Expected success result'); }}function handleResult(result: Result) { assertSuccess(result); // Type narrowed to Success console.log(result.data);}
Note: Use type predicates (x is T) when you want to check and
branch. Use assertion functions (asserts x is T) when failure should terminate
execution. Assertions are great for preconditions and invariants.
9.4 Discriminated Union Type Guards
Component
Description
Example
Purpose
Discriminant Property
Common literal property across union members
type: 'success' | 'error'
Distinguish union variants
Literal Type
Unique literal value per variant
'success', 'error'
Enable type narrowing
Switch Statement
Pattern match on discriminant
switch(value.type) { ... }
Exhaustive checking
Exhaustiveness Check
Ensure all cases handled
const _: never = value;
Compile-time completeness
Example: Basic discriminated unions
// Tagged union with discriminant propertytype Shape = | { kind: 'circle'; radius: number } | { kind: 'rectangle'; width: number; height: number } | { kind: 'square'; size: number };function getArea(shape: Shape): number { switch (shape.kind) { case 'circle': // Type narrowed to { kind: 'circle'; radius: number } return Math.PI * shape.radius ** 2; case 'rectangle': // Type narrowed to { kind: 'rectangle'; width: number; height: number } return shape.width * shape.height; case 'square': // Type narrowed to { kind: 'square'; size: number } return shape.size ** 2; default: // Exhaustiveness check const _exhaustive: never = shape; throw new Error('Unknown shape'); }}// If statements work toofunction describe(shape: Shape): string { if (shape.kind === 'circle') { return `Circle with radius ${shape.radius}`; } if (shape.kind === 'rectangle') { return `Rectangle ${shape.width}x${shape.height}`; } return `Square ${shape.size}x${shape.size}`;}
Example: API response handling
// Success/Error discriminated uniontype ApiResponse<T> = | { status: 'loading' } | { status: 'success'; data: T } | { status: 'error'; error: string };function handleResponse<T>(response: ApiResponse<T>) { switch (response.status) { case 'loading': console.log('Loading...'); break; case 'success': // response.data is accessible and typed as T console.log('Data:', response.data); break; case 'error': // response.error is accessible and typed as string console.error('Error:', response.error); break; }}// Redux-style action typestype Action = | { type: 'INCREMENT'; by: number } | { type: 'DECREMENT'; by: number } | { type: 'RESET' } | { type: 'SET'; value: number };function reducer(state: number, action: Action): number { switch (action.type) { case 'INCREMENT': return state + action.by; case 'DECREMENT': return state - action.by; case 'RESET': return 0; case 'SET': return action.value; default: // Exhaustiveness check catches missing cases const _: never = action; return state; }}
interface Cat { meow: () => void; purr: () => void;}interface Dog { bark: () => void; fetch: () => void;}type Pet = Cat | Dog;function interact(pet: Pet) { if ('meow' in pet) { // Type narrowed to Cat pet.meow(); pet.purr(); } else { // Type narrowed to Dog pet.bark(); pet.fetch(); }}// Multiple property checksfunction feedPet(pet: Pet) { if ('bark' in pet) { // Dog console.log('Feeding dog'); } else if ('meow' in pet) { // Cat console.log('Feeding cat'); }}
Example: Complex union narrowing
type Vehicle = | { type: 'car'; wheels: 4; engine: string } | { type: 'bike'; wheels: 2; pedals: boolean } | { type: 'boat'; propeller: string };function describe(vehicle: Vehicle) { // Check for specific property if ('engine' in vehicle) { // Narrowed to car console.log(`Car with ${vehicle.wheels} wheels and ${vehicle.engine}`); } else if ('pedals' in vehicle) { // Narrowed to bike console.log(`Bike with ${vehicle.pedals ? 'pedals' : 'no pedals'}`); } else { // Narrowed to boat console.log(`Boat with ${vehicle.propeller}`); }}// API response with optional fieldsinterface SuccessResponse { success: true; data: string;}interface ErrorResponse { success: false; error: string; code?: number;}type Response = SuccessResponse | ErrorResponse;function handleResponse(response: Response) { if ('error' in response) { // Narrowed to ErrorResponse console.error('Error:', response.error); if ('code' in response && response.code) { console.error('Error code:', response.code); } } else { // Narrowed to SuccessResponse console.log('Data:', response.data); }}
Example: Combining with other guards
// Combining in with typeoffunction processValue(value: unknown) { if (typeof value === 'object' && value !== null) { if ('length' in value) { // Likely an array or array-like console.log('Length:', (value as any).length); } if ('toString' in value) { // Has toString method (most objects) console.log('String:', value.toString()); } }}// Check method existenceinterface Flyable { fly: () => void;}interface Swimmable { swim: () => void;}type Creature = Flyable | Swimmable | (Flyable & Swimmable);function move(creature: Creature) { const canFly = 'fly' in creature; const canSwim = 'swim' in creature; if (canFly && canSwim) { console.log('Can fly and swim'); creature.fly(); creature.swim(); } else if (canFly) { creature.fly(); } else { creature.swim(); }}
9.6 Control Flow Analysis and Type Narrowing
Technique
Description
Example Pattern
Reliability
Truthiness Narrowing
if(x) narrows from union with null/undefined/false/0/""
if (x) { /* x is truthy */ }
Simple but imprecise
Equality Narrowing
=== and !== narrow to/from specific values
if (x === null) { }
Precise
Assignment Narrowing
Type narrowed after assignment in same scope
x = 'string'; /* x is string */
Within scope only
Early Return
Return/throw narrows type in rest of function
if (!x) return; /* x is truthy */
Very reliable
Switch Fallthrough
Type narrowed through switch cases
switch(x.type) { case 'a': }
Reliable with discriminants
Exception Narrowing
throw removes possibility from control flow
if (!x) throw Error();
Reliable
Example: Truthiness and equality narrowing
// Truthiness narrowingfunction processValue(value: string | null | undefined) { if (value) { // Type narrowed to string (null and undefined are falsy) console.log(value.toUpperCase()); }}// Be careful with 0 and empty stringfunction processNumber(value: number | null) { if (value) { // ⚠️ 0 is falsy, so this might not work as expected console.log(value.toFixed(2)); } // Better: explicit null check if (value !== null) { // Type narrowed to number (includes 0) console.log(value.toFixed(2)); }}// Equality narrowingfunction compare(x: string | number, y: string | number) { if (x === y) { // Both are same type (string or number) console.log(x, y); }}// Discriminant equalitytype Result = | { success: true; value: number } | { success: false; error: string };function handle(result: Result) { if (result.success === true) { // Narrowed to success case console.log(result.value); } else { // Narrowed to error case console.log(result.error); }}
Example: Control flow with early returns
// Early return patternfunction processUser(user: User | null | undefined): string { // Guard clause if (!user) { return 'No user'; } // After guard, user is definitely User if (!user.email) { return 'No email'; } // user and user.email both exist return user.email.toLowerCase();}// Multiple guardsfunction validateInput(input: string | null | undefined): string { if (input === null) { throw new Error('Input is null'); } if (input === undefined) { throw new Error('Input is undefined'); } // Type narrowed to string if (input.length === 0) { throw new Error('Input is empty'); } // Type is string with length > 0 return input.trim();}// Nested narrowingfunction processData(data: unknown): number { if (typeof data !== 'object' || data === null) { throw new Error('Not an object'); } // Type narrowed to object if (!('value' in data)) { throw new Error('No value property'); } const value = (data as any).value; if (typeof value !== 'number') { throw new Error('Value is not a number'); } // Type narrowed to number return value;}
Example: Assignment and control flow
// Type narrowing through assignmentfunction demo() { let x: string | number; x = 'hello'; // Type narrowed to string console.log(x.toUpperCase()); x = 42; // Type narrowed to number console.log(x.toFixed(2));}// Conditional assignmentfunction processOptional(value?: string) { let result: string; if (value !== undefined) { result = value; // result is string, value is string } else { result = 'default'; } // result is always string here console.log(result.toUpperCase());}// Type guards with assignmentsfunction findUser(id: string): User | undefined { // ... lookup logic return undefined;}function displayUser(id: string) { const user = findUser(id); if (!user) { console.log('User not found'); return; } // user is definitely User here console.log(user.name); // Can reassign const admin: User | Admin = user; if ('permissions' in admin) { // Narrowed to Admin console.log(admin.permissions); }}
Example: Complex control flow scenarios
// Unreachable code detectionfunction handleValue(value: string | number) { if (typeof value === 'string') { return value.toUpperCase(); } // Type narrowed to number return value.toFixed(2); // This would be detected as unreachable // console.log('Never executed');}// Exhaustiveness with control flowtype Status = 'pending' | 'approved' | 'rejected';function processStatus(status: Status): string { if (status === 'pending') { return 'Waiting...'; } if (status === 'approved') { return 'Approved!'; } if (status === 'rejected') { return 'Rejected!'; } // TypeScript knows this is unreachable // status has type 'never' here const _exhaustive: never = status; return _exhaustive;}// Type narrowing with logical operatorsfunction process(value: string | null | undefined) { // Using || for defaults const str = value || 'default'; // Type is string // Using ?? for null/undefined only const str2 = value ?? 'default'; // Type is string // Using && for chaining const result = value && value.toUpperCase(); // Type is string | null | undefined | false}
Note: TypeScript's control flow analysis tracks type narrowing through:
Conditional statements (if/else, switch, ternary)
Early exits (return, throw, break, continue)
Type guards (typeof, instanceof, in, user-defined)
Assignments within the same scope
Enable strictNullChecks for better control flow analysis.
Warning: Control flow analysis doesn't work across:
Function boundaries - type guards don't propagate to inner functions
Async boundaries - type narrowing lost after await
typeof/instanceof - Built-in guards for primitives and classes
Type predicates (x is T) - Custom guards that return boolean
Assertion functions (asserts x is T) - Guards that throw on failure
Discriminated unions - Use literal property to distinguish variants
in operator - Check property existence to narrow unions
Control flow - TypeScript tracks narrowing through conditionals and exits
10. Module System and Declaration Files
10.1 ES Module Syntax (import/export)
Syntax
Description
Example
Use Case
Named Export
Export specific values by name
export const value = 42;
Multiple exports from module
Default Export
Single main export per module
export default class X {}
Primary module export
Named Import
Import specific named exports
import { value } from './mod';
Selective imports
Default Import
Import default export
import Mod from './mod';
Main module import
Namespace Import
Import all exports as object
import * as Mod from './mod';
Bundle all exports
Re-export
Export from another module
export { value } from './mod';
Module aggregation
Side-effect Import
Import for side effects only
import './init';
Module initialization
Example: Named exports and imports
// math.ts - named exportsexport const PI = 3.14159;export const E = 2.71828;export function add(a: number, b: number): number { return a + b;}export function multiply(a: number, b: number): number { return a * b;}export interface MathResult { value: number; operation: string;}// main.ts - named importsimport { PI, E, add, multiply } from './math';console.log(PI, E);const sum = add(5, 3);const product = multiply(4, 2);// Import with aliasimport { add as sum, multiply as times } from './math';// Import specific itemsimport { PI } from './math';// Import type onlyimport type { MathResult } from './math';
Example: Default exports and mixed exports
// user.ts - default exportexport default class User { constructor(public name: string) {} greet() { return `Hello, ${this.name}`; }}// Alternative: export after declarationclass User { constructor(public name: string) {}}export default User;// app.ts - import defaultimport User from './user';const user = new User('Alice');// mixed.ts - both default and named exportsexport default class Database { connect() { /* ... */ }}export const DB_VERSION = '1.0';export function createConnection() { /* ... */ }// Import bothimport Database, { DB_VERSION, createConnection } from './mixed';// Can rename default importimport DB from './mixed';
Example: Re-exports and barrel exports
// models/user.tsexport interface User { id: string; name: string;}// models/product.tsexport interface Product { id: string; price: number;}// models/index.ts - barrel exportexport { User } from './user';export { Product } from './product';export * from './order'; // Re-export all// Or rename during re-exportexport { User as UserModel } from './user';// app.ts - import from barrelimport { User, Product } from './models';// Re-export with modificationsexport { User } from './user';export type { Product } from './product'; // Type-only re-exportexport { Order as PurchaseOrder } from './order';
Example: Namespace imports and dynamic imports
// utils.tsexport const version = '1.0';export function format(s: string) { return s.toUpperCase(); }export function parse(s: string) { return s.toLowerCase(); }// Import entire module as namespaceimport * as Utils from './utils';console.log(Utils.version);const formatted = Utils.format('hello');// Dynamic import (returns Promise)async function loadModule() { const utils = await import('./utils'); console.log(utils.version); utils.format('test');}// Conditional loadingif (condition) { import('./feature').then(module => { module.initialize(); });}// Dynamic import with typetype UtilsModule = typeof import('./utils');const module: UtilsModule = await import('./utils');
// Without esModuleInterop (old style)import * as express from 'express';const app = express(); // Works// With esModuleInterop: true (recommended)import express from 'express';const app = express(); // Works like CommonJS// require() in TypeScript (not recommended)const fs = require('fs'); // Type: anyconst fs: typeof import('fs') = require('fs'); // With types// Import assignment (legacy)import fs = require('fs');fs.readFileSync('/path');// Type-only import for CommonJS typesimport type { Express } from 'express';const app: Express = createExpressApp();
Example: TypeScript compiled to CommonJS
// source.ts - TypeScript sourceexport const value = 42;export default class MyClass {}// Compiled to CommonJS (target: "commonjs")"use strict";Object.defineProperty(exports, "__esModule", { value: true });exports.value = void 0;exports.value = 42;class MyClass {}exports.default = MyClass;// __esModule flag helps with interop// TypeScript/Babel use it to detect ES modules// Import in Node.jsconst mod = require('./source');console.log(mod.value); // 42console.log(mod.default); // MyClass (with esModuleInterop)// With esModuleInterop, can use:const MyClass = require('./source').default;
Note: Enable esModuleInterop and allowSyntheticDefaultImports in
tsconfig.json for better CommonJS compatibility. This allows import React from 'react' instead of
import * as React from 'react'.
10.3 Module Resolution Strategies (Node, Classic)
Strategy
Algorithm
Use Case
File Extensions
Node (Node10)
Mimics Node.js require() resolution
Most common, npm packages
.ts, .tsx, .d.ts, .js, .jsx
Node16/NodeNext
Node.js ESM resolution with package.json
Modern Node.js ESM projects
Respects "type": "module"
Classic
Legacy pre-1.6 resolution
Rarely used, backwards compat
.ts, .d.ts only
Bundler
Bundler-like resolution (Webpack/Vite)
SPA applications with bundlers
Assumes bundler handles resolution
Example: Node resolution strategy
// For: import { x } from './module';// TypeScript searches in order:// 1. Relative import './module'./module.ts./module.tsx./module.d.ts./module/package.json (check "types" field)./module/index.ts./module/index.tsx./module/index.d.ts// 2. Non-relative import 'lodash'// From /root/src/app.ts:/root/src/node_modules/lodash.ts/root/src/node_modules/lodash.tsx/root/src/node_modules/lodash.d.ts/root/src/node_modules/lodash/package.json ("types" field)/root/src/node_modules/lodash/index.ts/root/src/node_modules/lodash/index.d.ts/root/node_modules/lodash/.../node_modules/lodash/...// 3. Check @types/root/src/node_modules/@types/lodash.d.ts/root/src/node_modules/@types/lodash/index.d.ts/root/node_modules/@types/lodash/.../node_modules/@types/lodash/...
Example: Node16/NodeNext resolution
// package.json configuration{ "type": "module", // Use ESM "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }, "./utils": { "types": "./dist/utils.d.ts", "import": "./dist/utils.js" } }}// With Node16/NodeNext, must use extensions// ✗ Error - missing extensionimport { x } from './module';// ✓ Correct - with extensionimport { x } from './module.js'; // .js even for .ts files!// For directories, must use indeximport { y } from './utils/index.js';// Package importsimport pkg from 'package'; // Uses package.json "exports"import sub from 'package/submodule'; // Must be in exports map
Example: Path mapping and baseUrl
// tsconfig.json{ "compilerOptions": { "baseUrl": "./src", "paths": { "@/*": ["./*"], "@components/*": ["components/*"], "@utils/*": ["utils/*"], "@models": ["models/index"] } }}// Usage with path mappingimport { Button } from '@components/Button';import { User } from '@models';import { format } from '@utils/string';import config from '@/config';// Without mapping (relative paths)import { Button } from '../../components/Button';import { User } from '../../models/index';import { format } from '../utils/string';import config from './config';// Note: Bundlers need to be configured to understand paths// Webpack: use tsconfig-paths-webpack-plugin// Vite: use resolve.alias
Warning: Path mapping in tsconfig.json is compile-time only. For Node.js runtime, use a tool
like tsconfig-paths or configure your bundler. Node16/NodeNext resolution doesn't support paths
mapping.
10.4 Declaration Files (.d.ts) and Type Declarations
Feature
Syntax
Purpose
Use Case
Declaration File
.d.ts
Type definitions without implementation
Type libraries, JS libraries
declare var/let/const
declare const x: T;
Declare global variable
Global variables from scripts
declare function
declare function fn(): T;
Declare global function
Global functions
declare class
declare class C {}
Declare class structure
Classes from JS
declare namespace
declare namespace N {}
Group related declarations
Library namespaces
declare module
declare module 'pkg' {}
Module augmentation
Add types to external modules
Example: Basic declaration file
// types.d.ts - declaration file// Declare global variabledeclare const VERSION: string;declare let currentUser: User | null;// Declare global functiondeclare function $(selector: string): Element;declare function fetch(url: string): Promise<Response>;// Declare classdeclare class EventEmitter { on(event: string, handler: Function): void; emit(event: string, ...args: any[]): void;}// Declare interface (no declare needed)interface User { id: string; name: string;}// Declare type aliastype ID = string | number;// Declare enumdeclare enum Color { Red, Green, Blue}// Usage in .ts files (no import needed for global declarations)console.log(VERSION);const el = $('div');const user: User = { id: '1', name: 'Alice' };
// tsconfig.json{ "compilerOptions": { "declaration": true, // Generate .d.ts files "declarationMap": true, // Generate .d.ts.map for navigation "emitDeclarationOnly": false, // Emit both .js and .d.ts "declarationDir": "./types" // Output directory for .d.ts }}// source.tsexport interface User { id: string; name: string;}export function createUser(name: string): User { return { id: Math.random().toString(), name };}export default class UserManager { private users: User[] = []; add(user: User): void { this.users.push(user); }}// Generated: source.d.tsexport interface User { id: string; name: string;}export declare function createUser(name: string): User;export default class UserManager { private users; add(user: User): void;}
Example: @types packages
// Install type definitions// npm install --save-dev @types/node// npm install --save-dev @types/express// npm install --save-dev @types/react// @types/node provides Node.js typesimport * as fs from 'fs';import * as path from 'path';fs.readFileSync('./file.txt'); // Types available// @types/express provides Express typesimport express from 'express';const app = express();// Some packages include their own types// package.json has "types" fieldimport lodash from 'lodash-es'; // Has built-in types// Check if types exist: https://www.npmjs.com/~types// Or search: npm search @types/package-name// Override @types with local declarations// Create: node_modules/@types/custom/index.d.ts// Or configure typeRoots in tsconfig.json{ "compilerOptions": { "typeRoots": ["./types", "./node_modules/@types"] }}
10.5 Ambient Module Declarations
Pattern
Syntax
Purpose
Scope
Ambient Module
declare module 'name' {}
Provide types for untyped modules
Project-wide
Wildcard Module
declare module '*.css' {}
Type non-JS imports (CSS, images, etc.)
Asset imports
Module Augmentation
declare module 'pkg' { export ... }
Add types to existing module
Extend libraries
Global Augmentation
declare global {}
Add to global scope from module
Global extensions
Example: Ambient module declarations
// types.d.ts - ambient declarations// Declare module with no types availabledeclare module 'legacy-lib' { export function doSomething(value: any): any; export const VERSION: string;}// Now can importimport { doSomething, VERSION } from 'legacy-lib';// Minimal typing for quick fixesdeclare module 'no-types-available' { const content: any; export default content;}import lib from 'no-types-available';// Specific module path patterndeclare module '@company/*/config' { interface Config { apiKey: string; endpoint: string; } const config: Config; export default config;}import config from '@company/users/config';import config2 from '@company/products/config';
// Extend existing module with new exportsdeclare module 'express' { // Add custom properties to Request interface Request { user?: { id: string; name: string; }; startTime?: number; } // Add custom response methods interface Response { sendSuccess(data: any): void; sendError(message: string): void; }}// Now can use augmented typesimport { Request, Response } from 'express';app.use((req: Request, res: Response, next) => { req.startTime = Date.now(); // ✓ Type-safe if (req.user) { console.log(req.user.name); // ✓ Type-safe } res.sendSuccess({ ok: true }); // ✓ Type-safe});// Augment module from within a module// my-plugin.tsimport 'express';declare module 'express' { interface Request { customField: string; }}// Available after importimport './my-plugin';app.get('/', (req, res) => { console.log(req.customField); // ✓ Available});
Example: Global augmentation from module
// extensions.d.ts - module file with global augmentation// Must have at least one import/export to be a moduleexport {};// Augment global scopedeclare global { // Add to Window interface interface Window { myApp: { version: string; config: AppConfig; }; gtag: (command: string, ...args: any[]) => void; } // Add global variable var APP_ENV: 'development' | 'production'; // Add to Array prototype interface Array<T> { firstOrNull(): T | null; lastOrNull(): T | null; } // Add to String prototype interface String { toKebabCase(): string; toCamelCase(): string; }}// Usage anywhere in projectwindow.myApp.version; // ✓ Type-safeconsole.log(APP_ENV); // ✓ Type-safeconst arr = [1, 2, 3];const first = arr.firstOrNull(); // ✓ Type-safeconst str = 'hello-world';const kebab = str.toKebabCase(); // ✓ Type-safe
10.6 Triple-Slash Directives and Reference Types
Directive
Syntax
Purpose
Modern Alternative
/// <reference path="" />
Reference file by path
Include declaration file
Use import statements
/// <reference types="" />
Reference @types package
Include type package
Use types array in tsconfig
/// <reference lib="" />
Reference built-in lib
Include specific lib types
Use lib array in tsconfig
/// <reference no-default-lib="true" />
Exclude default lib
Custom lib definitions
Rarely needed
Example: Triple-slash path references (legacy)
// types.d.tsinterface User { id: string; name: string;}// main.ts - reference other file/// <reference path="./types.d.ts" />const user: User = { id: '1', name: 'Alice' };// Better modern approach: use importimport type { User } from './types';// Multiple references/// <reference path="./globals.d.ts" />/// <reference path="./utils.d.ts" />/// <reference path="./models.d.ts" />// Note: Triple-slash directives must be at top of file// Before any code or imports
Example: Reference types directive
// Include types from @types package/// <reference types="node" />// Now have Node.js types without explicit importconst fs: typeof import('fs') = require('fs');console.log(__dirname); // Global Node.js variable// Include jQuery types/// <reference types="jquery" />$('div').addClass('active'); // jQuery available globally// In declaration files (.d.ts)/// <reference types="react" />declare module 'my-react-library' { import { ComponentType } from 'react'; export const MyComponent: ComponentType<{}>;}// Modern approach in tsconfig.json{ "compilerOptions": { "types": ["node", "jest", "jquery"] }}
// Enum-like objectconst STATUS = { PENDING: 'pending', APPROVED: 'approved', REJECTED: 'rejected'} as const;type Status = typeof STATUS[keyof typeof STATUS];// Type: "pending" | "approved" | "rejected"function updateStatus(status: Status) { // status must be one of the literal values}updateStatus(STATUS.PENDING); // ✓ OKupdateStatus('pending'); // ✓ OKupdateStatus('invalid'); // ✗ Error// Configuration with constconst CONFIG = { api: { baseUrl: 'https://api.example.com', timeout: 5000, endpoints: { users: '/users', posts: '/posts' } }, features: { darkMode: true, notifications: false }} as const;// Extract typestype ApiEndpoint = typeof CONFIG.api.endpoints[keyof typeof CONFIG.api.endpoints];// Type: "/users" | "/posts"// Tuple with constconst point = [10, 20] as const;// Type: readonly [10, 20]function distance(p1: readonly [number, number], p2: readonly [number, number]) { return Math.sqrt((p2[0] - p1[0]) ** 2 + (p2[1] - p1[1]) ** 2);}distance(point, [30, 40] as const); // ✓ OK
11.2 satisfies Operator for Type Checking TS 4.9
Feature
Syntax
Description
vs Annotation
satisfies
value satisfies Type
Validate type without widening inference
Preserves literal types
Type Annotation
value: Type
Widen type to annotation
Loses literal types
Use Case
Type validation + inference
Get both type safety and narrow types
Best of both worlds
Example: satisfies vs type annotation
type Colors = 'red' | 'green' | 'blue';// Problem with type annotation - loses literal typesconst palette1: Record<Colors, string | number[]> = { red: '#ff0000', green: [0, 255, 0], blue: '#0000ff'};palette1.red.toUpperCase(); // ✗ Error: string | number[] doesn't have toUpperCasepalette1.green.map(x => x * 2); // ✗ Error: string | number[] doesn't have map// Solution with satisfies - preserves literal typesconst palette2 = { red: '#ff0000', green: [0, 255, 0], blue: '#0000ff'} satisfies Record<Colors, string | number[]>;palette2.red.toUpperCase(); // ✓ OK: red is stringpalette2.green.map(x => x * 2); // ✓ OK: green is number[]palette2.yellow = '#ffff00'; // ✗ Error: yellow not in Colors// satisfies validates structure but keeps narrow typestype RGB = [number, number, number];const color = [255, 0, 0] satisfies RGB;// Type: [number, number, number] (tuple, not array)color[0] = 100; // ✓ OKcolor[3] = 50; // ✗ Error: tuple only has 3 elements
Example: Practical satisfies patterns
// Route configuration with validationtype Route = { path: string; component: string; children?: Route[];};const routes = [ { path: '/', component: 'Home' }, { path: '/about', component: 'About', children: [ { path: '/about/team', component: 'Team' } ] }] satisfies Route[];// Type is preserved as literal array, not Route[]const homePath = routes[0].path; // Type: string, not widenedroutes[0].path = '/home'; // ✓ Can mutate// API response schema validationtype ApiResponse<T> = { status: 'success' | 'error'; data?: T; error?: string;};const response = { status: 'success', data: { id: 1, name: 'Alice' }} satisfies ApiResponse<{ id: number; name: string }>;if (response.status === 'success') { // response.data is { id: number; name: string }, not optional console.log(response.data.name); // ✓ OK}// Configuration with mixed typestype Config = { [key: string]: string | number | boolean;};const config = { host: 'localhost', port: 3000, ssl: true, debug: false} satisfies Config;// Preserves specific typesconst port: number = config.port; // ✓ OK: port is numberconst host: string = config.host; // ✓ OK: host is string
Example: Complex satisfies scenarios
// Event handlers with satisfiestype EventHandlers = { [K in string]: (...args: any[]) => void;};const handlers = { onClick: (e: MouseEvent) => console.log(e.clientX), onSubmit: (data: FormData) => console.log(data), onError: (error: Error) => console.error(error.message)} satisfies EventHandlers;// Type of handlers.onClick is (e: MouseEvent) => void, not (...args: any[]) => voidhandlers.onClick(new MouseEvent('click')); // ✓ Type-safe// Discriminated unions with satisfiestype Action = | { type: 'INCREMENT'; by: number } | { type: 'DECREMENT'; by: number } | { type: 'RESET' };const actions = [ { type: 'INCREMENT', by: 1 }, { type: 'DECREMENT', by: 5 }, { type: 'RESET' }] satisfies Action[];// Type preserved: each action has its specific shapeactions[0].by; // ✓ OK: numberactions[2].by; // ✗ Error: RESET doesn't have 'by'
Note: Use satisfies when you want:
Type validation without widening to the annotation type
To preserve literal types while ensuring structural compatibility
Autocomplete for object properties while maintaining narrow types
Both compile-time checking and runtime-friendly types
11.3 using Declaration for Resource Management TS 5.2
Feature
Syntax
Description
Disposal
using Declaration
using resource = init();
Auto-dispose resource when scope exits
Calls Symbol.dispose
await using
await using resource = init();
Async disposal when scope exits
Calls Symbol.asyncDispose
Disposable
Symbol.dispose
Interface for sync resource cleanup
Automatic cleanup
AsyncDisposable
Symbol.asyncDispose
Interface for async resource cleanup
Async cleanup
Example: Basic using declaration
// Implement Disposable interfaceclass FileHandle implements Disposable { constructor(private path: string) { console.log(`Opening ${path}`); } read() { return `Content of ${this.path}`; } [Symbol.dispose]() { console.log(`Closing ${this.path}`); // Cleanup logic here }}// Traditional approach - manual cleanupfunction readFileOld() { const file = new FileHandle('data.txt'); try { return file.read(); } finally { file[Symbol.dispose](); }}// With using - automatic cleanupfunction readFileNew() { using file = new FileHandle('data.txt'); return file.read(); // file[Symbol.dispose]() called automatically at scope exit}// Multiple resourcesfunction processFiles() { using file1 = new FileHandle('input.txt'); using file2 = new FileHandle('output.txt'); // Use both files const content = file1.read(); // Both disposed in reverse order (LIFO) // file2 disposed first, then file1}
Example: Async disposal with await using
// Implement AsyncDisposableclass DatabaseConnection implements AsyncDisposable { constructor(private connectionString: string) { console.log('Connecting...'); } async query(sql: string) { // Execute query return []; } async [Symbol.asyncDispose]() { console.log('Disconnecting...'); // Async cleanup await this.closeConnection(); } private async closeConnection() { // Wait for pending operations await new Promise(resolve => setTimeout(resolve, 100)); }}// Async resource managementasync function queryDatabase() { await using db = new DatabaseConnection('mongodb://localhost'); const results = await db.query('SELECT * FROM users'); // db[Symbol.asyncDispose]() called automatically return results;}// Multiple async resourcesasync function processData() { await using db = new DatabaseConnection('db1'); await using cache = new CacheConnection('redis://localhost'); const data = await db.query('SELECT * FROM data'); await cache.set('key', data); // Disposed in reverse order: cache, then db}
Example: Practical resource management patterns
// Lock/Mutex patternclass Lock implements Disposable { private locked = false; acquire() { if (this.locked) throw new Error('Already locked'); this.locked = true; } [Symbol.dispose]() { this.locked = false; }}function criticalSection() { using lock = new Lock(); lock.acquire(); // Critical code here console.log('In critical section'); // Lock automatically released at scope exit}// Transaction patternclass Transaction implements AsyncDisposable { private committed = false; async execute(sql: string) { // Execute SQL } async commit() { this.committed = true; // Commit transaction } async [Symbol.asyncDispose]() { if (!this.committed) { console.log('Rolling back transaction'); // Rollback if not committed } }}async function transferMoney(from: string, to: string, amount: number) { await using tx = new Transaction(); await tx.execute(`UPDATE accounts SET balance = balance - ${amount} WHERE id = '${from}'`); await tx.execute(`UPDATE accounts SET balance = balance + ${amount} WHERE id = '${to}'`); await tx.commit(); // Transaction disposed: commit() called, so no rollback}// Cleanup multiple resourcesclass ResourcePool implements Disposable { private resources: Disposable[] = []; add(resource: Disposable) { this.resources.push(resource); } [Symbol.dispose]() { // Dispose all resources in reverse order for (let i = this.resources.length - 1; i >= 0; i--) { this.resources[i][Symbol.dispose](); } }}
Note: The using declaration implements explicit resource
management (similar to C#'s using, Python's with, Java's try-with-resources). Resources are disposed in
reverse declaration order (LIFO).
11.4 Decorator Metadata and Reflect API TS 5.0
Feature
Status
Description
Use Case
Decorator Metadata
Stage 3 Proposal
Attach metadata to decorated declarations
Reflection, DI frameworks
Symbol.metadata
New symbol
Access decorator metadata
Runtime reflection
context.metadata
Decorator context
Store metadata during decoration
Framework integration
Reflect Metadata API
Library (reflect-metadata)
Design-time type reflection
DI, validation
Example: Modern decorators with metadata (TS 5.0+)
// Enable in tsconfig.json:// "experimentalDecorators": false (use new decorators)// Class decorator with metadatafunction Entity(tableName: string) { return function<T extends { new(...args: any[]): {} }>( target: T, context: ClassDecoratorContext ) { // Store metadata context.metadata[Symbol.for('tableName')] = tableName; return class extends target { static tableName = tableName; }; };}// Method decorator with metadatafunction Route(path: string, method: string) { return function( target: Function, context: ClassMethodDecoratorContext ) { // Store route metadata if (!context.metadata.routes) { context.metadata.routes = []; } (context.metadata.routes as any[]).push({ path, method, handler: context.name }); };}@Entity('users')class User { @Route('/users', 'GET') getUsers() { return []; } @Route('/users/:id', 'GET') getUser(id: string) { return { id }; }}// Access metadataconst metadata = (User as any)[Symbol.metadata];console.log(metadata[Symbol.for('tableName')]); // 'users'console.log(metadata.routes); // Array of route configs
Example: Reflect Metadata API (legacy approach)
// npm install reflect-metadata// Enable in tsconfig.json:// "experimentalDecorators": true// "emitDecoratorMetadata": trueimport 'reflect-metadata';// Define metadata keysconst REQUIRED_KEY = Symbol('required');const VALIDATION_KEY = Symbol('validation');// Property decorator using Reflectfunction Required(target: any, propertyKey: string) { Reflect.defineMetadata(REQUIRED_KEY, true, target, propertyKey);}function MinLength(length: number) { return function(target: any, propertyKey: string) { Reflect.defineMetadata(VALIDATION_KEY, { minLength: length }, target, propertyKey); };}class CreateUserDto { @Required @MinLength(3) username!: string; @Required email!: string; age?: number;}// Validation using metadatafunction validate(obj: any): string[] { const errors: string[] = []; for (const key of Object.keys(obj)) { // Check required const isRequired = Reflect.getMetadata(REQUIRED_KEY, obj, key); if (isRequired && !obj[key]) { errors.push(`${key} is required`); } // Check validation rules const validation = Reflect.getMetadata(VALIDATION_KEY, obj, key); if (validation?.minLength && obj[key]?.length < validation.minLength) { errors.push(`${key} must be at least ${validation.minLength} characters`); } } return errors;}const dto = new CreateUserDto();dto.username = 'ab'; // Too shortconst errors = validate(dto); // ['email is required', 'username must be at least 3 characters']
Example: Design-time type reflection
import 'reflect-metadata';// Automatic type reflection with emitDecoratorMetadatafunction Log(target: any, propertyKey: string) { // Get design-time type information const type = Reflect.getMetadata('design:type', target, propertyKey); const paramTypes = Reflect.getMetadata('design:paramtypes', target, propertyKey); const returnType = Reflect.getMetadata('design:returntype', target, propertyKey); console.log(`${propertyKey} type:`, type?.name); console.log(`${propertyKey} params:`, paramTypes?.map((t: any) => t.name)); console.log(`${propertyKey} return:`, returnType?.name);}class Service { @Log processUser(user: User, id: number): Promise<boolean> { return Promise.resolve(true); }}// Logs: // processUser type: Function// processUser params: ['User', 'Number']// processUser return: Promise// Dependency injection with type reflectionconst INJECT_KEY = Symbol('inject');function Injectable() { return function<T extends { new(...args: any[]): {} }>(target: T) { // Store parameter types for DI const paramTypes = Reflect.getMetadata('design:paramtypes', target) || []; Reflect.defineMetadata(INJECT_KEY, paramTypes, target); return target; };}@Injectable()class UserService { constructor(private db: DatabaseService, private logger: LoggerService) {}}// DI Containerclass Container { resolve<T>(target: new (...args: any[]) => T): T { const params = Reflect.getMetadata(INJECT_KEY, target) || []; const instances = params.map((param: any) => this.resolve(param)); return new target(...instances); }}const container = new Container();const userService = container.resolve(UserService);
11.5 Import Attributes and JSON Modules TS 5.3
Feature
Syntax
Description
Use Case
Import Attributes
import x from 'mod' with { }
Specify module type/attributes on import
JSON, CSS modules
JSON Modules
import json from './data.json' with { type: 'json' }
Import JSON as module
Static JSON import
resolveJsonModule
tsconfig option
Enable JSON module resolution
Type-safe JSON imports
Import Assertions
assert { type: 'json' }
Legacy syntax (pre-TS 5.3)
Deprecated
Example: JSON module imports
// tsconfig.json{ "compilerOptions": { "module": "esnext", "resolveJsonModule": true, "esModuleInterop": true }}// data.json{ "name": "MyApp", "version": "1.0.0", "config": { "apiUrl": "https://api.example.com", "timeout": 5000 }}// Modern syntax (TS 5.3+) with import attributesimport data from './data.json' with { type: 'json' };console.log(data.name); // ✓ Type-safe: stringconsole.log(data.config.timeout); // ✓ Type-safe: number// Type is inferred from JSON structuretype DataType = typeof data;// {// name: string;// version: string;// config: { apiUrl: string; timeout: number; }// }// Legacy syntax (pre-TS 5.3) - still worksimport legacyData from './data.json' assert { type: 'json' };// Without attributes (requires resolveJsonModule)import simpleData from './data.json';// Dynamic import with attributesconst dynamicData = await import('./data.json', { with: { type: 'json' }});
Example: CSS and other module types
// CSS Modules with import attributesimport styles from './styles.css' with { type: 'css' };// In browsers supporting CSS modulesdocument.adoptedStyleSheets = [styles];// TypeScript declaration for CSS modulesdeclare module '*.css' { const stylesheet: CSSStyleSheet; export default stylesheet;}// Web Assembly modulesimport wasmModule from './module.wasm' with { type: 'webassembly' };// Custom module types (requires bundler support)import config from './config.toml' with { type: 'toml' };import data from './data.yaml' with { type: 'yaml' };// Type declarations for custom formatsdeclare module '*.toml' { const content: Record<string, any>; export default content;}declare module '*.yaml' { const content: Record<string, any>; export default content;}
Example: Typed JSON imports
// config.json with strict typing{ "database": { "host": "localhost", "port": 5432, "name": "mydb" }, "features": { "auth": true, "logging": true }}// Import with type assertionimport configData from './config.json' with { type: 'json' };// Define expected structureinterface Config { database: { host: string; port: number; name: string; }; features: { auth: boolean; logging: boolean; };}// Validate at runtime (optional)function isValidConfig(data: unknown): data is Config { return ( typeof data === 'object' && data !== null && 'database' in data && 'features' in data );}// Use config with type safetyconst config: Config = configData;console.log(config.database.port); // ✓ Type: number// Array of dataimport users from './users.json' with { type: 'json' };interface User { id: number; name: string; email: string;}const typedUsers: User[] = users;typedUsers.forEach(user => { console.log(user.name.toUpperCase()); // ✓ Type-safe});
11.6 Node16/NodeNext Module Resolution
Feature
Description
Requirement
Behavior
Node16/NodeNext
Modern Node.js ESM resolution
File extensions required
Matches Node.js behavior
package.json "type"
Specify module system
"module" or "commonjs"
Determines .js meaning
Conditional Exports
Export different entry points
"exports" field
import vs require
File Extensions
Must include in imports
.js for .ts files
Mirrors runtime behavior
Example: Node16/NodeNext configuration
// package.json for ESM project{ "name": "my-package", "version": "1.0.0", "type": "module", // All .js files are ESM "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }, "./utils": { "types": "./dist/utils.d.ts", "import": "./dist/utils.js" } }}// tsconfig.json{ "compilerOptions": { "target": "ES2022", "module": "Node16", // or "NodeNext" "moduleResolution": "Node16", // or "NodeNext" "outDir": "./dist", "declaration": true }}// src/utils.tsexport function formatDate(date: Date): string { return date.toISOString();}// src/index.ts - MUST use .js extensionimport { formatDate } from './utils.js'; // ✓ .js even though source is .tsimport { formatDate } from './utils'; // ✗ Error: must include extension// For directories, use indeximport * as utils from './utils/index.js'; // ✓ OK// Relative imports need extensionsimport type { User } from './types.js'; // ✓ OKimport type { User } from './types'; // ✗ Error
Example: Dual package (CJS + ESM)
// package.json for dual package{ "name": "dual-package", "version": "1.0.0", "type": "module", "main": "./dist/index.cjs", // CJS entry point "module": "./dist/index.js", // ESM entry point "types": "./dist/index.d.ts", "exports": { ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", // ESM "require": "./dist/index.cjs", // CJS "default": "./dist/index.js" } }, "files": ["dist"]}// Build with both formats// tsconfig.json for ESM build{ "compilerOptions": { "module": "Node16", "outDir": "./dist" }}// Build CJS separately or use bundler// Output:// dist/index.js (ESM)// dist/index.cjs (CJS)// dist/index.d.ts (types)// Consumers can use either// ESM consumer:import pkg from 'dual-package';// CJS consumer:const pkg = require('dual-package');
Example: Subpath exports and patterns
// package.json with detailed exports{ "name": "my-lib", "type": "module", "exports": { // Main entry ".": { "types": "./dist/index.d.ts", "import": "./dist/index.js", "require": "./dist/index.cjs" }, // Specific subpaths "./utils": "./dist/utils.js", "./helpers": "./dist/helpers.js", // Pattern exports (all files in utils/) "./utils/*": { "types": "./dist/utils/*.d.ts", "import": "./dist/utils/*.js" }, // Conditional exports "./server": { "node": "./dist/server.js", "default": "./dist/server-browser.js" }, // Block access to internal files "./internal/*": null, // Package.json access "./package.json": "./package.json" }}// Usage:import lib from 'my-lib'; // Main entryimport { util } from 'my-lib/utils'; // Subpathimport { helper } from 'my-lib/utils/string.js'; // Patternimport server from 'my-lib/server'; // Conditionalimport internal from 'my-lib/internal/secret'; // ✗ Error: blocked
Note: Node16/NodeNext resolution is the modern standard for
Node.js projects. Key differences from classic Node resolution:
File extensions are required in imports
Use .js extension even when importing .ts files
package.json "type" field determines module system
Respects "exports" field for subpath exports
No automatic index.js resolution without explicit path
Warning: Node16/NodeNext is stricter than classic resolution:
Path mappings (tsconfig paths) are not supported
Must use .js extension in imports, not .ts
Directory imports require /index.js explicitly
Mixing ESM and CJS requires careful package.json configuration
TypeScript 5+ Features Summary
const type parameters - Preserve literal types in generic functions with
<const T>
satisfies operator - Type validation without widening, preserves literal
types
using declaration - Automatic resource disposal with Symbol.dispose
Decorator metadata - Attach metadata to decorators for reflection and DI
Import attributes - Specify module type with 'with { type }' syntax
Node16/NodeNext - Modern ESM resolution matching Node.js behavior
12. Asynchronous Programming and Promises
12.1 Promise<T> Types and async/await Syntax
Feature
Syntax
Description
Return Type
Promise<T>
Promise<string>
Generic type representing async operation result
T when awaited
async Function
async function() {}
Function that always returns Promise
Promise<ReturnType>
await
await promise
Wait for Promise to resolve, extract value
Unwrapped T from Promise<T>
Promise.resolve
Promise.resolve(value)
Create resolved Promise
Promise<T>
Promise.reject
Promise.reject(error)
Create rejected Promise
Promise<never>
Promise.all
Promise.all([p1, p2])
Wait for all Promises, return array
Promise<[T1, T2]>
Promise.race
Promise.race([p1, p2])
Return first settled Promise
Promise<T1 | T2>
Example: Basic Promise types
// Promise type annotationconst promise1: Promise<string> = Promise.resolve('hello');const promise2: Promise<number> = Promise.resolve(42);// Promise that might rejectfunction fetchData(): Promise<string> { return new Promise((resolve, reject) => { setTimeout(() => { if (Math.random() > 0.5) { resolve('Data loaded'); } else { reject(new Error('Failed to load')); } }, 1000); });}// Using Promises with then/catchfetchData() .then(data => console.log(data)) // data: string .catch(error => console.error(error)); // error: any// Promise chainingfunction getUser(id: string): Promise<User> { return fetch(`/api/users/${id}`) .then(response => response.json()) .then(data => data as User);}// Promise constructor typingconst customPromise = new Promise<number>((resolve, reject) => { resolve(42); // ✓ OK: number resolve('hello'); // ✗ Error: string not assignable to number});
Example: async/await syntax
// async function automatically wraps return in Promiseasync function getName(): Promise<string> { return 'Alice'; // Actually returns Promise<string>}async function getAge(): Promise<number> { return 25;}// await unwraps Promiseasync function displayUser() { const name = await getName(); // Type: string (not Promise<string>) const age = await getAge(); // Type: number console.log(`${name} is ${age} years old`);}// Multiple awaitsasync function loadData() { const users = await fetchUsers(); // User[] const posts = await fetchPosts(); // Post[] const comments = await fetchComments(); // Comment[] return { users, posts, comments };}// await with non-Promise values (no-op)async function demo() { const x = await 42; // Type: number (42 wrapped in Promise.resolve) const y = await 'hello'; // Type: string}
import { sum, fetchUser, User } from './math';describe('sum function', () => { it('should add two numbers', () => { const result: number = sum(2, 3); expect(result).toBe(5); }); it('should handle negative numbers', () => { expect(sum(-1, 1)).toBe(0); });});describe('fetchUser function', () => { it('should return a user object', async () => { const user: User = await fetchUser(1); expect(user).toHaveProperty('id'); expect(user).toHaveProperty('name'); expect(user).toHaveProperty('email'); expect(typeof user.id).toBe('number'); expect(typeof user.name).toBe('string'); }); it('should throw error for invalid id', async () => { await expect(fetchUser(-1)).rejects.toThrow('Invalid user ID'); });});// Testing genericsfunction identity<T>(value: T): T { return value;}describe('identity function', () => { it('should preserve type for string', () => { const result: string = identity('hello'); expect(result).toBe('hello'); }); it('should preserve type for number', () => { const result: number = identity(42); expect(result).toBe(42); }); it('should preserve type for object', () => { const obj = { name: 'John', age: 30 }; const result: typeof obj = identity(obj); expect(result).toEqual(obj); });});
15.2 Type-only Tests and expect-type Library
Library/Feature
Syntax
Description
Use Case
expect-type
npm i -D expect-type
Compile-time type assertions - no runtime code
Type-only testing
expectTypeOf
expectTypeOf(val).toEqualTypeOf<T>()
Assert exact type match
Verify precise types
toMatchTypeOf
expectTypeOf(val).toMatchTypeOf<T>()
Assert type is assignable to target
Check type compatibility
tsd
npm i -D tsd
Test TypeScript type definitions - .d.ts files
Library type definitions
dtslint
npm i -D dtslint
DefinitelyTyped testing tool
@types packages
@ts-expect-error
// @ts-expect-error
Assert that next line has type error
Negative type tests
Example: Type-only tests with expect-type
import { expectTypeOf } from 'expect-type';// Test function return typesfunction add(a: number, b: number): number { return a + b;}expectTypeOf(add).returns.toEqualTypeOf<number>();expectTypeOf(add).parameter(0).toEqualTypeOf<number>();expectTypeOf(add).parameters.toEqualTypeOf<[number, number]>();// Test generic typesfunction identity<T>(value: T): T { return value;}expectTypeOf(identity<string>).returns.toEqualTypeOf<string>();expectTypeOf(identity<number>).returns.toEqualTypeOf<number>();// Test type utilitiestype User = { id: number; name: string; email: string };type UserUpdate = Partial<User>;expectTypeOf<UserUpdate>().toMatchTypeOf<{ id?: number }>();expectTypeOf<UserUpdate>().toMatchTypeOf<{ name?: string }>();// Test object typesinterface Product { id: number; name: string; price: number;}const product: Product = { id: 1, name: 'Widget', price: 9.99 };expectTypeOf(product).toEqualTypeOf<Product>();expectTypeOf(product).toHaveProperty('id');expectTypeOf(product.id).toEqualTypeOf<number>();expectTypeOf(product.name).toEqualTypeOf<string>();// Test union and intersection typestype StringOrNumber = string | number;type NameAndAge = { name: string } & { age: number };expectTypeOf<StringOrNumber>().toMatchTypeOf<string>();expectTypeOf<StringOrNumber>().toMatchTypeOf<number>();expectTypeOf<NameAndAge>().toHaveProperty('name');expectTypeOf<NameAndAge>().toHaveProperty('age');
Example: Negative type tests
// Test that code produces type errorsfunction requiresNumber(n: number): void {}// @ts-expect-error - should fail with string argumentrequiresNumber('hello');// @ts-expect-error - should fail with undefinedrequiresNumber(undefined);// This should compile fine (no error)requiresNumber(42);// Test type narrowingfunction process(value: string | number) { if (typeof value === 'string') { // @ts-expect-error - toFixed doesn't exist on string value.toFixed(2); // This is fine value.toUpperCase(); }}// Test readonly enforcementinterface ReadonlyConfig { readonly apiKey: string;}const config: ReadonlyConfig = { apiKey: 'secret' };// @ts-expect-error - cannot assign to readonly propertyconfig.apiKey = 'new-secret';// Using expect-type for negative testsimport { expectTypeOf } from 'expect-type';expectTypeOf<string>().not.toEqualTypeOf<number>();expectTypeOf<{ a: string }>().not.toMatchTypeOf<{ b: number }>();// Test that types are NOT assignabletype Admin = { role: 'admin'; permissions: string[] };type User = { role: 'user'; name: string };expectTypeOf<Admin>().not.toMatchTypeOf<User>();expectTypeOf<User>().not.toMatchTypeOf<Admin>();
// Build time comparison for medium project (50k LOC)// TypeScript Compiler (tsc)// Time: ~45stsc --project tsconfig.json// Babel + @babel/preset-typescript// Time: ~35s (no type checking)babel src --out-dir dist --extensions ".ts,.tsx"// ts-loader (Webpack)// Time: ~40swebpack --mode production// ts-loader + transpileOnly + fork-ts-checker// Time: ~25s (parallel type checking)webpack --mode production// esbuild-loader (Webpack)// Time: ~5s (no type checking)webpack --mode production// esbuild (standalone)// Time: ~2s (no type checking)node esbuild.config.js// SWC// Time: ~3s (no type checking)swc src -d dist// Vite (esbuild)// Dev server start: <1s// Production build: ~5svite build// Recommendation:// Development: Vite or esbuild with separate tsc --noEmit --watch// Production: esbuild/SWC + tsc --noEmit in CI pipeline// Type safety: Always run tsc --noEmit for type checking
Note: Performance optimization best practices:
Compilation - Enable skipLibCheck, incremental, use project references for
monorepos
Build tools - Use esbuild/SWC for fast transpilation, run type checking
separately
Webpack - Use ts-loader with transpileOnly + fork-ts-checker, enable
filesystem cache
Vite - Best DX with instant HMR, use vite-plugin-checker for type checking
Tree shaking - Use ES modules, named exports, mark packages as side-effect
free
Analysis - Use webpack-bundle-analyzer or source-map-explorer to identify
bloat
// TypeScript-specific shortcuts in VS Code// NavigationF12 // Go to DefinitionCtrl+F12 // Go to ImplementationShift+F12 // Find All ReferencesAlt+F12 // Peek DefinitionCtrl+Shift+O // Go to Symbol in FileCtrl+T // Go to Symbol in Workspace// RefactoringF2 // Rename SymbolCtrl+. // Quick Fix / Code ActionsCtrl+Shift+R // Refactor menuAlt+Shift+F // Format Document// IntelliSenseCtrl+Space // Trigger SuggestCtrl+Shift+Space // Trigger Parameter HintsCtrl+I // Trigger Suggest (alternative)// Type InformationCtrl+K Ctrl+I // Show Hover (type info)// ImportsAlt+Shift+O // Organize ImportsCtrl+Shift+P // Command Palette → "Add Missing Import" → "Remove Unused Imports"// ErrorsF8 // Go to Next ErrorShift+F8 // Go to Previous ErrorCtrl+Shift+M // Show Problems Panel// TypeScript commands (Ctrl+Shift+P)"TypeScript: Restart TS Server""TypeScript: Select TypeScript Version""TypeScript: Go to Source Definition""TypeScript: Reload Project"
// Before - messy importsimport { z } from 'zod';import React from 'react';import { useState } from 'react';import './styles.css';import { formatDate } from '../utils';import type { User } from './types';import { api } from '@/api';// After - organized (Alt+Shift+O or save with organizeImports)import './styles.css';import React, { useState } from 'react';import { z } from 'zod';import { api } from '@/api';import { formatDate } from '../utils';import type { User } from './types';// VS Code settings for import organization{ "editor.codeActionsOnSave": { "source.organizeImports": "explicit" }, "typescript.preferences.importModuleSpecifier": "relative", "typescript.preferences.importModuleSpecifierEnding": "auto"}// Remove unused imports automaticallyimport { useState, useEffect, useMemo } from 'react'; // useMemo unused// After save (if configured):import { useState, useEffect } from 'react';// ESLint rule to enforce{ "rules": { "@typescript-eslint/no-unused-vars": ["error", { "vars": "all", "args": "after-used", "ignoreRestSiblings": false }] }}
IntelliSense - Auto-completion, parameter hints, type information, inlay
hints for productive coding
18. Error Handling and Debugging Techniques
18.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// ❌ Errorconst num: number = '42'; // Type 'string' is not assignable to type 'number'// ✅ Fixconst num: number = 42;// TS2345 - Wrong argument type// ❌ Errorfunction greet(name: string) { }greet(42); // Argument of type 'number' is not assignable to parameter of type 'string'// ✅ Fixgreet('Alice');// TS2339 - Property doesn't exist// ❌ Errorinterface User { name: string; }const user: User = { name: 'Alice' };console.log(user.age); // Property 'age' does not exist on type 'User'// ✅ Fix 1 - Add to interfaceinterface User { name: string; age?: number; }// ✅ Fix 2 - Type guardif ('age' in user) { console.log(user.age);}// TS2571 - Unknown type// ❌ Errorfunction process(data: unknown) { console.log(data.name); // Object is of type 'unknown'}// ✅ Fix - Type guardfunction process(data: unknown) { if (typeof data === 'object' && data !== null && 'name' in data) { console.log((data as { name: string }).name); }}// TS2769 - No matching overload// ❌ Errorfunction 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 typesadd(1, 2); // OKadd('1', '2'); // OK// TS2740 - Missing properties// ❌ Errorinterface Config { host: string; port: number; secure: boolean;}const config: Config = { host: 'localhost' }; // Missing 'port' and 'secure'// ✅ Fix - Provide all propertiesconst 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 annotationfunction 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 typetype 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 detailstsc --explainFiles// Use --noErrorTruncation for full error messagestsc --noErrorTruncation
18.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 broadconst data = []; // Inferred as: never[]data.push(1); // ❌ Error// Solution: Add type annotationconst data: number[] = [];data.push(1); // ✅ OK// 2. Break down complex types// Problem: Complex union causing issuestype 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 separatelyfunction 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.0const result = someLibraryFunction('param');// 4. Type assertions when you know better than compilerconst canvas = document.getElementById('canvas'); // HTMLElement | null// You know it's a canvas elementconst ctx = (canvas as HTMLCanvasElement).getContext('2d');// 5. Diagnostic types to inspect complex typestype 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 typestype IsAssignable<T, U> = T extends U ? true : false;type Test1 = IsAssignable<string, string | number>; // truetype 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 typestype Debug<T> = { [K in keyof T]: T[K] } & {};// Example: Understanding generic inferencefunction map<T, U>(array: T[], fn: (item: T) => U): U[] { return array.map(fn);}// Hover over each to see inferred typesconst numbers = [1, 2, 3];const strings = map(numbers, n => n.toString());// T = number, U = string, result = string[]// Use satisfies to validate without changing typeconst config = { host: 'localhost', port: 3000} satisfies Record<string, string | number>;config.host; // Still inferred as 'localhost' literal type// Debugging discriminated unionstype 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 matchtype 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
18.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 optionsmodule.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};
// Result type for error handling without exceptionstype Result<T, E = Error> = | { success: true; value: T } | { success: false; error: E };// Usagefunction 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 casesconst 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 functionsfunction ok<T>(value: T): Result<T, never> { return { success: true, value };}function err<E>(error: E): Result<never, E> { return { success: false, error };}// Async versionasync 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 resultsfunction 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 hierarchyclass 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 handlingfunction 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 typingasync 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 handlerfunction 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);}
18.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.tsexport 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.tsexport type { User } from './types'; // Re-export only typeexport { 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 optimizationimport { User, ApiClient, ValidationError } from './library';// User is only used for typesfunction processUser(user: User) { }// After optimizationimport type { User } from './library'; // Type-onlyimport { ApiClient, ValidationError } from './library'; // Runtime valuesfunction 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.tsimport type { TypeB } from './file-b'; // Safe - type-onlyexport interface TypeA { b: TypeB;}// file-b.tsimport type { TypeA } from './file-a'; // Safe - type-onlyexport 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 buildsource-map-explorer dist/*.js
18.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 variablesdeclare 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 typedconst apiUrl: string = process.env.API_URL; // Type-safeconst port: string = process.env.PORT ?? '3000';// Conditional compilation with dead code eliminationconst 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-shakingfunction debug(message: string) { if (process.env.NODE_ENV === 'development') { console.log('[DEBUG]', message); }}// Even better: Replace at build time// webpack.DefinePlugin or Vite defineconst DEBUG = __DEV__; // Replaced with true/false at build timeif (DEBUG) { console.log('Development mode');}// Type-safe config based on environmenttype 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)
// user.js - JavaScript file with type checking// @ts-check/** * @typedef {Object} User * @property {string} id * @property {string} name * @property {string} email * @property {boolean} isActive * @property {'admin' | 'user' | 'guest'} role *//** * Fetch a user by ID * @param {string} id - User ID * @returns {Promise<User>} User data */async function getUser(id) { const response = await fetch(`/api/users/${id}`); /** @type {User} */ const user = await response.json(); return user;}/** * Format user's display name * @param {User} user - User object * @returns {string} Formatted name */function formatUserName(user) { return `${user.name} (${user.email})`;}/** * @type {User[]} */const users = [];// TypeScript will now check these!const user = await getUser('123');console.log(formatUserName(user));// This will show an error!// @ts-expect-error - role is wrong typeconst badUser = { id: '1', name: 'Bad', email: 'bad@example.com', isActive: true, role: 'invalid' };// Import types from .d.ts or .ts files/** * @param {import('./types').Config} config */function initialize(config) { // config is typed based on imported type}// Generic types in JSDoc/** * @template T * @param {T[]} array * @param {(item: T) => boolean} predicate * @returns {T | undefined} */function find(array, predicate) { return array.find(predicate);}// This is type-safe!const numbers = [1, 2, 3];const result = find(numbers, n => n > 2); // result: number | undefined// Union types/** * @param {string | number} value * @returns {string} */function stringify(value) { return String(value);}// Intersection types (use multiple @typedef)/** * @typedef {Object} HasId * @property {string} id *//** * @typedef {Object} HasName * @property {string} name *//** * @typedef {HasId & HasName} Entity *//** * @param {Entity} entity */function processEntity(entity) { console.log(entity.id, entity.name); // Both properties available}
Example: Enabling checkJs project-wide
// tsconfig.json - Type check all JavaScript{ "compilerOptions": { "allowJs": true, "checkJs": true, // Type check all .js files "noEmit": true, // Don't generate output (just check) // Be lenient initially "strict": false, "noImplicitAny": false }, "include": ["src/**/*"], "exclude": ["node_modules"]}// Opt out specific files if needed// problematic-file.js// @ts-nocheck// This file won't be type checked// Gradually improve coverage// Run type checknpx tsc --noEmit// Fix errors one file at a time// When a file is fully typed, rename to .ts// package.json scripts{ "scripts": { "type-check": "tsc --noEmit", "type-check:watch": "tsc --noEmit --watch" }}
20.3 Legacy Code Integration Strategies
Strategy
Implementation
Use Case
Trade-off
Type Stubs
Create .d.ts files for legacy modules
External JS libraries without types
Manual maintenance required
any Escape Hatch
Use any for unknown legacy types
Quick migration, complex legacy code
Loses type safety
Wrapper Pattern
Wrap legacy code in typed interfaces
Isolate legacy code, provide typed API
Additional abstraction layer
Adapter Pattern
Convert legacy data structures to typed ones
Bridge old and new code
Runtime conversion overhead
Facade Pattern
Type-safe facade over legacy system
Complex legacy subsystems
Duplicate logic possible
Gradual Rewrite
Rewrite modules with proper types
Long-term modernization
High effort, longer timeline
Example: Creating type stubs for legacy code
// Legacy JavaScript library (can't modify)// legacy-lib.jsmodule.exports = { createConnection: function(config) { // Implementation return { query: function(sql, params) { // Query implementation }, close: function() { // Close connection } }; }, utils: { escape: function(str) { return str.replace(/'/g, "''"); } }};// Create type definitions// legacy-lib.d.tsexport interface Connection { query<T = any>(sql: string, params?: any[]): Promise<T[]>; close(): Promise<void>;}export interface Config { host: string; port: number; username: string; password: string; database: string;}export function createConnection(config: Config): Connection;export namespace utils { export function escape(str: string): string;}// Now can use with types!import * as legacyLib from './legacy-lib';const connection = legacyLib.createConnection({ host: 'localhost', port: 5432, username: 'user', password: 'pass', database: 'mydb'});const users = await connection.query<User>('SELECT * FROM users');// Wrapper pattern for better API// db-wrapper.tsimport * as legacyLib from './legacy-lib';export class Database { private connection: legacyLib.Connection; constructor(config: legacyLib.Config) { this.connection = legacyLib.createConnection(config); } async query<T>(sql: string, params: any[] = []): Promise<T[]> { // Add logging, error handling, retry logic console.log('Executing query:', sql); try { return await this.connection.query<T>(sql, params); } catch (error) { console.error('Query failed:', error); throw error; } } async findOne<T>(table: string, id: string): Promise<T | null> { const results = await this.query<T>( `SELECT * FROM ${table} WHERE id = $1`, [id] ); return results[0] ?? null; } async close(): Promise<void> { await this.connection.close(); }}// Usage with type safetyconst db = new Database({ /* config */ });const user = await db.findOne<User>('users', '123'); // Typed!
Example: Adapter pattern for legacy data
// Legacy API returns different structureinterface LegacyUserResponse { user_id: string; user_name: string; user_email: string; is_active: number; // 0 or 1 created_date: string; // Date string user_meta: string; // JSON string}// Modern typed structureinterface User { id: string; name: string; email: string; isActive: boolean; createdAt: Date; metadata: Record<string, unknown>;}// Adapter to convert legacy to modernclass UserAdapter { static fromLegacy(legacy: LegacyUserResponse): User { return { id: legacy.user_id, name: legacy.user_name, email: legacy.user_email, isActive: legacy.is_active === 1, createdAt: new Date(legacy.created_date), metadata: legacy.user_meta ? JSON.parse(legacy.user_meta) : {} }; } static toLegacy(user: User): LegacyUserResponse { return { user_id: user.id, user_name: user.name, user_email: user.email, is_active: user.isActive ? 1 : 0, created_date: user.createdAt.toISOString(), user_meta: JSON.stringify(user.metadata) }; }}// Service layer uses modern typesclass UserService { async getUser(id: string): Promise<User> { // Legacy API call const response = await fetch(`/api/legacy/users/${id}`); const legacyData: LegacyUserResponse = await response.json(); // Convert to modern structure return UserAdapter.fromLegacy(legacyData); } async updateUser(user: User): Promise<User> { // Convert to legacy format const legacyData = UserAdapter.toLegacy(user); const response = await fetch(`/api/legacy/users/${user.id}`, { method: 'PUT', body: JSON.stringify(legacyData) }); const updated: LegacyUserResponse = await response.json(); return UserAdapter.fromLegacy(updated); }}// Application code only sees modern typesconst service = new UserService();const user = await service.getUser('123'); // User typeconsole.log(user.isActive); // boolean, not number!
20.4 Third-party Library Type Definitions
Source
Installation
Quality
Notes
DefinitelyTyped
npm i -D @types/library-name
Community-maintained, varies
Most popular libraries covered
Built-in Types
Included with library - check package.json
Official, usually excellent
Look for "types" field in package.json
Custom .d.ts
Create in project: types/library-name/index.d.ts
Manual effort required
For libraries without types
Declaration Maps
Use library's .d.ts.map for source navigation
Best debugging experience
Jump to actual source code
Type Stubs
Minimal types to suppress errors
Low - just enough to compile
Quick fix, improve later
Module Augmentation
Extend existing type definitions
Good for plugins/extensions
Merge with library types
Example: Working with type definitions
// 1. Install types from DefinitelyTypednpm install --save-dev @types/lodashnpm install --save-dev @types/expressnpm install --save-dev @types/node// 2. Check if library has built-in types// Look in package.json{ "name": "some-library", "types": "./dist/index.d.ts", // Built-in types! "typings": "./dist/index.d.ts" // Alternative field name}// 3. Create custom types for untyped library// types/untyped-lib/index.d.tsdeclare module 'untyped-lib' { export function doSomething(param: string): void; export class SomeClass { constructor(config: any); method(): Promise<any>; }}// 4. Module augmentation to extend library types// types/express-extended.d.tsimport { User } from './models';declare global { namespace Express { interface Request { user?: User; // Add custom property sessionId?: string; } }}// Now can use extended typesimport { Request, Response } from 'express';app.get('/profile', (req: Request, res: Response) => { if (req.user) { // TypeScript knows about req.user! res.json(req.user); }});// 5. Ambient module declarations for non-JS assets// types/assets.d.tsdeclare module '*.png' { const value: string; export default value;}declare module '*.svg' { const content: React.FunctionComponent<React.SVGProps<SVGSVGElement>>; export default content;}declare module '*.css' { const classes: { [key: string]: string }; export default classes;}// Now can import assets with typesimport logo from './logo.png'; // stringimport Icon from './icon.svg'; // React componentimport styles from './styles.css'; // object// 6. Configure TypeScript to find custom types// tsconfig.json{ "compilerOptions": { "typeRoots": [ "./node_modules/@types", "./types" // Custom types directory ], "types": [ "node", "jest", "express" ] }}// 7. Handling libraries with wrong/outdated types// Use @ts-expect-error with commentimport { someFunction } from 'badly-typed-lib';// @ts-expect-error - Library types are incorrect, see issue #123const result = someFunction('param');
20.5 Team Onboarding and Training Approaches
Activity
Duration
Focus
Outcome
Fundamentals Workshop
Half day - 1 day
Basic types, interfaces, generics
Team can write typed code
Code Review Sessions
Ongoing - 30min weekly
Review TypeScript PRs, discuss patterns
Consistent code quality
Pair Programming
Ongoing - 2-4hrs weekly
Senior with junior, solve real problems
Hands-on learning
Style Guide Creation
1 week
Document team conventions
Consistency across codebase
Migration Pairing
Ongoing - during migration
Convert files together, teach patterns
Faster migration, knowledge sharing
Brown Bag Sessions
Monthly - 30-45min
Advanced topics, new features, tips
Continuous improvement
Example: Onboarding checklist
// TypeScript Onboarding Checklist// Week 1: Setup & Basics☐ Install VS Code with TypeScript extensions - ESLint, Prettier, Error Lens☐ Clone team repository with TypeScript config☐ Run `npm install` and `npm run type-check`☐ Complete basic TypeScript tutorial - Types, interfaces, functions☐ Read team style guide☐ Set up dev environment (tsconfig, linting)// Week 2: Reading & Understanding☐ Review 5 existing TypeScript files in codebase☐ Understand common patterns used by team☐ Shadow senior developer during code review☐ Ask questions about unfamiliar patterns☐ Read documentation on utility types// Week 3: Small Contributions☐ Convert 1-2 simple JavaScript files to TypeScript☐ Submit PR with proper types☐ Respond to code review feedback☐ Fix type errors in existing code☐ Add JSDoc types to JavaScript files// Week 4: Independent Work☐ Implement new feature with TypeScript☐ Write tests with proper types☐ Review others' TypeScript PRs☐ Refactor code to use better types☐ Share learnings with team// Ongoing Learning☐ Attend weekly TypeScript office hours☐ Contribute to team style guide☐ Learn advanced patterns (generics, utility types)☐ Stay updated with TypeScript releases☐ Help onboard new team members
Example: Team training resources
// Team TypeScript Learning Path// 1. Official Documentation// https://www.typescriptlang.org/docs/handbook/intro.html// Comprehensive, authoritative source// 2. Team Wiki / Confluence// Document team-specific patterns and conventions// Examples:// - How we handle API types// - Common utility types we use// - Error handling patterns// - Testing conventions// 3. Internal Code Examples// Create examples repository with common patterns// /examples/basic-types.tsinterface User { id: string; email: string; name: string;}// /examples/generics.tsfunction createResponse<T>(data: T): ApiResponse<T> { return { success: true, data };}// /examples/utility-types.tstype UpdateUserInput = Partial<Omit<User, 'id'>>;// 4. Code Review Guidelines// - Always provide types for function parameters and returns// - Prefer interfaces over type aliases for object shapes// - Use unknown over any when type is truly unknown// - Document complex types with JSDoc// - Use utility types instead of manual type manipulation// 5. Weekly Office Hours// Every Friday 2-3pm: TypeScript Q&A// - Bring questions about types you're stuck on// - Discuss challenging patterns// - Review recent TypeScript changes to codebase// 6. Slack Channel: #typescript-help// - Ask questions// - Share tips and articles// - Discuss TypeScript updates// 7. Recommended Courses// - TypeScript Deep Dive (free book)// - Execute Program: TypeScript// - Frontend Masters: TypeScript courses// - Total TypeScript by Matt Pocock// 8. Practice Exercises// Type Challenges: https://github.com/type-challenges/type-challenges// Start with easy, progress to hard// 9. Lunch & Learn Sessions// Monthly presentations:// - "Advanced Generic Patterns"// - "Type-safe API Clients"// - "Migrating Legacy Code"// - "TypeScript 5.0 New Features"// 10. Mentorship Program// Pair junior developers with TypeScript experts// Regular 1-on-1 sessions to review code and answer questions
20.6 Code Review Guidelines for TypeScript
Category
Check For
Good Practice
Anti-pattern
Type Safety
No any, proper types for params/returns
Explicit types, unknown over any
Excessive any usage, missing types
Type Inference
Let TypeScript infer when obvious
Infer simple types, annotate complex ones
Over-annotation, redundant types
Utility Types
Use built-in utilities appropriately
Partial<T>, Pick<T, K>, Omit<T, K>
Manual type construction when utility exists
Interfaces vs Types
Consistent usage per team convention
Interface for objects, type for unions/aliases
Inconsistent mixing without reason
Null Safety
Handle null/undefined properly
Optional chaining, nullish coalescing
Non-null assertions (!), unchecked access
Type Assertions
Minimize use of as keyword
Type guards, validation before assertion
Unsafe as casts, as any
Documentation
JSDoc for public APIs
Describe params, returns, examples
Missing docs for complex types
Example: Code review checklist
// TypeScript Code Review Checklist// ✅ Type Safety// ✓ Good - Explicit typesfunction getUser(id: string): Promise<User | null> { // ...}// ✗ Bad - Using anyfunction getUser(id: any): Promise<any> { // ...}// ✅ Type Inference// ✓ Good - Let TypeScript inferconst count = users.length; // number inferredconst names = users.map(u => u.name); // string[] inferred// ✗ Bad - Over-annotationconst count: number = users.length;const names: string[] = users.map((u: User): string => u.name);// ✅ Utility Types// ✓ Good - Use built-in utilitiestype UpdateUserInput = Partial<Omit<User, 'id' | 'createdAt'>>;// ✗ Bad - Manual type constructiontype UpdateUserInput = { name?: string; email?: string; age?: number; // ... manually list all optional fields};// ✅ Null Safety// ✓ Good - Safe accessconst email = user?.profile?.email ?? 'no-email@example.com';// ✗ Bad - Non-null assertionconst email = user!.profile!.email; // Can throw at runtime// ✅ Type Guards vs Assertions// ✓ Good - Type guardfunction isUser(value: unknown): value is User { return ( typeof value === 'object' && value !== null && 'id' in value && 'email' in value );}if (isUser(data)) { console.log(data.email); // Safe}// ✗ Bad - Unsafe assertionconst user = data as User; // No runtime check!console.log(user.email);// ✅ Discriminated Unions// ✓ Good - Proper discriminated uniontype Result<T> = | { success: true; data: T } | { success: false; error: string };function handle<T>(result: Result<T>) { if (result.success) { console.log(result.data); // TypeScript knows data exists } else { console.error(result.error); // TypeScript knows error exists }}// ✗ Bad - Unclear uniontype Result<T> = { data?: T; error?: string;};// ✅ Function Overloads// ✓ Good - Clear overloadsfunction format(value: string): string;function format(value: number): string;function format(value: Date): string;function format(value: string | number | Date): string { // Implementation return String(value);}// ✗ Bad - Single signature with unionfunction format(value: string | number | Date): string { return String(value);}// ✅ Const Assertions// ✓ Good - Narrow to literal typesconst config = { apiUrl: 'https://api.example.com', timeout: 5000} as const;// type: { readonly apiUrl: "https://api.example.com"; readonly timeout: 5000 }// ✗ Bad - Wider typesconst config = { apiUrl: 'https://api.example.com', // type: string timeout: 5000 // type: number};// ✅ Documentation// ✓ Good - JSDoc for complex functions/** * Fetch paginated users with optional filtering * @param page - Page number (1-indexed) * @param pageSize - Number of items per page * @param filters - Optional filters for user properties * @returns Promise resolving to paginated user data * @throws {ApiError} If the API request fails * @example * const result = await getUsers(1, 10, { role: 'admin' }); */async function getUsers( page: number, pageSize: number, filters?: UserFilters): Promise<PaginatedResponse<User>> { // ...}// ✗ Bad - No documentationasync function getUsers(page: number, pageSize: number, filters?: UserFilters) { // ...}
Example: PR review feedback examples
// Good Review Feedback// 1. Constructive type improvement// ❌ Current:function processData(data: any) { }// 💬 Feedback:// "Can we add a proper type for data? If the structure is unknown,// consider using 'unknown' and adding a type guard."// ✅ Suggested:interface DataInput { id: string; values: number[];}function processData(data: DataInput) { }// 2. Encourage utility types// ❌ Current:type UpdateUser = { name?: string; email?: string; age?: number;};// 💬 Feedback:// "We can use Partial<T> and Omit<T, K> here to derive from User type,// which will automatically stay in sync if User changes."// ✅ Suggested:type UpdateUser = Partial<Omit<User, 'id' | 'createdAt'>>;// 3. Safer null handling// ❌ Current:const userName = user.profile.name;// 💬 Feedback:// "Profile might be null/undefined. Consider using optional chaining// and providing a fallback."// ✅ Suggested:const userName = user.profile?.name ?? 'Anonymous';// 4. Better error types// ❌ Current:try { await fetchData();} catch (error) { console.error(error.message); // error is 'any'}// 💬 Feedback:// "Can we add proper typing for the error? Consider creating a type guard."// ✅ Suggested:try { await fetchData();} catch (error) { if (error instanceof Error) { console.error(error.message); } else { console.error('Unknown error:', error); }}// 5. Consistent naming// ❌ Current:interface IUser { } // Hungarian notationtype userRole = 'admin' | 'user'; // lowercase// 💬 Feedback:// "Let's follow our style guide: PascalCase for types/interfaces,// no 'I' prefix."// ✅ Suggested:interface User { }type UserRole = 'admin' | 'user';// Approval comments// ✅ "Great use of discriminated unions here! Very type-safe."// ✅ "Nice refactoring to use Pick<T, K>. Much cleaner."// ✅ "Love the type guards - this makes the code much safer."// ✅ "Excellent JSDoc documentation!"