Back to Blog
Advanced TypeScript Patterns Every Developer Should Master

Advanced TypeScript Patterns Every Developer Should Master

Ansh Gupta
7 min read
TypeScriptJavaScriptProgrammingWeb DevelopmentType Safety

Advanced TypeScript Patterns Every Developer Should Master

TypeScript's type system is incredibly powerful, offering features that go far beyond basic type annotations. Let's explore advanced patterns that will make your code more robust and maintainable.

1. Advanced Generics Patterns

Generic Constraints

Use constraints to limit what types can be used with your generics:

interface HasLength {
  length: number;
}

function logLength<T extends HasLength>(arg: T): T {
  console.log(arg.length);
  return arg;
}

logLength("Hello"); // ✅ Works
logLength([1, 2, 3]); // ✅ Works
logLength({ length: 5 }); // ✅ Works
// logLength(123); // ❌ Error: number doesn't have length

Multiple Type Parameters

function merge<T, U>(first: T, second: U): T & U {
  return { ...first, ...second };
}

const result = merge(
  { name: "John" },
  { age: 30, email: "john@example.com" }
);
// result type: { name: string } & { age: number; email: string }

Generic Utility Functions

function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
  const result = {} as Pick<T, K>;
  keys.forEach(key => {
    result[key] = obj[key];
  });
  return result;
}

const user = { id: 1, name: "John", email: "john@example.com", age: 30 };
const userSummary = pick(user, ["name", "email"]);
// Type: { name: string; email: string }

2. Conditional Types

Basic Conditional Types

type IsArray<T> = T extends any[] ? true : false;

type Test1 = IsArray<string[]>; // true
type Test2 = IsArray<number>;   // false

Advanced Conditional Types

type Flatten<T> = T extends (infer U)[] ? U : T;

type StringArray = Flatten<string[]>; // string
type NumberType = Flatten<number>;    // number

// Extract function return type
type ReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

function getData(): Promise<string> {
  return Promise.resolve("data");
}

type DataType = ReturnType<typeof getData>; // Promise<string>

3. Mapped Types

Basic Mapped Types

type Partial<T> = {
  [P in keyof T]?: T[P];
};

type Required<T> = {
  [P in keyof T]-?: T[P];
};

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

Advanced Mapped Types

// Create a type where all properties are functions
type Functionize<T> = {
  [K in keyof T]: () => T[K];
};

interface User {
  name: string;
  age: number;
}

type UserFunctions = Functionize<User>;
// {
//   name: () => string;
//   age: () => number;
// }

// Create getters and setters
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

type Setters<T> = {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type UserGettersSetters = Getters<User> & Setters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
//   setName: (value: string) => void;
//   setAge: (value: number) => void;
// }

4. Template Literal Types

Basic Template Literals

type World = "world";
type Greeting = `hello ${World}`; // "hello world"

// Create event names
type EventName<T extends string> = `on${Capitalize<T>}`;
type ClickEvent = EventName<"click">; // "onClick"
type HoverEvent = EventName<"hover">; // "onHover"

Advanced Template Literal Patterns

// CSS-in-JS type-safe styling
type CSSValue = string | number;
type CSSProperties = {
  color?: CSSValue;
  backgroundColor?: CSSValue;
  fontSize?: CSSValue;
  margin?: CSSValue;
  padding?: CSSValue;
};

type CSSPropertyKeys = keyof CSSProperties;
type CSSCustomProperties<T extends string> = {
  [K in T as `--${K}`]: CSSValue;
};

type ThemeVariables = CSSCustomProperties<"primary" | "secondary" | "accent">;
// {
//   "--primary": CSSValue;
//   "--secondary": CSSValue;
//   "--accent": CSSValue;
// }

5. Utility Types and Patterns

Deep Partial

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface Config {
  api: {
    baseUrl: string;
    timeout: number;
  };
  features: {
    analytics: boolean;
    notifications: boolean;
  };
}

type PartialConfig = DeepPartial<Config>;
// All properties and nested properties become optional

Type-Safe Event Emitter

interface EventMap {
  'user:login': { userId: string; timestamp: Date };
  'user:logout': { userId: string };
  'data:received': { data: any[]; count: number };
}

class TypedEventEmitter<T extends Record<string, any>> {
  private listeners: {
    [K in keyof T]?: ((data: T[K]) => void)[];
  } = {};

  on<K extends keyof T>(event: K, listener: (data: T[K]) => void): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }

  emit<K extends keyof T>(event: K, data: T[K]): void {
    const eventListeners = this.listeners[event];
    if (eventListeners) {
      eventListeners.forEach(listener => listener(data));
    }
  }
}

