Skip to main content
TypeScript

TypeScript Advanced Type System: Mastering Generics and Type Gymnastics

By Tech Blog56 min read
TypeScript Advanced Type System: Mastering Generics and Type Gymnastics

TypeScript Advanced Type System: Mastering Generics and Type Gymnastics

TypeScript's type system is one of its most powerful features. This comprehensive guide explores advanced type techniques including generics, conditional types, mapped types, and type gymnastics that will elevate your TypeScript skills.

Why Master Advanced Types?

  • Compile-Time Safety: Catch errors before runtime
  • Better Developer Experience: Enhanced IDE autocomplete
  • Self-Documenting Code: Types serve as living documentation
  • Refactoring Confidence: Safe large-scale code changes

1. Generics Fundamentals

1.1 Basic Generic Functions

function identity<T>(arg: T): T {
  return arg
}

const str = identity<string>('hello')
const num = identity(42) // Type inference works!

// Generic with multiple parameters
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second]
}

const result = pair('hello', 42) // [string, number]

1.2 Generic Constraints

interface Lengthwise {
  length: number
}

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

logLength('hello') // ✅ string has length
logLength([1, 2, 3]) // ✅ array has length
logLength(42) // ❌ Error: number has no length

// Keyof constraint
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
  return obj[key]
}

const user = { name: 'Alice', age: 30 }
const name = getProperty(user, 'name') // Type: string
const age = getProperty(user, 'age') // Type: number

1.3 Generic Classes

class DataStore<T> {
  private data: T[] = []

  add(item: T): void {
    this.data.push(item)
  }

  get(index: number): T | undefined {
    return this.data[index]
  }

  getAll(): T[] {
    return [...this.data]
  }
}

const numberStore = new DataStore<number>()
numberStore.add(1)
numberStore.add(2)

const stringStore = new DataStore<string>()
stringStore.add('hello')

2. Conditional Types

2.1 Basic Syntax

type IsString<T> = T extends string ? true : false

type A = IsString<string> // true
type B = IsString<number> // false

// Practical example: Extract array element type
type Flatten<T> = T extends Array<infer U> ? U : T

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

2.2 Distributive Conditional Types

type ToArray<T> = T extends any ? T[] : never

// Distributes over union types
type Arrays = ToArray<string | number>
// Result: string[] | number[]

// Filter union types
type Filter<T, U> = T extends U ? T : never

type Filtered = Filter<string | number | boolean, string | number>
// Result: string | number

2.3 The infer Keyword

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

function getUser() {
  return { name: 'Alice', age: 30 }
}

type User = GetReturnType<typeof getUser>
// { name: string; age: number }

// Unwrap Promise
type Awaited<T> = T extends Promise<infer U> ? U : T

type Data = Awaited<Promise<string>> // string

// Extract parameters
type GetParameters<T> = T extends (...args: infer P) => any ? P : never

function createPost(title: string, content: string) {
  return { title, content }
}

type Params = GetParameters<typeof createPost>
// [string, string]

3. Mapped Types

3.1 Built-in Mapped Types

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

// Partial: All properties optional
type PartialUser = Partial<User>
// { id?: number; name?: string; email?: string }

// Required: All properties required
type RequiredUser = Required<PartialUser>

// Readonly: All properties readonly
type ReadonlyUser = Readonly<User>

// Pick: Select specific properties
type UserPreview = Pick<User, 'id' | 'name'>
// { id: number; name: string }

// Omit: Exclude specific properties
type UserWithoutEmail = Omit<User, 'email'>
// { id: number; name: string }

3.2 Custom Mapped Types

// Add prefix to all keys
type Prefixed<T, P extends string> = {
  [K in keyof T as `${P}${string & K}`]: T[K]
}

interface Person {
  name: string
  age: number
}

type PrefixedPerson = Prefixed<Person, 'user_'>
// { user_name: string; user_age: number }

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

interface Mixed {
  id: number
  name: string
  age: number
  active: boolean
}

type Strings = PickByType<Mixed, string> // { name: string }
type Numbers = PickByType<Mixed, number> // { id: number; age: number }

3.3 Template Literal Types

type EventName = 'click' | 'focus' | 'blur'
type EventHandler = `on${Capitalize<EventName>}`
// "onClick" | "onFocus" | "onBlur"

// Combine multiple template literals
type HTTPMethod = 'GET' | 'POST' | 'PUT' | 'DELETE'
type Endpoint = '/users' | '/posts'
type APIRoute = `${HTTPMethod} ${Endpoint}`
// "GET /users" | "POST /users" | "GET /posts" | ...

// CSS properties
type CSSUnit = 'px' | 'em' | 'rem' | '%'
type Size = `${number}${CSSUnit}`

const width: Size = '100px' // ✅
const height: Size = '2em' // ✅
const invalid: Size = '100' // ❌

4. Utility Types

4.1 Object Utilities

// Record: Create object type
type PageConfig = Record<'home' | 'about' | 'contact', {
  title: string
  description: string
}>

const config: PageConfig = {
  home: { title: 'Home', description: 'Welcome' },
  about: { title: 'About', description: 'About us' },
  contact: { title: 'Contact', description: 'Get in touch' }
}

