Advanced TypeScript Patterns: Type-Safe Code That Scales

Published: (January 6, 2026 at 01:34 PM EST)
9 min read
Source: Dev.to

Source: Dev.to

Cover image for Advanced TypeScript Patterns: Type‑Safe Code That Scales

Sepehr Mohseni

Generic Constraints and Inference

Basic Generics with Constraints

// Constrain a generic to objects that have an `id` property
interface HasId {
  id: string | number;
}

/**
 * Find an element by its `id`.
 * @param items - Array of items that extend `HasId`
 * @param id    - The id to look for (must be the same type as the item's id)
 */
function findById<T extends HasId>(items: T[], id: T['id']): T | undefined {
  return items.find(item => item.id === id);
}

// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
  id: number;
  name: string;
  email: string;
}

const users: User[] = [
  { id: 1, name: 'John', email: 'john@example.com' },
  { id: 2, name: 'Jane', email: 'jane@example.com' },
];

const user = findById(users, 1); // Type: User | undefined

Generic Factory Functions

/**
 * Simple store implementation with type‑safe state handling.
 * @param initialState - The initial state of the store; its type is inferred.
 */
function createStore<T>(initialState: T) {
  let state = initialState;
  const listeners = new Set<(s: T) => void>();

  return {
    /** Returns the current state */
    getState: () => state,

    /**
     * Merges a partial update into the current state.
     * @param newState - Partial object containing the fields to update.
     */
    setState: (newState: Partial<T>) => {
      state = { ...state, ...newState };
      listeners.forEach(listener => listener(state));
    },

    /**
     * Subscribes a listener that will be called on every state change.
     * @param listener - Function receiving the new state.
     * @returns A function to unsubscribe the listener.
     */
    subscribe: (listener: (state: T) => void) => {
      listeners.add(listener);
      return () => listeners.delete(listener);
    },
  };
}

// ---------------------------------------------------
// Example usage
// ---------------------------------------------------
interface User {
  id: number;
  name: string;
  email: string;
}

const userStore = createStore({
  user: null as User | null,
  isLoading: false,
  error: null as string | null,
});

userStore.setState({ isLoading: true }); // ✅ Type‑safe

Tip: TypeScript can infer complex types from usage. Let the compiler do the heavy lifting for internal implementations, but always add explicit generic constraints (e.g., T extends HasId) for public APIs to keep them clear and safe.

Conditional Types

Basic Conditional Types

// Generic conditional type that maps a key to a response type
type ApiResponse<T> = T extends 'user'
  ? User
  : T extends 'product'
    ? Product
    : never;

// Example payload types
interface User {
  id: number;
  name: string;
}
interface Product {
  id: number;
  price: number;
}

// Usage
type UserResponse    = ApiResponse<'user'>;    // → User
type ProductResponse = ApiResponse<'product'>; // → Product

Practical example – unwrapping Promise types

// Simplified version of the built‑in `Awaited` type
type Unwrap<T> = T extends Promise<infer R> ? Unwrap<R> : T;

// Example
type Result = Unwrap<Promise<Promise<string>>>; // → string

Distributive Conditional Types

Conditional types automatically distribute over union types. This makes them handy for filtering or transforming each member of a union individually.

Removing null / undefined

// Remove `null` and `undefined` from a type
type NonNullable<T> = T extends null | undefined ? never : T;

// Example
type Example = NonNullable<string | null | undefined>; // → string

Extracting specific members from a union

// Keep only the `string` members of a union
type ExtractStrings<T> = T extends string ? T : never;

type Mixed       = string | number | boolean | 'hello' | 'world';
type OnlyStrings = ExtractStrings<Mixed>; // → string | 'hello' | 'world'

Note: Because the conditional type ExtractStrings<T> is applied to each member of Mixed separately, the result is a union of the members that satisfy the extends string check. This distributive behavior is what enables powerful type‑level filtering in TypeScript.

Infer Keyword for Type Extraction

The infer keyword lets you capture a type from within a conditional type and reuse it elsewhere. Below are the most common patterns for extracting information from types.

