Skip to content
All essays
WebMarch 10, 202522 min

Advanced TypeScript Types: Utility Types, Conditional Types, and More

Master advanced TypeScript types including utility types, conditional types, mapped types, and template literal types.

Ü
Ümit Uz
Mobile & Full Stack Developer

Introduction

TypeScript's type system is incredibly powerful and flexible. Understanding advanced type patterns enables you to create type-safe, maintainable code that catches errors at compile time.

Conditional Types

Basic Conditional Types

typescript
// Syntax: T extends U ? X : Y
type IsString<T> = T extends string ? true : false;

type Test1 = IsString<string>; // true
type Test2 = IsString<number>; // false
type Test3 = IsString<'hello'>; // true

Generic Conditional Types

typescript
type NonNullable<T> = T extends null | undefined ? never : T;

type Value1 = NonNullable<string | null>; // string
type Value2 = NonNullable<number | undefined>; // number
type Value3 = NonNullable<null>; // never

// Extract promise inner type
type Unpromise<T> = T extends Promise<infer U> ? U : T;

type Data = Unpromise<Promise<number>>; // number
type Text = Unpromise<string>; // string

Distributive Conditional Types

typescript
// Distributes over union types
type ToArray<T> = T extends any ? T[] : never;

type Numbers = ToArray<string | number>; // string[] | number[]

// Non-distributive
type ToArrayNonDistributive<T> = [T] extends [any] ? T[] : never;

type Numbers2 = ToArrayNonDistributive<string | number>; // (string | number)[]

Mapped Types

Basic Mapped Types

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

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

interface User {
  id: number;
  name: string;
  email: string;
}

type ReadonlyUser = Readonly<User>;
type PartialUser = Optional<User>;

Key Remapping with 'as'

typescript
type Getters<T> = {
  [P in keyof T as `get${Capitalize<P & string>} `]: () => T[P];
};

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

type UserGetters = Getters<User>;
// {
//   getName: () => string;
//   getAge: () => number;
// }

// Filter keys
type OnlyStrings<T> = {
  [P in keyof T as T[P] extends string ? P : never]: T[P];
};

type StringProps = OnlyStrings<User>;
// {
//   name: string;
// }

Template Literal Types

typescript
type EventName<T extends string> = `on${Capitalize<T>}`;

type ClickEvent = EventName<'click'>; // 'onClick'
type ChangeEvent = EventName<'change'>; // 'onChange'

// Combine template literals
type AllEvents = EventName<'click' | 'change' | 'submit'>;
// 'onClick' | 'onChange' | 'onSubmit'

// Complex combinations
type Color = 'red' | 'blue' | 'green';
type Size = 'small' | 'medium' | 'large';
type ColorSize = `${Color}-${Size}`;
// 'red-small' | 'red-medium' | ... | 'green-large'

// String manipulation
type TrimLeft<T extends string> = T extends ` ${infer Rest}` ? TrimLeft<Rest> : T;
type TrimRight<T extends string> = T extends `${infer Rest} ` ? TrimRight<Rest> : T;
type Trim<T extends string> = TrimLeft<TrimRight<T>>;

type Test1 = Trim<'  hello  '>; // 'hello'

Advanced Utility Types

Deep Partial

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

interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  api: {
    timeout: number;
  };
}

type PartialConfig = DeepPartial<Config>;

const config: PartialConfig = {
  database: {
    host: 'localhost'
    // port, credentials optional
  }
};

Deep Required

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

interface PartialData {
  user?: {
    name?: string;
    age?: number;
  };
}

type CompleteData = DeepRequired<PartialData>;
// {
//   user: {
//     name: string;
//     age: number;
//   };
// }

Deep Readonly

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

interface State {
  user: {
    name: string;
    preferences: {
      theme: string;
    };
  };
}

type ReadonlyState = DeepReadonly<State>;

const state: ReadonlyState = {
  user: {
    name: 'John',
    preferences: {
      theme: 'dark'
    }
  }
};

// state.user.name = 'Jane'; // Error
// state.user.preferences.theme = 'light'; // Error

Mutable

typescript
type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

interface ReadonlyInterface {
  readonly id: number;
  readonly name: string;
}

