सामग्री पर जाएं
·22 मिनट पढ़ने का समय

TypeScript Advanced Types: Conditional, Mapped, और Template Literal Magic

TypeScript के सबसे शक्तिशाली type features में गहरी डुबकी — conditional types, mapped types, infer keyword, और template literal types। असली patterns जो आप वाकई इस्तेमाल करेंगे।

साझा करें:X / TwitterLinkedIn

ज़्यादातर TypeScript developers एक ही जगह आकर रुक जाते हैं। उन्हें interfaces, generics, union types आते हैं। वे एक React component को type कर सकते हैं। वे red squiggles को हटा सकते हैं। लेकिन वे type system को एक ज़रूरी बुराई मानते हैं — कुछ ऐसा जो bugs रोकने के लिए है, न कि कुछ जो सक्रिय रूप से बेहतर software design करने में मदद करे।

मैं लगभग दो साल उस plateau पर रहा। जिस चीज़ ने मुझे बाहर निकाला वो थी यह समझना कि TypeScript का type system अपने आप में एक programming language है। इसमें conditionals, loops, pattern matching, recursion, और string manipulation है। एक बार जब आप यह internalize कर लेते हैं, सब कुछ बदल जाता है। आप compiler से लड़ना बंद करते हैं और उसके साथ collaborate करना शुरू करते हैं।

यह पोस्ट उन type-level features को cover करती है जो मैं production code में लगातार इस्तेमाल करता हूं। Academic exercises नहीं — असली patterns जिन्होंने मुझे असली bugs से बचाया है।

Type-Level Programming Mindset#

Syntax में जाने से पहले, मैं चाहता हूं कि आप types के बारे में कैसे सोचते हैं, उसे reframe करें।

Value-level programming में, आप ऐसे functions लिखते हैं जो runtime पर data transform करते हैं:

typescript
function extractIds(users: User[]): number[] {
  return users.map(u => u.id);
}

Type-level programming में, आप ऐसे types लिखते हैं जो compile time पर दूसरे types को transform करते हैं:

typescript
type ExtractIds<T extends { id: unknown }[]> = T[number]["id"];

Mental model वही है — input, transformation, output। फर्क यह है कि type-level code compilation के दौरान चलता है, runtime पर नहीं। यह zero JavaScript produce करता है। इसका एकमात्र उद्देश्य impossible states को unrepresentable बनाना है।

अगर आपने कभी ऐसा function लिखा है जहां चार parameters में से तीन केवल कुछ combinations में ही meaningful हैं, या जहां return type इस पर depend करता है कि आप क्या pass करते हैं, तो आपको पहले से type-level programming की ज़रूरत थी। बस शायद आपको पता नहीं था कि TypeScript इसे express कर सकता है।

Conditional Types#

Conditional types type system का if/else हैं। Syntax एक ternary जैसा दिखता है:

typescript
type IsString<T> = T extends string ? true : false;
 
type A = IsString<"hello">; // true
type B = IsString<42>;      // false
type C = IsString<string>;  // true

यहां extends keyword का मतलब inheritance नहीं है। इसका मतलब है "is assignable to।" इसे ऐसे सोचें: "क्या T string की shape में fit होता है?"

अपने खुद के Utility Types बनाना#

चलिए TypeScript के कुछ built-in utility types को rebuild करते हैं ताकि समझें वे कैसे काम करते हैं।

NonNullable — union से null और undefined हटाता है:

typescript
type MyNonNullable<T> = T extends null | undefined ? never : T;
 
type Test = MyNonNullable<string | null | undefined>;
// Result: string

never type "empty set" है — union से कुछ हटाना। जब conditional type की एक branch never resolve होती है, union का वो member बस गायब हो जाता है।

Extract — केवल उन union members को रखता है जो constraint से match करते हैं:

typescript
type MyExtract<T, U> = T extends U ? T : never;
 
type Numbers = Extract<string | number | boolean, number | boolean>;
// Result: number | boolean

Exclude — इसका उल्टा, matching members को हटाता है:

typescript
type MyExclude<T, U> = T extends U ? never : T;
 
type WithoutStrings = Exclude<string | number | boolean, string>;
// Result: number | boolean

Distributive Conditional Types#

यहां चीज़ें दिलचस्प होती हैं, और यहीं ज़्यादातर लोग confused होते हैं। जब आप एक union type को conditional type में pass करते हैं, TypeScript condition को union के हर member पर individually distribute करता है।

