+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
Part 63 of 354

🔍 Generic Type Inference: Automatic Type Detection

Master TypeScript's generic type inference to write cleaner code with automatic type detection 🚀

💎Advanced
35 min read

Prerequisites

  • Strong understanding of TypeScript generics 📝
  • Knowledge of type parameters and constraints 🔍
  • Familiarity with TypeScript's type system 💻

What you'll learn

  • Master automatic type inference in generics 🎯
  • Write cleaner code without explicit type annotations 🏗️
  • Understand inference algorithms and patterns 🛡️
  • Build inference-friendly generic APIs ✨

🎯 Introduction

Welcome to the fascinating world of generic type inference! 🎉 In this guide, we’ll explore how TypeScript automatically detects types in generic contexts, making your code cleaner and more maintainable.

You’ll discover how type inference is like a smart assistant 🤖 - it figures out what you mean without you having to spell everything out! Whether you’re building utilities 🔧, designing APIs 🌐, or creating complex type systems 🏗️, mastering type inference is essential for writing elegant TypeScript code.

By the end of this tutorial, you’ll be leveraging TypeScript’s inference engine to write less code while maintaining complete type safety! Let’s unlock the magic of automatic type detection! 🏊‍♂️

📚 Understanding Type Inference

🤔 How Type Inference Works

TypeScript infers types from usage patterns and context:

// 🎯 Basic inference - TypeScript figures out the type
function identity<T>(value: T): T {
  return value;
}

// No need to specify <string> - TypeScript infers it!
const result1 = identity("hello"); // Type is string
const result2 = identity(42); // Type is number
const result3 = identity({ name: "Alice" }); // Type is { name: string }

// 🏗️ Inference from multiple arguments
function pair<T, U>(first: T, second: U): [T, U] {
  return [first, second];
}

// TypeScript infers both T and U
const coords = pair(10, 20); // [number, number]
const mixed = pair("hello", true); // [string, boolean]

// 🔧 Inference with constraints
function getLength<T extends { length: number }>(item: T): number {
  return item.length;
}

// TypeScript infers the specific type while ensuring it has length
const strLen = getLength("hello"); // T is string
const arrLen = getLength([1, 2, 3]); // T is number[]
const objLen = getLength({ length: 5, value: "test" }); // T is { length: number, value: string }

// 🎨 Contextual inference
const numbers = [1, 2, 3];
const doubled = numbers.map(n => n * 2); // n is inferred as number
const strings = ["a", "b", "c"];
const uppercased = strings.map(s => s.toUpperCase()); // s is inferred as string

💡 Inference Patterns

Common patterns that enable powerful inference:

// 🎯 Return type inference
function createContainer<T>(value: T) {
  return {
    value,
    getValue: () => value,
    setValue: (newValue: T) => { value = newValue; },
    map: <U>(fn: (val: T) => U) => createContainer(fn(value))
  };
}

// All types are inferred!
const container = createContainer(42);
const value = container.getValue(); // number
const mapped = container.map(n => n.toString()); // Container<string>

// 🏗️ Inference from array patterns
function first<T>(arr: T[]): T | undefined {
  return arr[0];
}

function last<T>(arr: readonly T[]): T | undefined {
  return arr[arr.length - 1];
}

function middle<T>(arr: T[]): T[] {
  return arr.slice(1, -1);
}

const nums = [1, 2, 3, 4, 5];
const firstNum = first(nums); // number | undefined
const lastNum = last(nums); // number | undefined
const middleNums = middle(nums); // number[]

// 🔧 Inference with multiple candidates
function merge<T extends object, U extends object>(obj1: T, obj2: U): T & U {
  return { ...obj1, ...obj2 };
}

const person = { name: "Alice", age: 30 };
const job = { title: "Developer", company: "TechCorp" };
const employee = merge(person, job); // { name: string; age: number; title: string; company: string; }

// 🎨 Conditional type inference
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
type UnwrapArray<T> = T extends (infer U)[] ? U : T;