type MutableInterface = Mutable<ReadonlyInterface>;
// {
//   id: number;
//   name: string;
// }

Type Manipulation

Pick and Omit

typescript
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}

// Pick specific properties
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;

// Omit specific properties
type SafeUser = Omit<User, 'password'>;

// Combined
type UserDTO = Pick<PublicUser, 'id' | 'name'>;

Extract and Exclude

typescript
type Primitives = string | number | boolean | null | undefined;

// Exclude types from union
type NonNullable = Exclude<Primitives, null | undefined>;
// string | number | boolean

// Extract types from union
type OnlyStrings = Extract<Primitives, string>;
// string

type Functions = Extract<string | number | (() => void), Function>;
// () => void

ReturnType and Parameters

typescript
function getUser(id: number): { id: number; name: string } {
  return { id, name: 'John' };
}

type UserReturn = ReturnType<typeof getUser>;
// { id: number; name: string }

async function fetchData(url: string): Promise<number> {
  return 42;
}

type DataReturn = ReturnType<typeof fetchData>;
// Promise<number>

type DataAwaited = Awaited<DataReturn>;
// number

// Extract function parameters
type Params = Parameters<typeof getUser>;
// [number]

Branded Types

Creating Branded Types

typescript
type USD = number & { readonly __brand: 'USD' };
type EUR = number & { readonly __brand: 'EUR' };

const usd = (amount: number): USD => amount as USD;
const eur = (amount: number): EUR => amount as EUR;

const addUSD = (a: USD, b: USD): USD => {
  return usd((a + b) as number);
};

// Type-safe - can't mix currencies
const total1 = addUSD(usd(100), usd(50)); // ✓ Valid
const total2 = addUSD(usd(100), eur(50)); // ✗ Error

// Use branded types in APIs
function transferUSD(amount: USD, toAccount: string) {
  // Process transfer
}

transferUSD(usd(500), 'account-123'); // ✓ Valid
transferUSD(500, 'account-123'); // ✗ Error

Email Branded Type

typescript
type Email = string & { readonly __brand: 'Email' };

const email = (value: string): Email => {
  if (!value.includes('@')) {
    throw new Error('Invalid email');
  }
  return value as Email;
};

function sendEmail(to: Email, subject: string, body: string) {
  // Send email
}

sendEmail(email('user@example.com'), 'Hello', 'World'); // ✓ Valid
sendEmail('invalid-email', 'Hello', 'World'); // ✗ Error

Type Guards and Discriminated Unions

Type Guards

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

function isNumber(value: unknown): value is number {
  return typeof value === 'number';
}

function process(value: unknown) {
  if (isString(value)) {
    return value.toUpperCase(); // TypeScript knows it's string
  }
  if (isNumber(value)) {
    return value * 2; // TypeScript knows it's number
  }
  return null;
}

Discriminated Unions

typescript
type 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;
  }
}

// Exhaustive checking
function areaExhaustive(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:
      const _exhaustiveCheck: never = shape;
      return _exhaustiveCheck;
  }
}

Advanced Pattern Matching

typescript
type Result<T, E> =
  | { success: true; data: T }
  | { success: false; error: E };

function handleResult<T, E>(result: Result<T, E>) {
  if (result.success) {
    console.log(result.data); // T
  } else {
    console.log(result.error); // E
  }
}

// Generic pattern matcher
type Match<T, Cases> = Cases extends {
  [K in keyof Cases]: (value: Extract<T, { _type: K }>) => any
}
  ? { [K in keyof Cases]: ReturnType<Cases[K]> }[keyof Cases]
  : never;

type Action =
  | { type: 'FETCH_START' }
  | { type: 'FETCH_SUCCESS'; payload: { data: string } }
  | { type: 'FETCH_ERROR'; error: string };

function matchAction<T>(action: Action, cases: T): Match<Action, T> {
  const caseKey = action.type as keyof T;
  const handler = cases[caseKey] as (action: Action) => any;
  return handler(action);
}

const result = matchAction(
  { type: 'FETCH_SUCCESS', payload: { data: 'hello' } },
  {
    FETCH_START: () => 'Loading...',
    FETCH_SUCCESS: (action) => action.payload.data,
    FETCH_ERROR: (action) => action.error
  }
);