typescript
type ToArray<T> = T extends unknown ? T[] : never;
 
type Result = ToArray<string | number>;
// आप expect कर सकते हैं: (string | number)[]
// असली result:    string[] | number[]

TypeScript (string | number) extends unknown evaluate नहीं करता। बल्कि, वो string extends unknown और number extends unknown को अलग-अलग evaluate करता है, फिर results को union करता है।

इसीलिए Extract और Exclude वैसे काम करते हैं जैसे करते हैं। Distribution automatically होता है जब T एक "naked" type parameter होता है (किसी चीज़ में wrapped नहीं)।

अगर आप distribution को रोकना चाहते हैं, दोनों sides को tuple में wrap करें:

typescript
type ToArrayNonDist<T> = [T] extends [unknown] ? T[] : never;
 
type Result = ToArrayNonDist<string | number>;
// Result: (string | number)[]

मुझे इससे जितनी बार काटा गया है, मैं गिनना भी नहीं चाहता। अगर आपका conditional type union inputs के साथ unexpected results दे रहा है, distribution लगभग हमेशा कारण है।

Practical Example: API Response Handler#

यह एक pattern है जो मैं API responses deal करते समय इस्तेमाल करता हूं जो या तो succeed या fail हो सकते हैं:

typescript
type ApiResponse<T> =
  | { status: "success"; data: T; error: never }
  | { status: "error"; data: never; error: string };
 
type ExtractData<R> = R extends { status: "success"; data: infer D } ? D : never;
type ExtractError<R> = R extends { status: "error"; error: infer E } ? E : never;
 
type UserResponse = ApiResponse<{ id: number; name: string }>;
 
type UserData = ExtractData<UserResponse>;
// Result: { id: number; name: string }
 
type UserError = ExtractError<UserResponse>;
// Result: string

यह हर response shape के लिए manual type guards लिखने से ज़्यादा clean है।

infer Keyword#

infer types के लिए pattern matching है। यह आपको conditional type के अंदर एक type variable declare करने देता है जो TypeScript आपके लिए figure out करेगा।

इसे ऐसे सोचें: "मुझे पता है इस type की एक certain shape है। जो part मुझे चाहिए वो निकालो।"

Return Types Extract करना#

typescript
type MyReturnType<T> = T extends (...args: unknown[]) => infer R ? R : never;
 
type Fn = (x: number, y: string) => boolean;
type Result = MyReturnType<Fn>; // boolean

TypeScript pattern (...args) => something देखता है, आपके function को उसके against match करता है, और return type को R assign करता है।

Parameter Types Extract करना#

typescript
type MyParameters<T> = T extends (...args: infer P) => unknown ? P : never;
 
type Fn = (x: number, y: string) => void;
type Params = MyParameters<Fn>; // [x: number, y: string]

Promises Unwrap करना#

यह मैं हर समय इस्तेमाल करता हूं async functions के साथ:

typescript
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
 
type A = UnpackPromise<Promise<string>>;  // string
type B = UnpackPromise<Promise<number[]>>; // number[]
type C = UnpackPromise<string>;            // string (passthrough)

Deeply nested promises के लिए:

typescript
type DeepUnpackPromise<T> = T extends Promise<infer U>
  ? DeepUnpackPromise<U>
  : T;
 
type Deep = DeepUnpackPromise<Promise<Promise<Promise<number>>>>;
// Result: number

Array Element Types Extract करना#

typescript
type ElementOf<T> = T extends (infer E)[] ? E : never;
 
type Items = ElementOf<string[]>;         // string
type Mixed = ElementOf<(string | number)[]>; // string | number

एक Type में Multiple infer#

आप अलग-अलग parts extract करने के लिए infer कई बार इस्तेमाल कर सकते हैं:

typescript
type FirstAndLast<T extends unknown[]> =
  T extends [infer First, ...unknown[], infer Last]
    ? { first: First; last: Last }
    : never;
 
type Result = FirstAndLast<[1, 2, 3, 4]>;
// Result: { first: 1; last: 4 }

Real-World: Type-Safe Event Extraction#

यह एक pattern है जो मैं event-driven systems में इस्तेमाल करता हूं:

typescript
type EventMap = {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "order:created": { orderId: string; total: number };
  "order:shipped": { orderId: string; trackingId: string };
};
 
type EventName = keyof EventMap;
type EventPayload<E extends EventName> = EventMap[E];
 