function processValue<T>(value: T): UnwrapPromise<T> {
  if (value instanceof Promise) {
    throw new Error("Use processAsync for promises");
  }
  return value as UnwrapPromise<T>;
}

const syncResult = processValue(42); // number
const stringResult = processValue("hello"); // string

🚀 Advanced Inference Techniques

🔑 Inference with Mapped Types

Leveraging mapped types for powerful inference:

// 🎯 Key inference in mapped types
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};

function createGetters<T extends object>(obj: T): Getters<T> {
  const getters = {} as Getters<T>;
  
  for (const key in obj) {
    const capitalizedKey = key.charAt(0).toUpperCase() + key.slice(1);
    const getterName = `get${capitalizedKey}` as any;
    getters[getterName] = () => obj[key];
  }
  
  return getters;
}

const data = { name: "Alice", age: 30, active: true };
const getters = createGetters(data);
// Type is { getName: () => string; getAge: () => number; getActive: () => boolean; }

const name = getters.getName(); // string
const age = getters.getAge(); // number

// 🏗️ Deep inference with recursive types
type DeepReadonly<T> = {
  readonly [P in keyof T]: T[P] extends object ? DeepReadonly<T[P]> : T[P]
};

function deepFreeze<T extends object>(obj: T): DeepReadonly<T> {
  Object.freeze(obj);
  Object.values(obj).forEach(value => {
    if (typeof value === 'object' && value !== null) {
      deepFreeze(value);
    }
  });
  return obj as DeepReadonly<T>;
}

const config = {
  app: { name: "MyApp", version: "1.0.0" },
  features: { auth: true, analytics: false }
};

const frozen = deepFreeze(config);
// frozen.app.name = "Other"; // Error: readonly property

// 🔧 Inference with template literal types
type RouteParams<T extends string> = 
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? { [K in Param]: string } & RouteParams<Rest>
    : T extends `${infer _Start}:${infer Param}`
    ? { [K in Param]: string }
    : {};

function parseRoute<T extends string>(
  route: T,
  url: string
): RouteParams<T> {
  // Implementation details...
  return {} as RouteParams<T>;
}

const params = parseRoute("/users/:userId/posts/:postId", "/users/123/posts/456");
// Type is { userId: string; postId: string; }

// 🎨 Inference with conditional types
type EventHandlers<T> = {
  [K in keyof T as T[K] extends (...args: any[]) => any ? K : never]: T[K]
};

function extractHandlers<T extends object>(obj: T): EventHandlers<T> {
  const handlers = {} as EventHandlers<T>;
  
  for (const key in obj) {
    if (typeof obj[key] === 'function') {
      (handlers as any)[key] = obj[key];
    }
  }
  
  return handlers;
}

const component = {
  name: "Button",
  disabled: false,
  onClick: () => console.log("clicked"),
  onHover: () => console.log("hovered"),
  render: () => "<button>Click me</button>"
};

const handlers = extractHandlers(component);
// Type is { onClick: () => void; onHover: () => void; render: () => string; }

🎭 Function Composition Inference

Building type-safe function composition with inference:

// 🎯 Pipe function with perfect inference
type PipeArgs<F extends ((...args: any[]) => any)[]> = 
  F extends [(...args: infer A) => any, ...any[]] ? A : never;

type PipeReturn<F extends ((...args: any[]) => any)[]> = 
  F extends [...any[], (...args: any[]) => infer R] ? R : never;

function pipe<F extends ((...args: any[]) => any)[]>(
  ...fns: F
): (...args: PipeArgs<F>) => PipeReturn<F> {
  return (...args) => {
    return fns.reduce((result, fn, index) => {
      return index === 0 ? fn(...args) : fn(result);
    }, undefined as any);
  };
}

// Perfect type inference!
const process = pipe(
  (x: number) => x * 2,
  (x: number) => x + 10,
  (x: number) => x.toString(),
  (x: string) => x.split('').reverse().join('')
);

const result = process(5); // Type is string, value is "02"

