Volume 4

Q205: Generics, Constraints and Reusable Type APIs

Difficulty: SeniorFrequency: HighAnswer time: 8-12 minutes

What Interviewers Want To Evaluate

Interviewers want to know whether you can design reusable TypeScript APIs without making them hard to read.

They evaluate generic functions, constraints, defaults, inference, key-based helpers, reusable component props, utility types, and when generics become unnecessary abstraction.

Short Interview Answer

Generics let a function, component, or type preserve relationships between inputs and outputs. I use them when the caller should keep specific type information, such as mapping arrays, selecting object keys, or building reusable components. I add constraints with extends when the implementation needs certain properties, and I avoid generics when a concrete type or union communicates the design better.

Detailed Interview Answer

Generics are type parameters.

They let code stay reusable while preserving specific caller information.

function identity<T>(value: T): T {
  return value;
}

const name = identity("Krishna");
const count = identity(3);

The return type matches the input type.

That relationship is the point.

Generic Array Helper

A common example:

function first<T>(items: T[]): T | undefined {
  return items[0];
}

Callers keep their item type.

const user = first([{ id: "u1", name: "Ada" }]);

The result is inferred as:

{ id: string; name: string } | undefined

No manual type argument is required.

Multiple Type Parameters

Use multiple parameters when there is a real relationship.

function mapItems<TInput, TOutput>(
  items: TInput[],
  mapper: (item: TInput) => TOutput,
): TOutput[] {
  return items.map(mapper);
}

This captures input and output separately.

const lengths = mapItems(["one", "three"], (item) => item.length);

lengths becomes number[].

Constraints

Sometimes generic code needs a property.

function sortByName<T>(items: T[]) {
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
}

This fails because T could be anything.

Add a constraint.

function sortByName<T extends { name: string }>(items: T[]): T[] {
  return [...items].sort((a, b) => a.name.localeCompare(b.name));
}

Now the function accepts any item with a name, and it returns the same item shape.

keyof Constraints

Key-based APIs are common in frontend utilities.

function pick<TObject, TKey extends keyof TObject>(
  object: TObject,
  key: TKey,
): TObject[TKey] {
  return object[key];
}

Usage:

const user = {
  id: "u1",
  name: "Ada",
  age: 32,
};

const age = pick(user, "age");

age is inferred as number.

Invalid keys are rejected.

pick(user, "missing");

The compiler protects the relationship between object and key.

Generic Defaults

Defaults can reduce call-site noise.

type ApiResult<TData, TError = string> =
  | { ok: true; data: TData }
  | { ok: false; error: TError };

Most callers use the default error type.

type UserResult = ApiResult<{ id: string; name: string }>;

Special callers can override it.

type PaymentResult = ApiResult<Payment, { code: string; message: string }>;

Generic React Components

Generics can work well for reusable UI.

type SelectProps<TOption> = {
  options: TOption[];
  getLabel: (option: TOption) => string;
  onChange: (option: TOption) => void;
};

function Select<TOption>({ options, getLabel, onChange }: SelectProps<TOption>) {
  return (
    <ul>
      {options.map((option, index) => (
        <li key={index}>
          <button onClick={() => onChange(option)}>{getLabel(option)}</button>
        </li>
      ))}
    </ul>
  );
}

The component preserves the option type for callbacks.

That is useful when the caller has rich domain objects.

Generic Hooks

Hooks often use generics for data.

type AsyncState<TData> =
  | { status: "loading" }
  | { status: "success"; data: TData }
  | { status: "error"; message: string };

function useAsyncValue<TData>(loader: () => Promise<TData>): AsyncState<TData> {
  throw new Error("implementation omitted");
}

The hook can represent many data types while preserving loaded data shape.

Utility Types

TypeScript includes reusable generic utility types.

Examples:

type User = {
  id: string;
  name: string;
  email: string;
};

type UserDraft = Partial<User>;
type UserPreview = Pick<User, "id" | "name">;
type UserWithoutEmail = Omit<User, "email">;
type ReadonlyUser = Readonly<User>;

These are useful when they describe real data transformations.

Do not stack utility types until the result becomes unreadable.

Conditional Generic APIs

Advanced generics can become difficult quickly.

type MaybeArray<T> = T | T[];

function toArray<T>(value: MaybeArray<T>): T[] {
  return Array.isArray(value) ? value : [value];
}

This is simple and useful.

But deeply nested conditional types can hurt readability and compiler performance.

Senior engineers use advanced type programming only when it pays for itself.

Avoid Useless Generics

This generic is not useful:

function log<T>(message: string): void {
  console.log(message);
}

T is never used in a relationship.

A generic should connect at least two things:

input to output
object to key
props to callback
state to action
data to renderer

If it does not preserve a relationship, it may be noise.

Naming Generic Parameters

Single-letter names are fine for small utilities.

function first<T>(items: T[]): T | undefined {}

For complex APIs, descriptive names help.

type DataTableProps<TRow, TSortKey extends keyof TRow> = {
  rows: TRow[];
  sortKey: TSortKey;
  renderCell: (row: TRow, key: TSortKey) => React.ReactNode;
};

Names should make relationships easier to see.

Common Mistakes

Common mistakes include:

adding generics that do not relate values
using any inside generic helpers
over-constraining types too early
forcing callers to pass type arguments unnecessarily
building unreadable conditional type machinery
using index as key in real dynamic lists

The purpose is reusable safety, not type cleverness.

Senior Trade-Offs

Generics are valuable when they keep caller-specific information.

They are costly when they make APIs abstract before the product needs it.

I would use generics for:

data tables
select controls
form helpers
API result wrappers
cache helpers
typed events
utility functions

I would avoid them for simple components with fixed domain props.

Interview Framing

In an interview, say:

I use generics when an API needs to preserve a relationship between values. Constraints describe the minimum shape the implementation needs, while inference should keep call sites clean. If the generic does not protect a real relationship, I remove it.

That answer sounds practical and senior.

Revision Notes

  • Generics preserve type relationships.
  • Constraints let implementation require a minimum shape.
  • keyof connects object keys to object values.
  • Generic defaults reduce repeated type arguments.
  • Utility types are generic building blocks.
  • Avoid generics that do not connect inputs and outputs.

Follow-Up Questions

  • What problem do generics solve?
  • When should you add an extends constraint?
  • How does keyof help reusable APIs?
  • What makes a generic useless?
  • How would you type a reusable Select component?

How I Would Answer This In A Real Interview

I would say generics are for preserving relationships. If a function accepts an array and returns one item, or accepts an object and key and returns the matching value, generics let TypeScript keep the caller's exact information. I use constraints when the implementation needs a property, defaults to reduce call-site noise, and I avoid abstract type programming unless it improves real API safety or reuse.