// सभी user-related events extract करें
type UserEvents = Extract<EventName, `user:${string}`>;
// Result: "user:login" | "user:logout"
 
// केवल user events का payload पाएं
type UserEventPayloads = EventPayload<UserEvents>;
// Result: { userId: string; timestamp: number } | { userId: string }

Mapped Types#

Mapped types आपको existing type की हर property को transform करके नए object types बनाने देते हैं। ये type system का map() हैं।

Basics#

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

keyof T आपको सभी property keys का union देता है। [K in ...] हर एक पर iterate करता है। T[K] उस property का type है।

Scratch से Utility Types बनाना#

Required — सभी properties को mandatory बनाता है:

typescript
type MyRequired<T> = {
  [K in keyof T]-?: T[K];
};

-? syntax optional modifier हटाता है। इसी तरह, -readonly readonly हटाता है।

Pick — specific properties select करता है:

typescript
type MyPick<T, K extends keyof T> = {
  [P in K]: T[P];
};
 
interface User {
  id: number;
  name: string;
  email: string;
  password: string;
}
 
type PublicUser = MyPick<User, "id" | "name" | "email">;
// { id: number; name: string; email: string }

Omit — specific properties हटाता है:

typescript
type MyOmit<T, K extends keyof T> = {
  [P in Exclude<keyof T, K>]: T[P];
};
 
type SafeUser = MyOmit<User, "password">;
// { id: number; name: string; email: string }

Record — specific keys और value types के साथ object type बनाता है:

typescript
type MyRecord<K extends string | number | symbol, V> = {
  [P in K]: V;
};
 
type StatusMap = MyRecord<"active" | "inactive" | "banned", boolean>;
// { active: boolean; inactive: boolean; banned: boolean }

as के साथ Key Remapping#

TypeScript 4.1 ने key remapping introduce की, जो property names transform करने देती है:

typescript
type Getters<T> = {
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K];
};
 
interface Person {
  name: string;
  age: number;
  email: string;
}
 
type PersonGetters = Getters<Person>;
// {
//   getName: () => string;
//   getAge: () => number;
//   getEmail: () => string;
// }

आप never पर remap करके properties filter भी कर सकते हैं:

typescript
type OnlyStrings<T> = {
  [K in keyof T as T[K] extends string ? K : never]: T[K];
};
 
type StringProps = OnlyStrings<Person>;
// { name: string; email: string }

यह incredibly powerful है। आप simultaneously properties पर iterate कर रहे हैं, उन्हें filter कर रहे हैं, और उनके names transform कर रहे हैं — सब type level पर।

Practical Example: Form Validation Types#

यह pattern मैंने कई form libraries में इस्तेमाल किया है:

typescript
interface FormFields {
  username: string;
  email: string;
  age: number;
  bio: string;
}
 
type ValidationErrors<T> = {
  [K in keyof T]?: string[];
};
 
type TouchedFields<T> = {
  [K in keyof T]?: boolean;
};
 
type DirtyFields<T> = {
  [K in keyof T]?: boolean;
};
 
type FormState<T> = {
  values: T;
  errors: ValidationErrors<T>;
  touched: TouchedFields<T>;
  dirty: DirtyFields<T>;
  isValid: boolean;
  isSubmitting: boolean;
};
 
// अब आपकी form state fields के आधार पर पूरी तरह typed है
type MyFormState = FormState<FormFields>;

FormFields में एक field add करें, और हर related type automatically update हो जाता है। एक field remove करें, और compiler हर reference पकड़ लेता है। यही वो चीज़ है जो bugs की पूरी categories को रोकती है।

Template Literal Types#

Template literal types आपको type level पर strings manipulate करने देते हैं। ये JavaScript template literals जैसा ही backtick syntax इस्तेमाल करते हैं, लेकिन types के लिए।

Basic String Manipulation#

typescript
type Greeting = `Hello, ${string}`;
 
const a: Greeting = "Hello, world";    // OK
const b: Greeting = "Hello, TypeScript"; // OK
// const c: Greeting = "Hi, world";    // Error!

TypeScript built-in string manipulation types भी provide करता है:

typescript
type Upper = Uppercase<"hello">;     // "HELLO"
type Lower = Lowercase<"HELLO">;     // "hello"
type Cap = Capitalize<"hello">;      // "Hello"
type Uncap = Uncapitalize<"Hello">;  // "hello"