// ------------------------------------------------------------
// 1️⃣ Extract a function’s parameter types
// ------------------------------------------------------------
type Parameters<T> = T extends (...args: infer P) => any ? P : never;

// ------------------------------------------------------------
// 2️⃣ Extract a function’s return type
// ------------------------------------------------------------
type Return<T> = T extends (...args: any[]) => infer R ? R : never;

// ------------------------------------------------------------
// 3️⃣ Extract the element type of an array (or tuple)
// ------------------------------------------------------------
type ArrayElement<T> = T extends (infer E)[] ? E : never;

// ------------------------------------------------------------
// 4️⃣ Extract the props type of a React component
// ------------------------------------------------------------
type ComponentProps<T> = T extends React.ComponentType<infer P> ? P : never;

📚 Practical Example

// A simple async function
async function fetchUser(id: number): Promise<User> {
  /* … */
}

// Use the helpers defined above
type FetchUserParams = Parameters<typeof fetchUser>; // => [number]
type FetchUserReturn = Return<typeof fetchUser>;     // => Promise<User>

What the helpers do

HelperInputExtracted type
Parameters<T>(...args: infer P) => anyP – a tuple of the function’s parameters
Return<T>(...args: any[]) => infer RR – the function’s return type
ArrayElement<T>(infer E)[]E – the type of each array element
ComponentProps<T>React.ComponentType<infer P>P – the component’s props type

These utilities are the building blocks for many advanced type‑level patterns in TypeScript, such as creating higher‑order functions, typed wrappers, or generic component libraries.

Mapped Types

Transform Object Types

// Make all properties optional
type Partial<T> = {
  [P in keyof T]?: T[P];
};

// Make all properties required
type Required<T> = {
  [P in keyof T]-?: T[P];
};

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

// Pick specific properties
type Pick<T, K extends keyof T> = {
  [P in K]: T[P];
};

Advanced Mapped Types

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

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

/* Example */
type PersonGetters = Getters<Person>;
/* Result:
{
  getName: () => string;
  getAge: () => number;
}
*/

// Filter properties by their value type
type FilterByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};

interface Mixed {
  id: number;
  name: string;
  active: boolean;
  count: number;
}

/* Example */
type NumberProps = FilterByType<Mixed, number>;
/* Result:
{
  id: number;
  count: number;
}
*/

Note: By combining template‑literal types with mapped types you can build powerful type transformations—perfect for generating strongly‑typed API clients, form builders, and other meta‑programming scenarios.

Type Guards and Narrowing

Custom Type Guards

// Type predicate function
function isUser(value: unknown): value is User {
  return (
    typeof value === "object" &&
    value !== null &&
    "id" in value &&
    "email" in value &&
    typeof (value as User).id === "number" &&
    typeof (value as User).email === "string"
  );
}

// Usage
function processData(data: unknown) {
  if (isUser(data)) {
    // TypeScript knows `data` is `User` here
    console.log(`User ${data.name} (${data.id})`);
  } else {
    console.warn("Not a user");
  }
}

Discriminated Unions with Type Guards

interface SuccessResponse {
  status: 'success';
  data: User;
}

interface ErrorResponse {
  status: 'error';
  message: string;
}

type ApiResponse = SuccessResponse | ErrorResponse;

function handleResponse(response: ApiResponse) {
  if (response.status === 'success') {
    // TypeScript narrows to SuccessResponse
    console.log(response.data.name);
  } else {
    // TypeScript narrows to ErrorResponse
    console.log(response.message);
  }
}

Assertion Functions

/**
 * Asserts that the supplied value is a `User`.
 * Throws an error if the check fails, allowing TypeScript to narrow the type.
 */
function assertIsUser(value: unknown): asserts value is User {
  if (!isUser(value)) {
    throw new Error('Value is not a User');
  }
}

/** Example usage */
function processUserData(data: unknown) {
  // Narrow `data` to `User` after the assertion
  assertIsUser(data);
  console.log(data.email); // `data` is now known to be a `User`
}