// 🏗️ Compose with inference
function compose<F extends ((...args: any[]) => any)[]>(
  ...fns: F
): (...args: Parameters<F[F['length'] extends number ? F['length'] - 1 : 0]>) => ReturnType<F[0]> {
  return (...args) => {
    return fns.reduceRight((result, fn, index) => {
      return index === fns.length - 1 ? fn(...args) : fn(result);
    }, undefined as any);
  };
}

// 🔧 Async pipe with inference
type AsyncPipeReturn<F extends ((...args: any[]) => any)[]> = 
  F extends [...any[], (...args: any[]) => infer R] 
    ? R extends Promise<any> ? R : Promise<R>
    : Promise<any>;

function asyncPipe<F extends ((...args: any[]) => any)[]>(
  ...fns: F
): (...args: PipeArgs<F>) => AsyncPipeReturn<F> {
  return async (...args) => {
    let result: any = args;
    
    for (let i = 0; i < fns.length; i++) {
      result = i === 0 ? await fns[i](...result) : await fns[i](result);
    }
    
    return result;
  };
}

const fetchAndProcess = asyncPipe(
  async (id: string) => fetch(`/api/users/${id}`),
  async (response: Response) => response.json(),
  async (data: any) => ({ ...data, processed: true })
);

// Type is (id: string) => Promise<any>
const userData = await fetchAndProcess("123");

// 🎨 Inference with overloads
interface Transform {
  <T extends string>(value: T): Uppercase<T>;
  <T extends number>(value: T): string;
  <T extends boolean>(value: T): T extends true ? 1 : 0;
  <T>(value: T): T;
}

const transform: Transform = (value: any) => {
  if (typeof value === 'string') return value.toUpperCase();
  if (typeof value === 'number') return value.toString();
  if (typeof value === 'boolean') return value ? 1 : 0;
  return value;
};

const upper = transform("hello"); // "HELLO"
const str = transform(42); // "42"
const num = transform(true); // 1

🎪 Real-World Inference Patterns

🏭 Builder Pattern with Inference

Creating fluent builders with perfect type inference:

// 🎯 Fluent builder with progressive type building
class QueryBuilder<T = {}> {
  private query: T;
  
  constructor(query: T = {} as T) {
    this.query = query;
  }
  
  select<K extends string>(...fields: K[]): QueryBuilder<T & { select: K[] }> {
    return new QueryBuilder({ ...this.query, select: fields });
  }
  
  where<C extends Record<string, any>>(
    conditions: C
  ): QueryBuilder<T & { where: C }> {
    return new QueryBuilder({ ...this.query, where: conditions });
  }
  
  orderBy<F extends string>(
    field: F,
    direction: 'asc' | 'desc' = 'asc'
  ): QueryBuilder<T & { orderBy: { field: F; direction: 'asc' | 'desc' } }> {
    return new QueryBuilder({ ...this.query, orderBy: { field, direction } });
  }
  
  limit(count: number): QueryBuilder<T & { limit: number }> {
    return new QueryBuilder({ ...this.query, limit: count });
  }
  
  build(): T {
    return this.query;
  }
}

// Type builds progressively with each method call!
const query = new QueryBuilder()
  .select('id', 'name', 'email')
  .where({ active: true, role: 'admin' })
  .orderBy('createdAt', 'desc')
  .limit(10)
  .build();

// Type is:
// {
//   select: ("id" | "name" | "email")[];
//   where: { active: boolean; role: string };
//   orderBy: { field: "createdAt"; direction: "desc" };
//   limit: number;
// }

// 🏗️ State machine builder with inference
type StateConfig<S extends string, E extends string> = {
  initial: S;
  states: {
    [K in S]: {
      on?: {
        [Event in E]?: S;
      };
      entry?: () => void;
      exit?: () => void;
    };
  };
};

class StateMachineBuilder<
  States extends string = never,
  Events extends string = never