Event Name Patterns#

यहां template literal types सच में चमकते हैं। Type-safe event systems के लिए एक pattern:

typescript
type EventNames<T extends string> = `${T}Changed` | `${T}Deleted` | `${T}Created`;
 
type ModelEvents = EventNames<"user" | "order" | "product">;
// "userChanged" | "userDeleted" | "userCreated"
// | "orderChanged" | "orderDeleted" | "orderCreated"
// | "productChanged" | "productDeleted" | "productCreated"

दो type parameters से नौ events। और सब type-safe हैं।

CSS-in-TypeScript Patterns#

Template literal types CSS patterns को type level पर enforce कर सकते हैं:

typescript
type CSSUnit = "px" | "rem" | "em" | "vh" | "vw" | "%";
type CSSValue = `${number}${CSSUnit}`;
 
function setWidth(value: CSSValue): void {
  // implementation
}
 
setWidth("100px");   // OK
setWidth("2.5rem");  // OK
setWidth("50%");     // OK
// setWidth("100");  // Error: valid CSS value नहीं है
// setWidth("big");  // Error

Template Literals से Strings Parse करना#

आप template literal types को infer के साथ strings parse करने के लिए इस्तेमाल कर सकते हैं:

typescript
type ParseRoute<T extends string> =
  T extends `${infer _Start}:${infer Param}/${infer Rest}`
    ? Param | ParseRoute<`/${Rest}`>
    : T extends `${infer _Start}:${infer Param}`
      ? Param
      : never;
 
type RouteParams = ParseRoute<"/users/:userId/posts/:postId">;
// Result: "userId" | "postId"

यही वो foundation है जिस पर tRPC जैसी type-safe routing libraries काम करती हैं। String /users/:userId/posts/:postId को type level पर parse किया जाता है parameter names extract करने के लिए।

Template Literals को Mapped Types के साथ Combine करना#

यहां चीज़ें सच में powerful हो जाती हैं:

typescript
type EventHandlers<T extends Record<string, unknown>> = {
  [K in keyof T as `on${Capitalize<string & K>}Change`]: (
    newValue: T[K],
    oldValue: T[K]
  ) => void;
};
 
interface ThemeConfig {
  color: string;
  fontSize: number;
  darkMode: boolean;
}
 
type ThemeHandlers = EventHandlers<ThemeConfig>;
// {
//   onColorChange: (newValue: string, oldValue: string) => void;
//   onFontSizeChange: (newValue: number, oldValue: number) => void;
//   onDarkModeChange: (newValue: boolean, oldValue: boolean) => void;
// }

एक simple config interface से, आपको सही parameter types के साथ पूरी तरह typed event handlers मिलते हैं। यही वो type-level code है जो वास्तव में development time बचाता है।

Advanced: Dot-Notation Path Types#

एक और advanced pattern — nested object के सभी possible dot-notation paths generate करना:

typescript
type DotPaths<T, Prefix extends string = ""> = T extends object
  ? {
      [K in keyof T & string]: T[K] extends object
        ? `${Prefix}${K}` | DotPaths<T[K], `${Prefix}${K}.`>
        : `${Prefix}${K}`;
    }[keyof T & string]
  : never;
 
interface Config {
  database: {
    host: string;
    port: number;
    credentials: {
      username: string;
      password: string;
    };
  };
  server: {
    port: number;
  };
}
 
type ConfigPaths = DotPaths<Config>;
// "database" | "database.host" | "database.port"
// | "database.credentials" | "database.credentials.username"
// | "database.credentials.password" | "server" | "server.port"

अब आप एक getConfig function लिख सकते हैं जो हर possible path autocomplete करे और हर path के लिए सही type return करे:

typescript
type GetValueByPath<T, P extends string> =
  P extends `${infer Key}.${infer Rest}`
    ? Key extends keyof T
      ? GetValueByPath<T[Key], Rest>
      : never
    : P extends keyof T
      ? T[P]
      : never;
 
function getConfig<P extends ConfigPaths>(path: P): GetValueByPath<Config, P> {
  // implementation
  const keys = path.split(".");
  let result: unknown = config;
  for (const key of keys) {
    result = (result as Record<string, unknown>)[key];
  }
  return result as GetValueByPath<Config, P>;
}
 
const host = getConfig("database.host");         // type: string
const port = getConfig("database.port");         // type: number
const username = getConfig("database.credentials.username"); // type: string

