Fossils🧠 ConceptualTypeScript Generics Explained
ðŸĶ–DinosaurTypeScriptGenericsInterview

TypeScript Generics Explained

Generics separate TypeScript beginners from experts. Your answer shows whether you use the type system or fight it.

TypeScript Generics Explained

Interview Question: "Explain TypeScript generics and when you'd use conditional types."

The Generics Answer

"Generics let you write functions, classes, and types that work with any type while preserving type safety. They're type-level parameters — just like function parameters let you pass different values, generics let you pass different types."

Basic Generic Function

function identity<T>(value: T): T {
  return value;
}
 
const str = identity("hello"); // type: string
const num = identity(42);       // type: number

"Without generics, you'd either lose type information (using any) or write separate functions for each type. Generics give you reusability without sacrificing safety."

Generic Constraints

interface HasLength {
  length: number;
}
 
function logLength<T extends HasLength>(value: T): T {
  console.log(value.length);
  return value;
}
 
logLength("hello");    // OK — string has .length
logLength([1, 2, 3]);  // OK — array has .length
logLength(42);         // Error — number has no .length

"Constraints narrow what a generic accepts. T extends HasLength means T must have a length property. You get autocomplete and type checking while keeping it generic."

Real-World: Type-Safe API Client

interface ApiEndpoints {
  "/users": { response: User[]; params: { role?: string } };
  "/users/:id": { response: User; params: { id: string } };
  "/posts": { response: Post[]; params: { limit?: number } };
}
 
async function api<E extends keyof ApiEndpoints>(
  endpoint: E,
  params?: ApiEndpoints[E]["params"]
): Promise<ApiEndpoints[E]["response"]> {
  const response = await fetch(buildUrl(endpoint, params));
  return response.json();
}
 
const users = await api("/users", { role: "admin" }); // type: User[]
const post = await api("/posts", { limit: 10 });       // type: Post[]
await api("/users", { limit: 10 }); // Error — limit doesn't exist on /users params

Generic React Components

interface ListProps<T> {
  items: T[];
  renderItem: (item: T) => React.ReactNode;
  keyExtractor: (item: T) => string;
}
 
function List<T>({ items, renderItem, keyExtractor }: ListProps<T>) {
  return (
    <ul>
      {items.map((item) => (
        <li key={keyExtractor(item)}>{renderItem(item)}</li>
      ))}
    </ul>
  );
}
 
// Usage — T is inferred as User
<List
  items={users}
  renderItem={(user) => <span>{user.name}</span>}
  keyExtractor={(user) => user.id}
/>

Conditional Types

"Conditional types are the if/else of the type system. They let types compute other types based on conditions."

Basic Syntax

type IsString<T> = T extends string ? "yes" : "no";
 
type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"

infer — Extracting Types from Patterns

type ReturnTypeOf<T> = T extends (...args: any[]) => infer R ? R : never;
 
type A = ReturnTypeOf<() => string>;           // string
type B = ReturnTypeOf<(x: number) => boolean>; // boolean
 
type UnwrapPromise<T> = T extends Promise<infer U> ? U : T;
 
type C = UnwrapPromise<Promise<string>>; // string
type D = UnwrapPromise<number>;          // number

"infer is pattern matching for types. It 'captures' a type from a structural position. infer R in a function return position extracts the return type."

Distributive Conditional Types

type ToArray<T> = T extends any ? T[] : never;
 
type Result = ToArray<string | number>;
// Distributes: string[] | number[] (NOT (string | number)[])

"When a conditional type acts on a union, it distributes — applies independently to each union member. This is usually what you want but can be surprising."

Mapped Types

type Readonly<T> = { readonly [K in keyof T]: T[K] };
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
 
type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K];
};
 
interface User {
  id: number;
  name: string;
  email: string;
  age: number;
}
 
type StringFields = PickByType<User, string>;
// { name: string; email: string }

Discriminated Unions

type Result<T> =
  | { status: "success"; data: T }
  | { status: "error"; error: Error }
  | { status: "loading" };
 
function handleResult<T>(result: Result<T>) {
  switch (result.status) {
    case "success":
      console.log(result.data);  // TypeScript knows data exists here
      break;
    case "error":
      console.log(result.error); // TypeScript knows error exists here
      break;
    case "loading":
      break; // No data or error available
  }
}

"Discriminated unions combined with generics are how you model state machines in TypeScript. The status field acts as the discriminant — TypeScript narrows the type in each branch automatically."

What Interviewers Look For

  • Explains generics as type parameters — not just "makes things reusable"
  • Shows constraints — extends for narrowing generic types
  • Real-world examples — API clients, React components, state machines
  • Understands infer — pattern matching for types
  • Knows when NOT to use generics — over-generic code is worse than duplicated code

Common Mistakes

  • Over-generic code — making everything generic when concrete types are fine
  • Using any as an escape hatch — defeats the purpose of TypeScript
  • Generic naming conventions — T for type, K for key, V for value, E for element. Don't use T for everything in multi-generic signatures
  • Forgetting distribution — conditional types on unions distribute by default
  • Not using constraints — a bare <T> gives you no useful properties to work with

Follow-Up Questions

"How do you avoid over-engineering with generics?"

"I follow the rule of three — don't make something generic until you have three concrete use cases. Start with specific types, then extract a generic when the pattern repeats. Over-generic code is harder to read, harder to debug, and the type errors are incomprehensible."

"What's the difference between unknown and any?"

"any disables type checking — anything goes. unknown is type-safe — you must narrow it before using it. Use unknown for values from external boundaries (API responses, user input) and narrow with type guards."

"When would you use a mapped type vs a conditional type?"

"Mapped types transform the shape of an object type — adding/removing modifiers, filtering keys, remapping values. Conditional types branch based on type relationships. Often they're combined: a mapped type with a conditional type in the value position to transform different properties differently."

Red Flags

  • Saying generics are "like templates in C++" without explaining the structural typing difference
  • Using any instead of generics to make code flexible
  • Cannot explain infer or give an example
  • No awareness of discriminated unions for state modeling
  • Treating TypeScript as "JavaScript with type annotations" rather than leveraging the type system