const emitter = new TypedEventEmitter<EventMap>();

// ✅ Type-safe event handling
emitter.on('user:login', ({ userId, timestamp }) => {
  console.log(`User ${userId} logged in at ${timestamp}`);
});

// ❌ TypeScript error: Property 'invalidProp' doesn't exist
// emitter.on('user:login', ({ invalidProp }) => {});

6. Brand Types for Type Safety

// Create branded types for better type safety
type UserId = string & { readonly __brand: unique symbol };
type Email = string & { readonly __brand: unique symbol };

function createUserId(id: string): UserId {
  return id as UserId;
}

function createEmail(email: string): Email {
  if (!email.includes('@')) {
    throw new Error('Invalid email');
  }
  return email as Email;
}

function sendWelcomeEmail(userId: UserId, email: Email): void {
  // Implementation
}

const userId = createUserId("123");
const email = createEmail("user@example.com");

sendWelcomeEmail(userId, email); // ✅ Works
// sendWelcomeEmail(email, userId); // ❌ TypeScript error

7. Advanced Function Overloads

interface CreateElementOptions {
  className?: string;
  id?: string;
}

// Function overloads for different return types
function createElement(tag: 'div'): HTMLDivElement;
function createElement(tag: 'span'): HTMLSpanElement;
function createElement(tag: 'input'): HTMLInputElement;
function createElement(tag: string): HTMLElement;
function createElement(tag: string, options?: CreateElementOptions): HTMLElement {
  const element = document.createElement(tag);
  if (options?.className) {
    element.className = options.className;
  }
  if (options?.id) {
    element.id = options.id;
  }
  return element;
}

const div = createElement('div');    // Type: HTMLDivElement
const span = createElement('span');  // Type: HTMLSpanElement
const input = createElement('input'); // Type: HTMLInputElement

8. Recursive Types

// File system structure
interface FileSystemNode {
  name: string;
  type: 'file' | 'directory';
  children?: FileSystemNode[];
}

// JSON-like recursive type
type JSONValue = 
  | string
  | number
  | boolean
  | null
  | JSONValue[]
  | { [key: string]: JSONValue };

// Type-safe path navigation
type PathToProperty<T, Path extends string> = 
  Path extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? PathToProperty<T[Key], Rest>
      : never
    : Path extends keyof T
      ? T[Path]
      : never;

interface NestedObject {
  user: {
    profile: {
      name: string;
      age: number;
    };
  };
}

type UserName = PathToProperty<NestedObject, 'user.profile.name'>; // string

Best Practices

1. Use Type Guards

function isString(value: unknown): value is string {
  return typeof value === 'string';
}

function processValue(value: unknown) {
  if (isString(value)) {
    // TypeScript knows value is string here
    console.log(value.toUpperCase());
  }
}

2. Leverage const assertions

const fruits = ['apple', 'banana', 'orange'] as const;
type Fruit = typeof fruits[number]; // 'apple' | 'banana' | 'orange'

const theme = {
  colors: {
    primary: '#007bff',
    secondary: '#6c757d'
  }
} as const;

type ThemeColor = typeof theme.colors[keyof typeof theme.colors];
// '#007bff' | '#6c757d'

Conclusion

These advanced TypeScript patterns provide:

  • Better type safety through precise type definitions
  • Enhanced developer experience with better autocomplete
  • Reduced runtime errors through compile-time checks
  • More maintainable code with clear type contracts

Master these patterns to write more robust and maintainable TypeScript applications. The investment in learning advanced types pays dividends in code quality and developer productivity.

Remember: The goal isn't to use these patterns everywhere, but to know when they provide value and how to implement them effectively.

AG

About Ansh Gupta

Frontend Developer with 3 years of experience building modern web applications. Based in Indore, India, passionate about React, TypeScript, and creating exceptional user experiences.

Learn more about me