Full autocomplete, full type safety, types से zero runtime overhead।

Recursive Types#

TypeScript recursive type definitions support करता है, जो arbitrarily nested structures handle करने देता है।

Deep Partial#

Built-in Partial<T> केवल top-level properties को optional बनाता है। Nested objects के लिए, आपको recursion चाहिए:

typescript
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object
    ? T[K] extends Array<infer U>
      ? Array<DeepPartial<U>>
      : DeepPartial<T[K]>
    : T[K];
};
 
interface Settings {
  theme: {
    colors: {
      primary: string;
      secondary: string;
    };
    fonts: {
      heading: string;
      body: string;
    };
  };
  notifications: {
    email: boolean;
    push: boolean;
  };
}
 
type PartialSettings = DeepPartial<Settings>;
// हर nested property अब optional है

Array check पर ध्यान दें: इसके बिना, arrays को objects जैसा treat किया जाएगा और उनके numeric indices optional हो जाएंगे, जो आप नहीं चाहते।

Deep Readonly#

वही pattern, अलग modifier:

typescript
type DeepReadonly<T> = {
  readonly [K in keyof T]: T[K] extends object
    ? T[K] extends Function
      ? T[K]
      : T[K] extends Array<infer U>
        ? ReadonlyArray<DeepReadonly<U>>
        : DeepReadonly<T[K]>
    : T[K];
};

Function check important है — इसके बिना, function properties पर उनकी internal structure पर readonly apply होगा, जिसका कोई मतलब नहीं है।

JSON Type#

यह एक classic recursive type है — किसी भी valid JSON value को represent करना:

typescript
type JsonPrimitive = string | number | boolean | null;
type JsonArray = JsonValue[];
type JsonObject = { [key: string]: JsonValue };
type JsonValue = JsonPrimitive | JsonArray | JsonObject;
 
function parseJson(input: string): JsonValue {
  return JSON.parse(input);
}
 
function isJsonObject(value: JsonValue): value is JsonObject {
  return typeof value === "object" && value !== null && !Array.isArray(value);
}

JSON parsing के लिए any इस्तेमाल करने से यह बहुत बेहतर है। यह consumer को बताता है "आपको कुछ वापस मिलेगा, लेकिन इस्तेमाल करने से पहले आपको इसे narrow करना होगा।" जो बिल्कुल सच है।

Nested Menus के लिए Recursive Type#

एक practical example — navigation menu structure को type करना:

typescript
interface MenuItem {
  label: string;
  href: string;
  icon?: string;
  children?: MenuItem[];
}
 
type Menu = MenuItem[];
 
const navigation: Menu = [
  {
    label: "Products",
    href: "/products",
    children: [
      {
        label: "Software",
        href: "/products/software",
        children: [
          { label: "IDE", href: "/products/software/ide" },
          { label: "CLI Tools", href: "/products/software/cli" },
        ],
      },
      { label: "Hardware", href: "/products/hardware" },
    ],
  },
  { label: "About", href: "/about" },
];

Recursive children property full type safety के साथ infinite nesting देती है।

Real-World Patterns#

कुछ patterns share करता हूं जो मैंने वास्तव में production systems में इस्तेमाल किए हैं।

Builder Pattern with Types#

Builder pattern significantly ज़्यादा useful हो जाता है जब type system track करे कि क्या set किया गया है:

typescript
type RequiredKeys = "host" | "port" | "database";
 
type BuilderState = {
  [K in RequiredKeys]?: true;
};
 
class DatabaseConfigBuilder<State extends BuilderState = {}> {
  private config: Partial<{
    host: string;
    port: number;
    database: string;
    ssl: boolean;
    poolSize: number;
  }> = {};
 
  setHost(host: string): DatabaseConfigBuilder<State & { host: true }> {
    this.config.host = host;
    return this as unknown as DatabaseConfigBuilder<State & { host: true }>;
  }
 
  setPort(port: number): DatabaseConfigBuilder<State & { port: true }> {
    this.config.port = port;
    return this as unknown as DatabaseConfigBuilder<State & { port: true }>;
  }
 
  setDatabase(db: string): DatabaseConfigBuilder<State & { database: true }> {
    this.config.database = db;
    return this as unknown as DatabaseConfigBuilder<State & { database: true }>;
  }
 
  setSsl(ssl: boolean): DatabaseConfigBuilder<State> {
    this.config.ssl = ssl;
    return this as unknown as DatabaseConfigBuilder<State>;
  }
 