> {
  private config: Partial<StateConfig<States, Events>> = {};
  
  initial<S extends States>(state: S): StateMachineBuilder<States, Events> {
    this.config.initial = state;
    return this;
  }
  
  addState<S extends string>(
    state: S,
    config?: {
      on?: Record<Events, States | S>;
      entry?: () => void;
      exit?: () => void;
    }
  ): StateMachineBuilder<States | S, Events> {
    if (!this.config.states) {
      this.config.states = {} as any;
    }
    (this.config.states as any)[state] = config || {};
    return this as any;
  }
  
  addEvent<E extends string>(): StateMachineBuilder<States, Events | E> {
    return this as any;
  }
  
  build(): StateConfig<States, Events> {
    return this.config as StateConfig<States, Events>;
  }
}

// Types are inferred as you build!
const machine = new StateMachineBuilder()
  .addState('idle', {
    entry: () => console.log('Entering idle')
  })
  .addState('loading')
  .addState('error')
  .addEvent<'FETCH'>()
  .addEvent<'SUCCESS'>()
  .addEvent<'FAILURE'>()
  .initial('idle')
  .build();

// 🔧 Pipeline builder with transform inference
class Pipeline<TInput, TOutput = TInput> {
  private transforms: ((value: any) => any)[] = [];
  
  constructor(private phantom?: { input: TInput; output: TOutput }) {}
  
  pipe<TNext>(
    transform: (value: TOutput) => TNext
  ): Pipeline<TInput, TNext> {
    this.transforms.push(transform);
    return new Pipeline({ input: {} as TInput, output: {} as TNext });
  }
  
  async execute(input: TInput): Promise<TOutput> {
    let result: any = input;
    
    for (const transform of this.transforms) {
      result = await transform(result);
    }
    
    return result;
  }
}

// Type flows through the pipeline!
const dataPipeline = new Pipeline<string>()
  .pipe(str => str.split(','))
  .pipe(arr => arr.map(s => parseInt(s)))
  .pipe(nums => nums.filter(n => n > 0))
  .pipe(nums => nums.reduce((a, b) => a + b, 0));

const sum = await dataPipeline.execute("1,2,3,4,5"); // Type is number

🔐 Validation with Inference

Type-safe validation with automatic schema inference:

// 🎯 Schema builder with type inference
type InferType<T> = 
  T extends StringSchema ? string :
  T extends NumberSchema ? number :
  T extends BooleanSchema ? boolean :
  T extends ArraySchema<infer U> ? InferType<U>[] :
  T extends ObjectSchema<infer S> ? { [K in keyof S]: InferType<S[K]> } :
  never;

class StringSchema {
  min(length: number): this { return this; }
  max(length: number): this { return this; }
  email(): this { return this; }
  pattern(regex: RegExp): this { return this; }
}

class NumberSchema {
  min(value: number): this { return this; }
  max(value: number): this { return this; }
  integer(): this { return this; }
  positive(): this { return this; }
}

class BooleanSchema {}

class ArraySchema<T> {
  constructor(private itemSchema: T) {}
  min(length: number): this { return this; }
  max(length: number): this { return this; }
}

class ObjectSchema<T extends Record<string, any>> {
  constructor(private shape: T) {}
}

// Builder functions with inference
const z = {
  string: () => new StringSchema(),
  number: () => new NumberSchema(),
  boolean: () => new BooleanSchema(),
  array: <T>(schema: T) => new ArraySchema(schema),
  object: <T extends Record<string, any>>(shape: T) => new ObjectSchema(shape)
};

// Define schema
const userSchema = z.object({
  id: z.number().positive(),
  name: z.string().min(2).max(50),
  email: z.string().email(),
  tags: z.array(z.string()),
  settings: z.object({
    theme: z.string(),
    notifications: z.boolean()
  })
});

// Type is automatically inferred!
type User = InferType<typeof userSchema>;
// {
//   id: number;
//   name: string;
//   email: string;
//   tags: string[];
//   settings: {
//     theme: string;
//     notifications: boolean;
//   };
// }