/**
 * Asserts that a value is neither `null` nor `undefined`.
 *
 * @param value   The value to check.
 * @param message Optional custom error message.
 *
 * @throws Error if `value` is `null` or `undefined`.
 */
function assertDefined<T>(
  value: T | null | undefined,
  message?: string
): asserts value is T {
  if (value === null || value === undefined) {
    throw new Error(message ?? 'Value is not defined');
  }
}

Template Literal Types

No code example provided in the original segment.

Building Type‑Safe APIs

// ------------------------------------------------------------
// 1️⃣ Event handler types
// ------------------------------------------------------------

// Allowed event names
type EventName = 'click' | 'focus' | 'blur';

// Utility type that builds the handler name (e.g. "onClick")
type EventHandler<E extends EventName> = `on${Capitalize<E>}`;

// Example usage
type ClickHandler = EventHandler<'click'>; // "onClick"
type FocusHandler = EventHandler<'focus'>; // "onFocus"
type BlurHandler  = EventHandler<'blur'>;  // "onBlur"
// ------------------------------------------------------------
// 2️⃣ Route parameters extraction
// ------------------------------------------------------------

// Route patterns
type Route =
  | '/users/:id'
  | '/posts/:postId/comments/:commentId';

/**
 * Recursively extracts all `:param` placeholders from a route string.
 *
 * @template T - The route string to analyse.
 * @returns A union of the extracted parameter names.
 */
type ExtractParams<T extends string> =
  // Match a segment that contains a parameter followed by more segments
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ExtractParams<Rest>
    // Match the final segment that contains a parameter
    : T extends `${infer _Start}:${infer Param}`
      ? Param
      // No more parameters
      : never;

// Example extractions
type UserRouteParams    = ExtractParams<'/users/:id'>; // "id"
type CommentRouteParams = ExtractParams<'/posts/:postId/comments/:commentId'>;
//   → "postId" | "commentId"

The snippet above demonstrates how to:

  1. Generate type‑safe event‑handler names using template literal types and Capitalize.
  2. Extract route parameters from a URL pattern, yielding a union of the placeholder names.

Type‑Safe CSS Properties

type CSSUnit = 'px' | 'em' | 'rem' | '%' | 'vh' | 'vw';
type CSSValue = `${number}${CSSUnit}`;

interface Spacing {
  margin: CSSValue;
  padding: CSSValue;
}

const spacing: Spacing = {
  margin: '16px',    // Valid
  padding: '1.5rem', // Valid
  // margin: '16',   // Error: not a valid CSSValue
};

Utility Types in Practice

(Section retained; no specific code was provided.)

Building a Form Library

Below is a small, type‑safe form‑configuration helper. It infers the appropriate input type from the shape of a data model and lets you add labels, validation, and required flags.

// ------------------------------------------------------------
// 1️⃣  Generic field‑configuration type
// ------------------------------------------------------------
type FieldConfig<T> = {
  [K in keyof T]: {
    /** Input type inferred from the property type */
    type:
      T[K] extends string
        ? 'text' | 'email' | 'password'
        : T[K] extends number
        ? 'number'
        : T[K] extends boolean
        ? 'checkbox'
        : 'text';

    /** Human‑readable label */
    label: string;

    /** Optional “required” flag */
    required?: boolean;

    /** Optional validator – returns an error string or `undefined` */
    validate?: (value: T[K]) => string | undefined;
  };
};

// ------------------------------------------------------------
// 2️⃣  Example data model
// ------------------------------------------------------------
interface UserForm {
  name: string;
  email: string;
  age: number;
  newsletter: boolean;
}

// ------------------------------------------------------------
// 3️⃣  Concrete configuration for `UserForm`
// ------------------------------------------------------------
const userFormConfig: FieldConfig<UserForm> = {
  name: {
    type: 'text',
    label: 'Full Name',
    required: true,
    validate: value => 
  }
}
Back to Blog

Related posts

Read more »