  setPoolSize(size: number): DatabaseConfigBuilder<State> {
    this.config.poolSize = size;
    return this as unknown as DatabaseConfigBuilder<State>;
  }
 
  build(
    this: DatabaseConfigBuilder<{ host: true; port: true; database: true }>
  ): Required<Pick<typeof this.config, "host" | "port" | "database">> &
    Partial<Pick<typeof this.config, "ssl" | "poolSize">> {
    return this.config as ReturnType<typeof this.build>;
  }
}
 
// यह compile होता है:
const config = new DatabaseConfigBuilder()
  .setHost("localhost")
  .setPort(5432)
  .setDatabase("myapp")
  .setSsl(true)
  .build();
 
// यह compile time पर fail होता है — .setDatabase() missing है:
// const bad = new DatabaseConfigBuilder()
//   .setHost("localhost")
//   .setPort(5432)
//   .build(); // Error: 'database' missing है

build() method तभी callable होता है जब सभी required fields set हो चुके हों। यह compile time पर पकड़ा जाता है, runtime पर नहीं।

Type-Safe Event Emitter#

यहां एक event emitter है जहां payload types enforce होते हैं:

typescript
type EventMap = Record<string, unknown>;
 
class TypedEventEmitter<Events extends EventMap> {
  private listeners: {
    [K in keyof Events]?: Array<(payload: Events[K]) => void>;
  } = {};
 
  on<K extends keyof Events>(
    event: K,
    listener: (payload: Events[K]) => void
  ): void {
    if (!this.listeners[event]) {
      this.listeners[event] = [];
    }
    this.listeners[event]!.push(listener);
  }
 
  emit<K extends keyof Events>(event: K, payload: Events[K]): void {
    this.listeners[event]?.forEach((listener) => listener(payload));
  }
 
  off<K extends keyof Events>(
    event: K,
    listener: (payload: Events[K]) => void
  ): void {
    const handlers = this.listeners[event];
    if (handlers) {
      this.listeners[event] = handlers.filter((h) => h !== listener) as typeof handlers;
    }
  }
}
 
// Usage
interface AppEvents {
  "user:login": { userId: string; timestamp: number };
  "user:logout": { userId: string };
  "notification:new": { message: string; level: "info" | "warning" | "error" };
}
 
const emitter = new TypedEventEmitter<AppEvents>();
 
// पूरी तरह typed — IDE event names और payload shapes autocomplete करता है
emitter.on("user:login", (payload) => {
  console.log(payload.userId);     // string
  console.log(payload.timestamp);  // number
});
 
// Type error: payload match नहीं करता
// emitter.emit("user:login", { userId: "123" });
// Error: 'timestamp' missing है
 
// Type error: event exist नहीं करता
// emitter.on("user:signup", () => {});
// Error: "user:signup" AppEvents में नहीं है

मैंने तीन अलग-अलग production projects में इस pattern का इस्तेमाल किया है। यह compile time पर event-related bugs की पूरी categories पकड़ता है।

Discriminated Unions with Exhaustive Checking#

Discriminated unions शायद TypeScript में सबसे ज़्यादा useful single pattern हैं। Exhaustive checking के साथ मिलकर, ये guarantee करते हैं कि आप हर case handle करें:

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;
    default:
      // यह line exhaustive checking ensure करती है
      const _exhaustive: never = shape;
      return _exhaustive;
  }
}

अगर कोई नया shape variant add करे (जैसे "pentagon"), यह function compile होने में fail होगा क्योंकि never को value assign नहीं की जा सकती। Compiler आपको हर case handle करने पर मजबूर करता है।

मैं इसे एक helper function के साथ आगे ले जाता हूं:

typescript
function assertNever(value: never, message?: string): never {
  throw new Error(message ?? `Unexpected value: ${JSON.stringify(value)}`);
}
 
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;
    default:
      assertNever(shape, `Unknown shape kind: ${(shape as Shape).kind}`);
  }
}

Type-Safe State Machines#

Discriminated unions state machines को भी खूबसूरती से model करते हैं:

typescript
type ConnectionState =
  | { status: "disconnected" }
  | { status: "connecting"; attempt: number }
  | { status: "connected"; socket: WebSocket; connectedAt: Date }
  | { status: "error"; error: Error; lastAttempt: Date };
 
