DEV Community

Alex Chen
Alex Chen

Posted on

TypeScript Deep Dive: Advanced Types and Patterns (2026)

TypeScript Deep Dive: Advanced Types and Patterns (2026)

You know the basics of TypeScript. Now let's unlock the real power — types that catch bugs at compile time you didn't even know existed.

Utility Types in Practice

// You use these every day. Here's how they actually work:

// Partial<T> — Make all properties optional
interface User {
  id: string;
  name: string;
  email: string;
  role: 'admin' | 'user';
}
type UserUpdate = Partial<User>;
// { id?: string; name?: string; email?: string; role?: 'admin'|'user' }

// Real-world: Update function that accepts partial data
function updateUser(id: string, updates: Partial<User>): User {
  const existing = getUserFromDb(id);
  return { ...existing, ...updates }; // Type-safe merge
}

// Required<T> — Opposite of Partial
type RequiredUser = Required<Partial<User>>;
// All properties become required again

// Omit<T, K> — Remove specific keys
type CreateUserInput = Omit<User, 'id'>;
// { name: string; email: string; role: 'admin'|'user' } — no id needed

// Pick<T, K> — Keep only specific keys
type PublicUser = Pick<User, 'name' | 'role'>;
// { name: string; role: 'admin'|'user' }

// Record<K, V> — Dictionary type
const rolePermissions: Record<string, string[]> = {
  admin: ['read', 'write', 'delete'],
  user: ['read'],
};
// rolePermissions['admin'] → string[]
// rolePermissions['unknown'] → string[] (no error!)

// Better: Use as const + specific key type
type Role = 'admin' | 'user' | 'moderator';
const permissions: Record<Role, string[]> = {
  admin: ['*'],
  user: ['read'],
  moderator: ['read', 'write'],
};

// Extract<T, U> — Extract types matching a condition
type StringKeys<T> = Extract<keyof T, string>; // Only string keys

// Exclude<T, U> — Opposite of Extract
type NonFunctionKeys<T> = Exclude<keyof T, Function>;

// ReturnType<T> — Get return type of a function
function fetchData(url: string): Promise<{ data: unknown; status: number }> {
  return fetch(url).then(r => r.json());
}
type FetchResult = ReturnType<typeof fetchData>;
// Promise<{ data: unknown; status: number }>

// Parameters<T> — Get parameter types as tuple
type FetchParams = Parameters<typeof fetchData>;
// [url: string]

// Awaited<T> — Unwrap Promise (TypeScript 4.5+)
type ResolvedData = Awaited<FetchResult>;
// { data: unknown; status: number }
Enter fullscreen mode Exit fullscreen mode

Conditional Types

// The most powerful TypeScript feature you might not be using enough

// Basic conditional: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;
type A = IsString<'hello'>; // true
type B = IsString<number>;   // false

// Practical: Extract non-nullable fields
type NonNullableFields<T> = {
  [K in keyof T]: null extends T[K] ? never : K
}[keyof T];

interface UserData {
  name: string;
  email: string | null;
  phone?: string;
  age: number | undefined;
}

type RequiredFields = NonNullableFields<UserData>;
// "name" (only name is guaranteed non-nullable)

// Practical: Deep Partial (recursive)
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

type PartialConfig = DeepPartial<{
  database: {
    host: string;
    port: number;
    ssl: { enabled: boolean; cert: string };
  };
}>;
// All nested properties are now optional!

// Practical: Flatten nested object paths
type Paths<T, P extends string = ''> = T extends object
  ? { [K in keyof T & string]: Paths<T[K], `${P}${P ? '.' : ''}${K}`> }[keyof T & string]
  : P;

type ConfigPaths = Paths<{ db: { host: string; port: number }; api: { url: string } }>;
// "db.host" | "db.port" | "api.url"

// infer keyword — extract parts of a type
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type Unwrapped = UnwrapPromise<Promise<string>>; // string

type ArrayElement<T> = T extends (infer E)[] ? E : never;
type Elem = ArrayElement<[string, number, boolean]>; // string | number | boolean

// First parameter type of a function
type FirstParam<T> = T extends (...args: [infer F, ...any[]]) => any ? F : never;
Enter fullscreen mode Exit fullscreen mode

Template Literal Types

// Build types from strings — incredibly powerful for APIs

// CSS property names
type CSSProperty = `--${string}`; // Any CSS custom property

// Event naming convention
type EventName = `on${Capitalize<string>}`;
type ClickEvent = EventName<'click'>; // "onClick"

// HTTP methods
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE';

// API endpoint builder
type ApiPath = `/api/${string}`;
type Endpoint = `${HttpMethod} ${ApiPath}`;
// "GET /api/users" | "POST /api/users" | ...

// Path-based routing (like Express)
type Route = 
  | `GET /api/users`
  | `GET /api/users/:id`
  | `POST /api/users`
  | `PUT /api/users/:id`
  | `DELETE /api/users/:id`;

// Extract path params from route
type PathParams<T extends string> = 
  T includes `:${infer Param}` 
    ? Param | PathParams<T.replace(`:${Param}`, '')>
    : never;

type UsersIdParams = PathParams<'/api/users/:id'>; // "id"

// Object keys from template literals
type EventHandler = {
  [K in `on${Capitalize<string>`}]?: (...args: any[]) => void;
};

// Uppercase/Lowercase/Capitalize/Uncapitalize
type SnackCase = Lowercase<'HelloWorld'>; // "helloworld"
type CamelCase = Capitalize<'hello world'>; // "Hello world"