Recursive Types

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

const data: JSONValue = {
  name: 'John',
  age: 30,
  isActive: true,
  tags: ['developer', 'typescript'],
  metadata: {
    created: '2025-01-01',
    updated: null
  }
};

// Tree structure
type TreeNode<T> = {
  value: T;
  children: TreeNode<T>[];
};

const tree: TreeNode<string> = {
  value: 'root',
  children: [
    {
      value: 'child1',
      children: []
    },
    {
      value: 'child2',
      children: [
        {
          value: 'grandchild',
          children: []
        }
      ]
    }
  ]
};

Type-Level Programming

typescript
// Type-level arithmetic
type Add<A extends number, B extends number> =
  [...Tuple<A>, ...Tuple<B>]['length'] extends number
    ? [...Tuple<A>, ...Tuple<B>]['length']
    : never;

type Tuple<T extends number, U extends any[] = []> =
  U['length'] extends T ? U : Tuple<T, [...U, any]>;

type Result = Add<2, 3>; // 5

// Type-level string manipulation
type Split<S extends string, D extends string> =
  S extends `${infer T}${D}${infer U}` ? [T, ...Split<U, D>] : [S];

type Parts = Split<'hello-world-foo', '-'>;
// ['hello', 'world', 'foo']

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

interface Config {
  database: {
    connection: {
      host: string;
      port: number;
    };
  };
}

type Host = Path<Config, 'database.connection.host'>; // string

Best Practices

1. Prefer Type Inference

typescript
// Let TypeScript infer when possible
const config = {
  apiUrl: 'https://api.example.com',
  timeout: 5000
}; // Type inferred

// Only specify types when necessary
interface User {
  id: number;
  name: string;
}

2. Use Utility Types

typescript
// Leverage built-in utility types
type Update<T> = Partial<T>;
type Readonly = Readonly<T>;
type Pick = Pick<T, K>;

3. Create Reusable Types

typescript
// Generic, reusable type definitions
type ApiResponse<T> = {
  data: T;
  status: number;
  message: string;
};

type PaginatedResponse<T> = ApiResponse<{
  items: T[];
  total: number;
  page: number;
}>;

4. Document Complex Types

typescript
/**
 * Deep partial type - makes all nested properties optional
 * @example
 * type Config = DeepPartial<{
 *   database: { host: string; port: number };
 * }>;
 * // Can specify: { database: { host: 'localhost' } }
 */
type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object
    ? DeepPartial<T[P]>
    : T[P];
};

Real-World Example: Form Builder

typescript
type FieldType = 'text' | 'number' | 'email' | 'password' | 'select';

type Field<T extends FieldType> = T extends 'select'
  ? {
      type: T;
      name: string;
      label: string;
      options: string[];
      required?: boolean;
    }
  : {
      type: T;
      name: string;
      label: string;
      placeholder?: string;
      required?: boolean;
    };

type FormSchema<T extends readonly Field<any>[]> = {
  [K in keyof T]: T[K] extends Field<infer U>
    ? U extends 'select'
      ? { [Name in T[K]['name']]: string }
      : U extends 'number'
        ? { [Name in T[K]['name']]: number }
        : { [Name in T[K]['name']]: string }
    : never
}[number];

const schema = [
  { type: 'text' as const, name: 'username', label: 'Username' },
  { type: 'email' as const, name: 'email', label: 'Email' },
  { type: 'select' as const, name: 'role', label: 'Role', options: ['admin', 'user'] }
] as const;

type FormData = FormSchema<typeof schema>;
// {
//   username: string;
//   email: string;
//   role: string;
// }

Conclusion

Advanced TypeScript types provide powerful tools for creating type-safe applications. Master these patterns to write more robust and maintainable code.

Key takeaways:

  • Conditional types enable flexible type logic
  • Mapped types transform properties systematically
  • Template literal types manipulate strings at type level
  • Utility types simplify common type operations
  • Branded types add runtime safety to primitives
  • Type guards narrow types correctly

Use these advanced type patterns to catch errors at compile time and improve your code quality!

Enjoyed this essay? Share it.

Related essays

Next essay
Developer Productivity: Tools and Techniques