type ConnectionAction =
  | { type: "CONNECT" }
  | { type: "CONNECTED"; socket: WebSocket }
  | { type: "DISCONNECT" }
  | { type: "ERROR"; error: Error }
  | { type: "RETRY" };
 
function connectionReducer(
  state: ConnectionState,
  action: ConnectionAction
): ConnectionState {
  switch (state.status) {
    case "disconnected":
      if (action.type === "CONNECT") {
        return { status: "connecting", attempt: 1 };
      }
      return state;
 
    case "connecting":
      if (action.type === "CONNECTED") {
        return {
          status: "connected",
          socket: action.socket,
          connectedAt: new Date(),
        };
      }
      if (action.type === "ERROR") {
        return {
          status: "error",
          error: action.error,
          lastAttempt: new Date(),
        };
      }
      return state;
 
    case "connected":
      if (action.type === "DISCONNECT") {
        state.socket.close();
        return { status: "disconnected" };
      }
      if (action.type === "ERROR") {
        return {
          status: "error",
          error: action.error,
          lastAttempt: new Date(),
        };
      }
      return state;
 
    case "error":
      if (action.type === "RETRY") {
        return { status: "connecting", attempt: 1 };
      }
      if (action.type === "DISCONNECT") {
        return { status: "disconnected" };
      }
      return state;
 
    default:
      assertNever(state, "Unknown connection state");
  }
}

हर state में केवल वो properties हैं जो उस state के लिए meaningful हैं। आप disconnected होने पर socket access नहीं कर सकते। Connected होने पर error access नहीं कर सकते। Type system state machine की constraints enforce करता है।

Branded Types for Domain Safety#

एक और pattern जो मुझे essential लगता है — branded types का इस्तेमाल ऐसे values को mix होने से रोकने के लिए जो same underlying type share करते हैं:

typescript
type Brand<T, B extends string> = T & { readonly __brand: B };
 
type UserId = Brand<string, "UserId">;
type OrderId = Brand<string, "OrderId">;
type Email = Brand<string, "Email">;
 
function createUserId(id: string): UserId {
  return id as UserId;
}
 
function createOrderId(id: string): OrderId {
  return id as OrderId;
}
 
function getUser(id: UserId): Promise<User> {
  // ...
}
 
function getOrder(id: OrderId): Promise<Order> {
  // ...
}
 
const userId = createUserId("user_123");
const orderId = createOrderId("order_456");
 
getUser(userId);   // OK
// getUser(orderId); // Error! OrderId UserId को assignable नहीं है

UserId और OrderId दोनों runtime पर strings हैं। लेकिन compile time पर, ये distinct types हैं। आप literally एक order ID वहां pass नहीं कर सकते जहां user ID expected है। यह हर project में real bugs पकड़ चुका है जहां मैंने इसे इस्तेमाल किया है।

Common Pitfalls#

Advanced types powerful हैं, लेकिन इनमें traps हैं। यह वो है जो मैंने कठिन तरीके से सीखा है।

Circular Reference Limits#

TypeScript में types के लिए recursion depth limit है (currently लगभग 50 levels, हालांकि यह vary करता है)। अगर आप बहुत deep जाते हैं, तो आपको dreaded "Type instantiation is excessively deep and possibly infinite" error मिलेगा।