// Exclude: Remove from union
type T1 = Exclude<'a' | 'b' | 'c', 'a'> // 'b' | 'c'

// Extract: Keep only matching
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'b'> // 'a' | 'b'

// NonNullable: Remove null/undefined
type T3 = NonNullable<string | null | undefined> // string

4.2 Function Utilities

// ReturnType
function createUser(name: string, age: number) {
  return { id: Math.random(), name, age }
}

type User = ReturnType<typeof createUser>
// { id: number; name: string; age: number }

// Parameters
type CreateUserParams = Parameters<typeof createUser>
// [string, number]

// ConstructorParameters
class Person {
  constructor(public name: string, public age: number) {}
}

type PersonParams = ConstructorParameters<typeof Person>
// [string, number]

// InstanceType
type PersonInstance = InstanceType<typeof Person> // Person

5. Advanced Type Patterns

5.1 Deep Readonly

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

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

type ReadonlyConfig = DeepReadonly<Config>
// All nested properties are readonly

5.2 Type-Safe Path Access

type PathKeys<T> = {
  [K in keyof T]: T[K] extends object
    ? K | `${K & string}.${PathKeys<T[K]> & string}`
    : K
}[keyof T]

interface Data {
  user: {
    profile: {
      name: string
      age: number
    }
  }
}

type Paths = PathKeys<Data>
// "user" | "user.profile" | "user.profile.name" | "user.profile.age"

6. Real-World Applications

6.1 API Response Types

Perfect for React 19:

type ApiResponse<T> =
  | { success: true; data: T }
  | { success: false; error: { code: string; message: string } }

async function fetchUser(id: number): Promise<ApiResponse<User>> {
  try {
    const response = await fetch(`/api/users/${id}`)
    const data = await response.json()
    return { success: true, data }
  } catch (error) {
    return {
      success: false,
      error: { code: 'FETCH_ERROR', message: 'Failed to fetch user' }
    }
  }
}

// Type guard
function isSuccess<T>(res: ApiResponse<T>): res is { success: true; data: T } {
  return res.success
}

// Usage
const result = await fetchUser(1)
if (isSuccess(result)) {
  console.log(result.data.name) // ✅ Type-safe
}

6.2 Form State Management

type FormField<T> = {
  value: T
  error?: string
  touched: boolean
}

type FormState<T> = {
  [K in keyof T]: FormField<T[K]>
}

interface LoginForm {
  email: string
  password: string
}

type LoginState = FormState<LoginForm>
// {
//   email: FormField<string>
//   password: FormField<string>
// }

6.3 Redux-Style Actions

type Action<T extends string, P = void> = P extends void
  ? { type: T }
  : { type: T; payload: P }

type UserAction =
  | Action<'LOGIN', { email: string; token: string }>
  | Action<'LOGOUT'>
  | Action<'UPDATE', Partial<User>>

function reducer(state: UserState, action: UserAction): UserState {
  switch (action.type) {
    case 'LOGIN':
      return { ...state, ...action.payload }
    case 'LOGOUT':
      return initialState
    case 'UPDATE':
      return { ...state, ...action.payload }
  }
}

7. Performance Tips

// ❌ Avoid: Over-complex types
type Bad<T> = T extends A ? T extends B ? T extends C ? D : E : F : G

// ✅ Better: Split into steps
type Step1<T> = T extends A ? T : never
type Step2<T> = Step1<T> extends B ? T : never
type Good<T> = Step2<T> extends C ? D : E

// Use type aliases for reusability
type ProcessedData = DeepReadonly<DeepPartial<ComplexType>>

8. Integration Examples

8.1 Next.js API Routes

Works with Next.js 15:

import { NextRequest, NextResponse } from 'next/server'

interface CreatePostBody {
  title: string
  content: string
}

interface CreatePostResponse {
  success: boolean
  postId?: string
}

export async function POST(
  request: NextRequest
): Promise<NextResponse<CreatePostResponse>> {
  const body: CreatePostBody = await request.json()
  // Type-safe implementation
}

8.2 Database Queries

Works with PostgreSQL:

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

type QueryResult<T> = Promise<{ rows: T[]; rowCount: number }>

async function findUsers(): QueryResult<User> {
  return db.query('SELECT * FROM users')
}

9. Best Practices

  • ✅ Enable strict: true in tsconfig.json
  • ✅ Use type inference when possible
  • ✅ Prefer unknown over any
  • ✅ Write type guards for runtime checks
  • ✅ Keep types simple and readable
  • ✅ Use utility types from TypeScript
  • ✅ Split complex types into smaller ones
  • ✅ Document complex type logic

Related Resources

Conclusion

TypeScript's advanced type system enables building robust, maintainable applications. Master generics for reusability, conditional types for flexibility, and mapped types for transformations. The goal is type safety and developer experience, not complexity. Start simple and gradually adopt advanced patterns as needed.

Comments

Share your thoughts and join the discussion

Login required to comment0 / 1000

Please or to comment

Comments (0)

Related Articles