Skip to main content
โšก Calmops

Advanced Type Patterns in TypeScript

Advanced Type Patterns in TypeScript

Advanced type patterns enable sophisticated type manipulation. This article covers utility types, conditional types, and advanced inference.

Introduction

Advanced type patterns provide:

  • Type manipulation
  • Code reusability
  • Type safety
  • Flexibility
  • Maintainability

Understanding these helps you:

  • Create powerful types
  • Build reusable utilities
  • Maintain type safety
  • Reduce code duplication
  • Build scalable systems

Utility Types

Common Utility Types

// โœ… Good: Partial - make all properties optional
type User = { name: string; age: number; email: string };
type PartialUser = Partial<User>;
// { name?: string; age?: number; email?: string }

// โœ… Good: Required - make all properties required
type RequiredUser = Required<PartialUser>;
// { name: string; age: number; email: string }

// โœ… Good: Readonly - make all properties readonly
type ReadonlyUser = Readonly<User>;
// { readonly name: string; readonly age: number; readonly email: string }

// โœ… Good: Pick - select specific properties
type UserPreview = Pick<User, 'name' | 'email'>;
// { name: string; email: string }

// โœ… Good: Omit - exclude specific properties
type UserWithoutEmail = Omit<User, 'email'>;
// { name: string; age: number }

// โœ… Good: Record - create object with specific keys
type Status = 'active' | 'inactive' | 'pending';
type StatusCount = Record<Status, number>;
// { active: number; inactive: number; pending: number }

// โœ… Good: Exclude - exclude types from union
type NonString = Exclude<string | number | boolean, string>;
// number | boolean

// โœ… Good: Extract - extract types from union
type StringOrNumber = Extract<string | number | boolean, string | number>;
// string | number

// โœ… Good: ReturnType - get function return type
function add(a: number, b: number): number {
  return a + b;
}
type AddReturn = ReturnType<typeof add>;
// number

// โœ… Good: Parameters - get function parameter types
type AddParams = Parameters<typeof add>;
// [a: number, b: number]

// โœ… Good: InstanceType - get class instance type
class User {
  name: string;
}
type UserInstance = InstanceType<typeof User>;
// User

Conditional Types

Basic Conditional Types

// โœ… Good: Basic conditional type
type IsString<T> = T extends string ? true : false;

type A = IsString<'hello'>; // true
type B = IsString<42>; // false

// โœ… Good: Conditional with union
type Flatten<T> = T extends Array<infer U> ? U : T;

type Str = Flatten<string[]>; // string
type Num = Flatten<number>; // number

// โœ… Good: Nested conditionals
type DeepFlat<T> = T extends Array<infer U>
  ? U extends Array<infer V>
    ? V
    : U
  : T;

type Result = DeepFlat<number[][]>; // number

Conditional Type Inference

// โœ… Good: Infer in conditional types
type GetReturnType<T> = T extends (...args: any[]) => infer R ? R : never;

type Num = GetReturnType<() => number>; // number
type Str = GetReturnType<() => string>; // string

// โœ… Good: Extract function parameters
type GetParams<T> = T extends (...args: infer P) => any ? P : never;

type Params = GetParams<(a: string, b: number) => void>;
// [a: string, b: number]

// โœ… Good: Extract Promise type
type Awaited<T> = T extends Promise<infer U> ? U : T;

type PromiseString = Awaited<Promise<string>>; // string
type PlainNumber = Awaited<number>; // number

Mapped Types

Basic Mapped Types

// โœ… Good: Basic mapped type
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

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

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

type UserSetters = Setters<User>;
// { setName: (value: string) => void; setAge: (value: number) => void }

// โœ… Good: Readonly mapping
type ReadonlyProps<T> = {
  readonly [K in keyof T]: T[K];
};

type ReadonlyUser = ReadonlyProps<User>;
// { readonly name: string; readonly age: number }

Advanced Mapped Types

// โœ… Good: Conditional mapped type
type Getters<T> = {
  [K in keyof T as T[K] extends Function ? never : `get${Capitalize<string & K>}`]: () => T[K];
};

// โœ… Good: Filter properties by type
type StringPropertiesOnly<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};

type User = { name: string; age: number; email: string };
type UserStrings = StringPropertiesOnly<User>;
// { name: string; email: string }

// โœ… Good: Create getters for specific types
type GettersForStrings<T> = {
  [K in keyof T as T[K] extends string ? `get${Capitalize<string & K>}` : never]: () => T[K];
};

type UserStringGetters = GettersForStrings<User>;
// { getName: () => string; getEmail: () => string }

Template Literal Types