// 🏗️ Form builder with inference
class FormBuilder<T = {}> {
  private fields: T = {} as T;
  
  field<K extends string, V>(
    name: K,
    config: {
      type: V;
      validators?: Array<(value: V) => boolean>;
      default?: V;
    }
  ): FormBuilder<T & Record<K, V>> {
    (this.fields as any)[name] = config;
    return this as any;
  }
  
  build(): FormSchema<T> {
    return new FormSchema(this.fields);
  }
}

class FormSchema<T> {
  constructor(private schema: T) {}
  
  validate(data: T): ValidationResult<T> {
    // Validation logic
    return { valid: true, errors: {} };
  }
  
  getDefaults(): T {
    // Get default values
    return {} as T;
  }
}

type ValidationResult<T> = {
  valid: boolean;
  errors: Partial<Record<keyof T, string[]>>;
};

// Build form with inferred types
const loginForm = new FormBuilder()
  .field('username', {
    type: '' as string,
    validators: [v => v.length >= 3]
  })
  .field('password', {
    type: '' as string,
    validators: [v => v.length >= 8]
  })
  .field('rememberMe', {
    type: false as boolean,
    default: false
  })
  .build();

// Type is inferred!
const formData = loginForm.getDefaults();
// { username: string; password: string; rememberMe: boolean; }

🎮 Event System with Inference

Building type-safe event systems with automatic type detection:

// 🎯 Event emitter with inferred event types
class TypedEmitter<T extends Record<string, (...args: any[]) => void> = {}> {
  private handlers = new Map<keyof T, Set<Function>>();
  
  on<K extends keyof T>(event: K, handler: T[K]): this {
    if (!this.handlers.has(event)) {
      this.handlers.set(event, new Set());
    }
    this.handlers.get(event)!.add(handler);
    return this;
  }
  
  emit<K extends keyof T>(
    event: K,
    ...args: Parameters<T[K]>
  ): this {
    this.handlers.get(event)?.forEach(handler => {
      handler(...args);
    });
    return this;
  }
  
  // Infer and add new event types
  addEvent<K extends string, H extends (...args: any[]) => void>(
    event: K
  ): TypedEmitter<T & Record<K, H>> {
    return this as any;
  }
}

// Start with empty emitter
const emitter = new TypedEmitter()
  .addEvent<'login', (user: { id: string; name: string }) => void>('login')
  .addEvent<'logout', () => void>('logout')
  .addEvent<'error', (error: Error, context?: string) => void>('error');

// Types are fully inferred!
emitter
  .on('login', (user) => console.log(`${user.name} logged in`))
  .on('logout', () => console.log('User logged out'))
  .on('error', (err, ctx) => console.error(`Error in ${ctx}: ${err.message}`));

emitter.emit('login', { id: '123', name: 'Alice' });
emitter.emit('error', new Error('Failed'), 'auth');

// 🏗️ Observable with type inference
class Observable<T> {
  constructor(
    private subscribe: (observer: Observer<T>) => Subscription
  ) {}
  
  pipe<R>(...operators: OperatorFunction<any, any>[]): Observable<R> {
    return operators.reduce(
      (source, operator) => operator(source),
      this
    ) as Observable<R>;
  }
  
  static create<T>(
    subscribe: (observer: Observer<T>) => void | Subscription
  ): Observable<T> {
    return new Observable(subscribe);
  }
}

interface Observer<T> {
  next(value: T): void;
  error?(err: any): void;
  complete?(): void;
}

interface Subscription {
  unsubscribe(): void;
}

type OperatorFunction<T, R> = (source: Observable<T>) => Observable<R>;

// Operator functions with inference
function map<T, R>(
  project: (value: T, index: number) => R
): OperatorFunction<T, R> {
  return (source) => Observable.create(observer => {
    let index = 0;
    return source.subscribe({
      next: (value) => observer.next(project(value, index++)),
      error: (err) => observer.error?.(err),
      complete: () => observer.complete?.()
    });
  });
}