// String manipulation for validation
type ValidColor = `#${string${number}}`; // Hex color pattern
type CssSize = `${number}px` | `${number}%` | `auto` | `fit-content`;
Enter fullscreen mode Exit fullscreen mode

Mapped Types with Key Remapping

// TypeScript 4.1+ allows remapping keys in mapped types

// Getter/setter pairs
type Accessors<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
} & {
  [K in keyof T as `set${Capitalize<string & K>}`]: (value: T[K]) => void
};

type UserAccessors = Accessors<{ name: string; age: number }>;
// {
//   getName: () => string;
//   setName: (value: string) => void;
//   getAge: () => number;
//   setAge: (value: number) => void;
// }

// Filter by key pattern
type OnlyStringKeys<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K]
};

// Create readonly version
type Immutable<T> = {
  readonly [K in keyof T]: T[K] extends object ? Immutable<T[K]> : T[K]
};

// Optionalize nullable fields
type OptionalNullable<T> = {
  [K in keyof T]: null extends T[K] ? T[K] | undefined : T[K]
};

// Branded types (nominal typing hack)
type Brand<T, B extends string> = T & { __brand: B };
type UserId = Brand<string, 'UserId'>;
type OrderId = Brand<string, 'OrderId'>;

function createUserId(id: string): UserId {
  return id as UserId; // Only factory can create!
}

function getUser(id: UserId) { /* ... */ }
function getOrder(id: OrderId) { /* ... */ */

const uid = createUserId('abc123');
getUser(uid);     // ✅ Works
getOrder(uid);    // ❌ Error! UserId is not OrderId
// Even though both are strings at runtime, TypeScript treats them as different types
Enter fullscreen mode Exit fullscreen mode

Discriminated Unions for State Machines

// This is THE pattern for handling complex state

type RequestState<T> =
  | { status: 'idle' }
  | { status: 'loading' }
  | { status: 'success'; data: T; timestamp: number }
  | { status: 'error'; error: Error; retryCount: number };

// Every state is explicit — no undefined is possible
function handleState<T>(state: RequestState<T>) {
  switch (state.status) {
    case 'idle':
      console.log('Ready to fetch');
      break;
    case 'loading':
      console.log('Loading...');
      break;
    case 'success':
      // TypeScript KNOWS data exists here
      console.log(`Got data at ${state.timestamp}:`, state.data);
      break;
    case 'error':
      // TypeScript KNOWS error and retryCount exist here
      console.log(`Error (${state.retryCount} retries):`, state.error.message);
      if (state.retryCount < 3) {
        retry(); // Safe to call
      }
      break;
    default:
      // Exhaustive check! If you add a new state, TS errors here.
      const _exhaustive: never = state;
      throw new Error(`Unknown state: ${_exhaustive}`);
  }
}

// Recursive discriminated union (for nested structures)
type JsonValue =
  | string
  | number
  | boolean
  | null
  | JsonObject
  | JsonArray;

interface JsonObject {
  [key: string]: JsonValue;
}
interface JsonArray extends Array<JsonValue> {}

// Parse JSON with full type safety
const config: JsonValue = JSON.parse(readFile('config.json'));
if (typeof config === 'object' && config !== null && 'database' in config) {
  const db = config.database as JsonObject;
  if (typeof db.host === 'string') {
    console.log(db.host); // Fully narrowed!
  }
}
Enter fullscreen mode Exit fullscreen mode

Error Handling Patterns

// Result type — functional error handling
type Result<T, E = AppError> =
  | { success: true; value: T }
  | { success: false; error: E };

// Usage: No try/catch at call site!
async function safeFetch<T>(url: string): Promise<Result<T>> {
  try {
    const res = await fetch(url);
    if (!res.ok) {
      return { success: false, error: new HttpError(res.status, res.statusText) };
    }
    const data = await res.json() as T;
    return { success: true, value: data };
  } catch (err) {
    return { success: false, error: new NetworkError(err instanceof Error ? err.message : String(err)) };
  }
}

// Call site is clean:
const result = await safeFetch<User>('/api/user/123');
if (result.success) {
  console.log(result.value.name); // TypeScript knows value exists
} else {
  handleAppError(result.error); // TypeScript knows error exists
}

// Assert functions (narrowing via assertion)
function assert(condition: unknown, msg?: string): asserts condition {
  if (!condition) throw new AssertionError(msg || 'Assertion failed');
}

function assertDefined<T>(val: T, msg?: string): asserts val is NonNullable<T> {
  if (val === undefined || val === null) throw new AssertionError(msg || 'Value is defined');
}

// Satisfies operator — check type without changing it
type Config = {
  port: number;
  host: string;
  tls?: boolean;
};

const cfg = {
  port: 3000,
  host: 'localhost',
  // Missing tls? That's OK, it's optional
} satisfies Config;

// But this would fail:
const badCfg = {
  port: '3000', // ❌ string, not number!
  host: 'localhost',
} satisfies Config; // Type error!

// Const assertions — lock down literal types
const ROUTES = {
  HOME: '/',
  ABOUT: '/about',
  LOGIN: '/login',
} as const;

type RouteKey = typeof ROUTES[keyof typeof ROUTES]; // "/" | "/about" | "/login"
type RouteName = keyof typeof ROUTES; // "HOME" | "ABOUT" | "LOGIN"

function navigate(route: RouteName) {
  window.location.href = ROUTES[route]; // Type-safe routing!
}
navigate('HOME'); // ✅
navigate('PROFILE'); // ❌ Error!
Enter fullscreen mode Exit fullscreen mode

What's your favorite advanced TypeScript pattern? Which one here is new to you?

Follow @armorbreak for more practical developer guides.

Top comments (0)