typescript
// यह deeply nested objects के लिए recursion limit hit करेगा
type DeepPartial<T> = {
  [K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
 
// Fix: depth counter add करें
type DeepPartialSafe<T, Depth extends number[] = []> =
  Depth["length"] extends 10
    ? T
    : {
        [K in keyof T]?: T[K] extends object
          ? DeepPartialSafe<T[K], [...Depth, 0]>
          : T[K];
      };

Depth counter trick एक tuple इस्तेमाल करती है जो हर recursive step के साथ बढ़ती है। जब यह आपकी limit hit करती है, recursion रुक जाता है।

Performance Implications#

Complex types आपके editor और build times को significantly slow कर सकते हैं। मैंने ऐसे projects देखे हैं जहां एक overly clever type ने हर keystroke के feedback loop में 3 seconds जोड़ दिए।

Warning signs:

  • IDE autocomplete दिखाने में 2 seconds से ज़्यादा लेता है
  • tsc --noEmit type add करने के बाद noticeably longer लेता है
  • "Type instantiation is excessively deep" errors दिखते हैं
typescript
// यह बहुत ज़्यादा clever है — combinatorial explosion generate करता है
type AllCombinations<T extends string[]> = T extends [
  infer First extends string,
  ...infer Rest extends string[]
]
  ? `${First}${AllCombinations<Rest>}` | AllCombinations<Rest>
  : "";
 
// 5-6 elements से ज़्यादा के साथ यह मत करें

Advanced Types कब इस्तेमाल न करें#

शायद यह सबसे important section है। Advanced types तब इस्तेमाल होने चाहिए जब ये bugs रोकें या developer experience improve करें। दिखावे के लिए नहीं।

इन्हें तब मत इस्तेमाल करें जब:

  • एक simple Record<string, unknown> काफी हो
  • Type केवल एक जगह इस्तेमाल होता है और concrete type ज़्यादा clear हो
  • आप type debug करने में उतना ही time खर्च कर रहे हैं जितना यह बचाएगा
  • आपकी team इसे read या maintain नहीं कर सकती
  • Runtime check ज़्यादा appropriate होगा

इन्हें तब इस्तेमाल करें जब:

  • एक pattern कई types में repeat होता है (mapped types)
  • Function का return type उसके input पर depend करता है (conditional types)
  • आप library API बना रहे हैं जो self-documenting होनी चाहिए
  • आप illegal states को unrepresentable बनाना चाहते हैं
  • आप code reviews में same category के bugs दिखते-दिखते थक गए हैं
typescript
// Over-engineered — simple config के लिए यह मत करें
type Config = DeepReadonly<
  DeepRequired<
    Merge<DefaultConfig, Partial<UserConfig>>
  >
>;
 
// बस यह करें
interface Config {
  readonly host: string;
  readonly port: number;
  readonly debug: boolean;
}

मेरा rule of thumb: अगर type explain करने में उस bug को explain करने से ज़्यादा समय लगता है जो यह रोकता है, तो इसे simplify करें।

Complex Types Debug करना#

जब complex type काम नहीं कर रहा, मैं इस helper से "देखता" हूं कि TypeScript ने क्या resolve किया:

typescript
// IDE hover tooltips में inspection के लिए type expand करता है
type Prettify<T> = {
  [K in keyof T]: T[K];
} & {};
 
// Debug करने के लिए इस्तेमाल करें
type Problematic = SomeComplexType<Input>;
type Debug = Prettify<Problematic>;
// अब IDE में Debug पर hover करें resolved type देखने के लिए

& {} trick TypeScript को type eagerly evaluate करने पर मजबूर करती है type alias दिखाने की बजाय। इसने मुझे घंटों की confusion से बचाया है।

एक और technique — isolate और incrementally test करें:

typescript
// सब एक साथ debug करने की बजाय:
type Final = StepThree<StepTwo<StepOne<Input>>>;
 
// अलग-अलग तोड़ें:
type AfterStep1 = StepOne<Input>;       // check करने के लिए hover करें
type AfterStep2 = StepTwo<AfterStep1>;  // check करने के लिए hover करें
type AfterStep3 = StepThree<AfterStep2>; // check करने के लिए hover करें

TL;DR#

  • Conditional types (T extends U ? X : Y) types के लिए if/else हैं। Unions के साथ distributive behavior से सावधान रहें।
  • infer pattern matching है — function signatures, promises, और arrays जैसी structures से types extract करने के लिए इस्तेमाल करें।
  • Mapped types ({ [K in keyof T]: ... }) properties पर iterate करते हैं। Key remapping और filtering के लिए as के साथ combine करें।
  • Template literal types type level पर strings manipulate करते हैं। Mapped types के साथ combine करने पर, API design के लिए incredibly powerful हैं।
  • Recursive types nested structures handle करते हैं लेकिन compiler explosions से बचने के लिए depth limits चाहिए।
  • Discriminated unions exhaustive checking के साथ TypeScript में सबसे ज़्यादा high-value pattern हैं। हर जगह इस्तेमाल करें।
  • Branded types same underlying type share करने वाले values को mix होने से रोकते हैं। Implement करने में simple, impact में high।
  • Types over-engineer मत करें। अगर type समझना उस bug से ज़्यादा मुश्किल है जो यह रोकता है, simplify करें। Goal आपके codebase को safer बनाना है, type golf competition जीतना नहीं।

TypeScript type system Turing complete है, जिसका मतलब है आप इसके साथ लगभग कुछ भी कर सकते हैं। कला यह जानने में है कि आपको कब करना चाहिए

संबंधित पोस्ट