function filter<T>(
  predicate: (value: T, index: number) => boolean
): OperatorFunction<T, T> {
  return (source) => Observable.create(observer => {
    let index = 0;
    return source.subscribe({
      next: (value) => {
        if (predicate(value, index++)) {
          observer.next(value);
        }
      },
      error: (err) => observer.error?.(err),
      complete: () => observer.complete?.()
    });
  });
}

// Type flows through operators!
const numbers$ = Observable.create<number>(observer => {
  [1, 2, 3, 4, 5].forEach(n => observer.next(n));
  observer.complete?.();
  return { unsubscribe: () => {} };
});

const processed$ = numbers$.pipe(
  filter(n => n % 2 === 0),
  map(n => n * 2),
  map(n => `Number: ${n}`)
); // Type is Observable<string>

🎮 Hands-On Exercise

Let’s build a type-safe API client with automatic type inference!

📝 Challenge: Inferred API Client

Create an API client that:

  1. Infers types from endpoint definitions
  2. Provides type-safe request/response handling
  3. Supports different HTTP methods
  4. Includes automatic error handling
// Your challenge: Implement this API client
interface ApiEndpoints {
  '/users': {
    GET: { response: User[] };
    POST: { body: CreateUserDto; response: User };
  };
  '/users/:id': {
    GET: { params: { id: string }; response: User };
    PUT: { params: { id: string }; body: UpdateUserDto; response: User };
    DELETE: { params: { id: string }; response: void };
  };
  '/posts': {
    GET: { query?: { limit?: number; offset?: number }; response: Post[] };
    POST: { body: CreatePostDto; response: Post };
  };
}

// Example usage to support:
const api = createApiClient<ApiEndpoints>({
  baseURL: 'https://api.example.com'
});

// Types should be fully inferred!
const users = await api.get('/users'); // User[]
const user = await api.post('/users', {
  body: { name: 'John', email: '[email protected]' }
}); // User

const posts = await api.get('/posts', {
  query: { limit: 10 }
}); // Post[]

💡 Solution

Click to see the solution
// 🎯 Complete type-safe API client implementation
type HttpMethod = 'GET' | 'POST' | 'PUT' | 'DELETE' | 'PATCH';

type EndpointConfig = {
  params?: Record<string, string>;
  query?: Record<string, any>;
  body?: any;
  response: any;
};

type ApiEndpointMap = Record<string, Partial<Record<HttpMethod, EndpointConfig>>>;

// Extract types from endpoint configuration
type ExtractParams<T> = T extends { params: infer P } ? P : {};
type ExtractQuery<T> = T extends { query: infer Q } ? Q : never;
type ExtractBody<T> = T extends { body: infer B } ? B : never;
type ExtractResponse<T> = T extends { response: infer R } ? R : never;

// Build request options type
type RequestOptions<T extends EndpointConfig> = 
  {} extends ExtractParams<T> 
    ? {} extends ExtractQuery<T>
      ? T extends { body: any }
        ? { body: ExtractBody<T> }
        : {}
      : T extends { body: any }
        ? { query?: ExtractQuery<T>; body: ExtractBody<T> }
        : { query?: ExtractQuery<T> }
    : {} extends ExtractQuery<T>
      ? T extends { body: any }
        ? { params: ExtractParams<T>; body: ExtractBody<T> }
        : { params: ExtractParams<T> }
      : T extends { body: any }
        ? { params: ExtractParams<T>; query?: ExtractQuery<T>; body: ExtractBody<T> }
        : { params: ExtractParams<T>; query?: ExtractQuery<T> };

// API client interface
interface ApiClient<T extends ApiEndpointMap> {
  get<K extends keyof T>(
    endpoint: K,
    ...args: T[K] extends { GET: infer C } 
      ? {} extends RequestOptions<C> 
        ? [options?: RequestOptions<C>]
        : [options: RequestOptions<C>]
      : never
  ): T[K] extends { GET: infer C } ? Promise<ExtractResponse<C>> : never;
  
