Skip to content
Advanced10 min study

Generics

Preserve relationships between types using inference, constraints, keyof, indexed access, mapped types, and conditional types.

Question progress0 / 10 completed
Start the lesson
Generics visual explanation

WHAT YOU WILL BE ABLE TO DO

Learning outcomes

  • Explain generics in plain language.
  • Connect the behavior to the underlying browser or framework model.
  • Implement the core pattern and reason through edge cases.
  • Answer common follow-ups without relying on memorized phrases.

01Overview

A generic is a type parameter that lets a function, type, or class stay reusable across many concrete types while preserving the relationship between its inputs and outputs — unlike any, which discards that relationship entirely.

02Mental model

extends constrains what a type parameter must satisfy, and a default (= SomeType) supplies a fallback when the caller doesn't specify one.

03Examples

TypeScript
function pluck<T, K extends keyof T>(obj: T, key: K): T[K] {  return obj[key];}
const user = { id: 1, name: 'Ada' };const name = pluck(user, 'name'); // inferred as stringpluck(user, 'missing'); // compile error — not a key of user

04Check understanding

Why does pluck(user, 'name') return type string instead of just unknown?

K extends keyof T constrains K to the object's actual keys, so T[K] resolves to the specific property type at that key — the compiler tracks the relationship instead of widening it away.

DDConcept deep dives

Deep dive 1

A type parameter records a relationship

Generics are valuable when the type of one input determines another input, the output, or a member of a returned structure. identity<T>(value: T): T preserves exactly what the caller supplied. Replacing T with unknown would accept the input but lose the relationship on return.

  • A parameter used only once may not express a useful relationship.
  • Let inference determine type arguments when call-site evidence is clear.
  • Choose descriptive type parameter names when several domain types interact.

Deep dive 2

Constraints state required capabilities

An unconstrained T could be any type, so its properties cannot be assumed. T extends Constraint says the implementation requires those members while preserving the caller's additional information. keyof and indexed access types can then relate a selected key K to its exact value type T[K].

  • Constrain only the operations the implementation performs.
  • A constraint does not perform runtime validation.
  • Returning the constraint instead of T can accidentally discard caller-specific information.
TypeScript
function get<T, K extends keyof T>(object: T, key: K): T[K] {  return object[key];}
const user = { id: 7, name: 'Ada' };const name = get(user, 'name'); // string

K can only be a known key of T, and the return type stays correlated with the selected key instead of widening to string | number.

Deep dive 3

Mapped and conditional types transform type structure

Mapped types iterate over property keys to create related object types, while conditional types select a result based on assignability. A conditional over a naked type parameter distributes across union members, which is powerful for filtering unions but surprising when whole-union comparison was intended.

  • Use key remapping to rename or filter mapped properties.
  • Wrap both sides in a tuple to suppress distributive conditional behavior.
  • Prefer readable named helpers over deeply nested type expressions that obscure API errors.

QAInterview questions and model answers

Attempt each answer aloud before opening it. The model answer shows the depth and precision expected in an interview; it is not a script to memorize.

Intermediate · Coding · 1 min · Question 1What problem do generics solve?Open model answer

Model answer

Generics express relationships between types while preserving information. An identity function using T returns the same type it receives; using unknown would lose that relationship and require narrowing or assertion at the call site.

TypeScript
function first<T>(arr: T[]): T | undefined {  return arr[0];}
first([1, 2, 3]);        // number | undefinedfirst(['a', 'b']);       // string | undefined  — relationship preserved
Open question page →
Intermediate · Coding · 1 min · Question 2What is a generic constraint?Open model answer

Model answer

A constraint such as T extends { id: string } limits accepted types to those with required capabilities while retaining the caller's more specific type. It should state only what the implementation actually needs.

TypeScript
function byId<T extends { id: string }>(items: T[], id: string) {  return items.find((item) => item.id === id); // .id is guaranteed}
Open question page →
Intermediate · Coding · 1 min · Question 3How do keyof and indexed access types work together?Open model answer

Model answer

keyof T produces the known property keys of T, and T[K] describes the value type at key K. A get function using K extends keyof T can return the precise property type rather than a broad union.

TypeScript
function get<T, K extends keyof T>(obj: T, key: K): T[K] {  return obj[key];}
const u = { id: 1, name: 'Ada' };get(u, 'name'); // string  (not string | number)get(u, 'age');  // Error — not a key of u
Open question page →
Intermediate · Coding · 1 min · Question 4When is a generic type parameter unnecessary?Open model answer

Model answer

If it appears only once and does not connect inputs, outputs, or members, it may add ceremony without information. Use the concrete constraint type or unknown instead. Type parameters should model a real relationship.

TypeScript
// Pointless generic — T is used once, connects nothingfunction log<T>(x: T): void { console.log(x); }// Just: function log(x: unknown): void
Open question page →
Advanced · Coding · 1 min · Question 5What are conditional types?Open model answer

Model answer

A conditional type selects one type or another based on assignability. When the checked type is a naked type parameter, it distributes over unions. Wrapping both sides in tuples can suppress distribution.

TypeScript
type Unwrap<T> = T extends Promise<infer U> ? U : T;type A = Unwrap<Promise<string>>; // stringtype B = Unwrap<number>;          // number
type NonNull<T> = T extends null | undefined ? never : T;type C = NonNull<string | null>; // string  (distributes over the union)
Open question page →
Intermediate · Conceptual · 1 min · Question 6Why can a generic function still be unsafe internally?Open model answer

Model answer

TypeScript checks the declared relationships, but assertions, any, unchecked indexed access, and inaccurate constraints can violate them. Generic syntax does not replace runtime validation for external data.

Open question page →
Beginner · Conceptual · 1 min · Question 7What is the difference between T[] and Array<T>?Open model answer

Model answer

They normally describe the same mutable array relationship, with Array<T> often easier to compose in complex generic syntax and T[] usually more concise.

Open question page →
Advanced · Conceptual · 1 min · Question 8Where should a type parameter live on an interface?Open model answer

Model answer

Put it on the interface when all members share one chosen type, or on a call signature when each invocation should infer an independent type.

Open question page →
Advanced · Conceptual · 1 min · Question 9What is variance?Open model answer

Model answer

Variance describes how assignability between generic instantiations follows relationships between their type arguments. Producers tend toward covariance and consumers toward contravariance under sound checking.

Open question page →
Advanced · Conceptual · 1 min · Question 10What does infer do in a conditional type?Open model answer

Model answer

It introduces a type variable inferred from the matched structure, allowing helpers to extract parts such as a function's return value or an array element type.

Open question page →

SCScenario questions

Scenario 1

Design a table column API where each column key and formatter value match the row type.

  1. Make the row type the outer generic.
  2. Represent each column as a union over keyof Row.
  3. Tie a column's key K to a formatter receiving Row[K].
  4. Avoid collapsing the formatter parameter to a union of every field type.
Reveal worked answer

I would use a mapped union: for each K in keyof Row, create { key: K; format?: (value: Row[K], row: Row) => ReactNode }, then index that mapped type by keyof Row. This preserves the correlation between a selected key and its formatter value.

Verify and go deeper