Basic Template Literal Types

// โœ… Good: Template literal types
type EventName = `on${Capitalize<'click' | 'change' | 'submit'>}`;
// 'onClick' | 'onChange' | 'onSubmit'

// โœ… Good: Generate property names
type Getters<T extends Record<string, any>> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};

// โœ… Good: Create event handlers
type EventHandlers<T extends Record<string, any>> = {
  [K in keyof T as `on${Capitalize<string & K>}`]: (value: T[K]) => void;
};

type User = { name: string; age: number };
type UserHandlers = EventHandlers<User>;
// { onName: (value: string) => void; onAge: (value: number) => void }

Type Inference Patterns

Advanced Inference

// โœ… Good: Infer from object structure
type InferObject<T> = T extends { data: infer D } ? D : never;

type Result = InferObject<{ data: string; status: number }>;
// string

// โœ… Good: Infer from array
type InferArray<T> = T extends (infer U)[] ? U : never;

type Item = InferArray<string[]>;
// string

// โœ… Good: Infer from function
type InferFunction<T> = T extends (...args: any[]) => infer R ? R : never;

type Result = InferFunction<(a: number) => string>;
// string

// โœ… Good: Deep inference
type DeepInfer<T> = T extends { nested: { value: infer V } } ? V : never;

type Value = DeepInfer<{ nested: { value: number } }>;
// number

Practical Advanced Patterns

Type-Safe API Builder

// โœ… Good: Type-safe API builder
type ApiEndpoint = {
  path: string;
  method: 'GET' | 'POST' | 'PUT' | 'DELETE';
  request?: Record<string, any>;
  response: Record<string, any>;
};

type ApiBuilder<T extends Record<string, ApiEndpoint>> = {
  [K in keyof T]: (
    data: T[K]['request'] extends undefined ? void : T[K]['request']
  ) => Promise<T[K]['response']>;
};

const endpoints = {
  getUser: {
    path: '/users/:id',
    method: 'GET' as const,
    response: { id: number; name: string }
  },
  createUser: {
    path: '/users',
    method: 'POST' as const,
    request: { name: string; email: string },
    response: { id: number; name: string; email: string }
  }
};

type Api = ApiBuilder<typeof endpoints>;
// {
//   getUser: (data: void) => Promise<{ id: number; name: string }>;
//   createUser: (data: { name: string; email: string }) => Promise<{ id: number; name: string; email: string }>;
// }

Type-Safe Form Builder

// โœ… Good: Type-safe form builder
type FormField = {
  type: 'text' | 'number' | 'email' | 'checkbox';
  required?: boolean;
  validate?: (value: any) => boolean;
};

type FormSchema = Record<string, FormField>;

type FormValues<T extends FormSchema> = {
  [K in keyof T]: T[K]['type'] extends 'checkbox'
    ? boolean
    : T[K]['type'] extends 'number'
    ? number
    : string;
};

const userForm = {
  name: { type: 'text' as const, required: true },
  age: { type: 'number' as const },
  email: { type: 'email' as const, required: true },
  subscribe: { type: 'checkbox' as const }
};

type UserFormValues = FormValues<typeof userForm>;
// {
//   name: string;
//   age: number;
//   email: string;
//   subscribe: boolean;
// }

Best Practices

  1. Use utility types:

    // โœ… Good
    type PartialUser = Partial<User>;
    
    // โŒ Bad
    type PartialUser = { name?: string; age?: number };
    
  2. Leverage conditional types:

    // โœ… Good
    type Flatten<T> = T extends Array<infer U> ? U : T;
    
    // โŒ Bad
    type Flatten<T> = T extends Array<any> ? any : T;
    
  3. Use mapped types for consistency:

    // โœ… Good
    type Getters<T> = {
      [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
    };
    
    // โŒ Bad
    type Getters<T> = {
      getName: () => T['name'];
      getAge: () => T['age'];
    };
    

Common Mistakes

  1. Over-complicating types:

    // โŒ Bad
    type Complex<T> = T extends any ? T : never;
    
    // โœ… Good
    type Simple<T> = T;
    
  2. Not using infer:

    // โŒ Bad
    type GetReturn<T> = T extends (...args: any[]) => any ? any : never;
    
    // โœ… Good
    type GetReturn<T> = T extends (...args: any[]) => infer R ? R : never;
    

Summary

Advanced type patterns are powerful. Key takeaways:

  • Use utility types
  • Leverage conditional types
  • Use mapped types
  • Apply template literals
  • Infer types effectively
  • Build type-safe APIs
  • Maintain readability
  • Document complex types

Next Steps

Comments