  post<K extends keyof T>(
    endpoint: K,
    ...args: T[K] extends { POST: infer C }
      ? {} extends RequestOptions<C>
        ? [options?: RequestOptions<C>]
        : [options: RequestOptions<C>]
      : never
  ): T[K] extends { POST: infer C } ? Promise<ExtractResponse<C>> : never;
  
  put<K extends keyof T>(
    endpoint: K,
    ...args: T[K] extends { PUT: infer C }
      ? {} extends RequestOptions<C>
        ? [options?: RequestOptions<C>]
        : [options: RequestOptions<C>]
      : never
  ): T[K] extends { PUT: infer C } ? Promise<ExtractResponse<C>> : never;
  
  delete<K extends keyof T>(
    endpoint: K,
    ...args: T[K] extends { DELETE: infer C }
      ? {} extends RequestOptions<C>
        ? [options?: RequestOptions<C>]
        : [options: RequestOptions<C>]
      : never
  ): T[K] extends { DELETE: infer C } ? Promise<ExtractResponse<C>> : never;
}

// 🏗️ Implementation
class ApiClientImpl<T extends ApiEndpointMap> implements ApiClient<T> {
  constructor(private config: { baseURL: string; headers?: Record<string, string> }) {}
  
  async get<K extends keyof T>(
    endpoint: K,
    options?: any
  ): Promise<any> {
    return this.request('GET', endpoint as string, options);
  }
  
  async post<K extends keyof T>(
    endpoint: K,
    options?: any
  ): Promise<any> {
    return this.request('POST', endpoint as string, options);
  }
  
  async put<K extends keyof T>(
    endpoint: K,
    options?: any
  ): Promise<any> {
    return this.request('PUT', endpoint as string, options);
  }
  
  async delete<K extends keyof T>(
    endpoint: K,
    options?: any
  ): Promise<any> {
    return this.request('DELETE', endpoint as string, options);
  }
  
  private async request(
    method: HttpMethod,
    endpoint: string,
    options?: {
      params?: Record<string, string>;
      query?: Record<string, any>;
      body?: any;
    }
  ): Promise<any> {
    // Build URL with params
    let url = endpoint;
    if (options?.params) {
      url = url.replace(/:(\w+)/g, (_, key) => options.params![key] || '');
    }
    
    // Add query parameters
    const fullUrl = new URL(url, this.config.baseURL);
    if (options?.query) {
      Object.entries(options.query).forEach(([key, value]) => {
        if (value !== undefined) {
          fullUrl.searchParams.append(key, String(value));
        }
      });
    }
    
    // Make request
    const response = await fetch(fullUrl.toString(), {
      method,
      headers: {
        'Content-Type': 'application/json',
        ...this.config.headers
      },
      body: options?.body ? JSON.stringify(options.body) : undefined
    });
    
    if (!response.ok) {
      throw new Error(`Request failed: ${response.status} ${response.statusText}`);
    }
    
    // Handle void responses
    if (response.status === 204 || response.headers.get('content-length') === '0') {
      return;
    }
    
    return response.json();
  }
}

// 🔧 Factory function
function createApiClient<T extends ApiEndpointMap>(
  config: { baseURL: string; headers?: Record<string, string> }
): ApiClient<T> {
  return new ApiClientImpl<T>(config);
}

// 🎨 Advanced features
interface ApiClientAdvanced<T extends ApiEndpointMap> extends ApiClient<T> {
  // Interceptors
  interceptRequest(interceptor: (config: RequestConfig) => RequestConfig): void;
  interceptResponse(interceptor: (response: any) => any): void;
  
  // Batch requests
  batch<K extends (keyof T)[]>(
    requests: {
      [I in keyof K]: {
        method: HttpMethod;
        endpoint: K[I];
        options?: any;
      };
    }
  ): Promise<{
    [I in keyof K]: any;
  }>;
  
  // WebSocket support
  ws<K extends keyof T>(
    endpoint: K
  ): T[K] extends { WS: infer W } ? WebSocketConnection<W> : never;
}

interface RequestConfig {
  method: HttpMethod;
  url: string;
  headers: Record<string, string>;
  body?: any;
}

interface WebSocketConnection<T> {
  send(data: T extends { send: infer S } ? S : never): void;
  onMessage(handler: (data: T extends { receive: infer R } ? R : never) => void): void;
  close(): void;
}

// 💫 Test with example types
interface User {
  id: string;
  name: string;
  email: string;
}

interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
}

interface CreateUserDto {
  name: string;
  email: string;
}

interface UpdateUserDto {
  name?: string;
  email?: string;
}

interface CreatePostDto {
  title: string;
  content: string;
  authorId: string;
}

interface ApiEndpoints {
  '/users': {
    GET: { response: User[] };
    POST: { body: CreateUserDto; response: User };
  };
  '/users/:id': {
    GET: { params: { id: string }; response: User };
    PUT: { params: { id: string }; body: UpdateUserDto; response: User };
    DELETE: { params: { id: string }; response: void };
  };
  '/posts': {
    GET: { query?: { limit?: number; offset?: number }; response: Post[] };
    POST: { body: CreatePostDto; response: Post };
  };
  '/posts/:id': {
    GET: { params: { id: string }; response: Post };
  };
  '/ws/chat': {
    WS: {
      send: { message: string; userId: string };
      receive: { message: string; userId: string; timestamp: Date };
    };
  };
}

// Test the implementation
async function testApiClient() {
  console.log('=== API Client Test ===\n');
  
  // Mock fetch
  global.fetch = async (url: any, options: any) => {
    console.log(`${options.method} ${url}`);
    if (options.body) {
      console.log('Body:', JSON.parse(options.body));
    }
    
    // Mock responses
    const responses: Record<string, any> = {
      'GET https://api.example.com/users': [
        { id: '1', name: 'Alice', email: '[email protected]' }
      ],
      'POST https://api.example.com/users': {
        id: '2', name: 'John', email: '[email protected]'
      },
      'GET https://api.example.com/posts?limit=10': [
        { id: '1', title: 'First Post', content: 'Content', authorId: '1' }
      ]
    };
    
    return {
      ok: true,
      status: 200,
      json: async () => responses[`${options.method} ${url}`] || null,
      headers: new Map()
    } as any;
  };
  
  const api = createApiClient<ApiEndpoints>({
    baseURL: 'https://api.example.com'
  });
  
  try {
    // All types are inferred!
    console.log('Fetching users...');
    const users = await api.get('/users'); // User[]
    console.log('Users:', users);
    
    console.log('\nCreating user...');
    const newUser = await api.post('/users', {
      body: { name: 'John', email: '[email protected]' }
    }); // User
    console.log('New user:', newUser);
    
    console.log('\nFetching posts with pagination...');
    const posts = await api.get('/posts', {
      query: { limit: 10 }
    }); // Post[]
    console.log('Posts:', posts);
    
    // Type errors (uncomment to see):
    // await api.get('/invalid-endpoint'); // Error!
    // await api.post('/users', { body: { invalid: true } }); // Error!
    // await api.get('/posts', { query: { invalid: true } }); // Error!
    
  } catch (error) {
    console.error('Error:', error);
  }
  
  console.log('\n✅ Type inference working perfectly!');
}

testApiClient();

🎯 Summary

You’ve mastered generic type inference in TypeScript! 🎉 You learned how to:

  • 🔍 Leverage automatic type detection in generic contexts
  • 🤖 Write cleaner code without explicit type annotations
  • 🎯 Understand how TypeScript’s inference algorithm works
  • 🏗️ Build APIs that infer types from usage patterns
  • 🔄 Create powerful patterns with progressive type building
  • ✨ Design inference-friendly generic systems

Type inference is one of TypeScript’s most powerful features, enabling you to write less code while maintaining complete type safety. You’re now equipped to create elegant APIs that feel magical to use!

Keep leveraging the power of inference! 🚀