Skip to main content

What is generic in TypeScript

Generics in TypeScript are type parameters that let you write a function, class, or interface once and reuse it for any type, while TypeScript still enforces full type safety at each call site.

Theory

TL;DR

  • Generics are like a mold: define the shape once (<T>), fill it with any type at use, get full type checking for that type
  • Main difference: any throws away type info; generics pass the exact type through
  • TypeScript infers T automatically in most cases, no explicit <string> needed at call sites
  • Use generics when the same logic applies to 2+ types; use concrete types for single-type code
  • Constraints like T extends object narrow what types are allowed

Quick example

typescript
// Without generics: loses type safety function first(arr: any[]): any { return arr[0]; } const val = first([1, 2, 3]); // val is 'any', no autocomplete, no checks // val.toUpperCase(); // TypeScript silent, but crashes at runtime // With generics: type flows through function first<T>(arr: T[]): T | undefined { return arr[0]; } const num = first([1, 2, 3]); // num is number const str = first(['a', 'b']); // str is string // num.toUpperCase(); // Compile error, TypeScript catches it

T is inferred from the argument. You rarely need to write first<number>([1, 2, 3]) explicitly.

Key difference

The difference between generics and any is not just style. With any, TypeScript stops checking entirely: no autocomplete, no error detection. With generics, TypeScript substitutes the actual type at each call site. Call first([1, 2, 3]) and TypeScript treats every T in that function as number. The type flows through rather than disappears.

When to use

  • Same logic, different types (array operations, data wrappers, API helpers): generics
  • One specific type only: use that concrete type directly
  • Multiple input types, same output shape: generics with constraints (T extends {id: string})
  • Shared utilities across the codebase: generics (callers pick their types)
  • Only 2-3 known variants: overloads can be simpler

How TypeScript handles generics

TypeScript treats T as an abstract placeholder during declaration, then performs type instantiation at each call site. This happens entirely at compile time. The emitted JavaScript has no trace of generics. For T extends string, the compiler narrows possible substitutions before generating JS. No runtime cost, similar to Java type erasure.

Common mistakes

Mistake: mutating generic input assuming any-like freedom

typescript
// Wrong function append<T>(arr: T[]): T[] { arr.push('extra' as any); // Breaks the caller's T[] return arr; } // Right function append<T>(arr: T[], item: T): T[] { return [...arr, item]; // Caller controls the type }

T is fixed at the call site. Pushing a string into a T[] where T is number breaks callers even if TypeScript misses it behind a cast.

Mistake: forgetting constraints on object operations

typescript
// Wrong, T may be a primitive and spread fails function merge<T>(a: T, b: T) { return { ...a, ...b }; } // Right function merge<T extends object>(a: T, b: T) { return { ...a, ...b }; }

Mistake: no arguments to drive inference

typescript
// Wrong, T inferred as never[] function createArray<T>(): T[] { return []; } const arr = createArray(); // never[] // Right, parameter drives inference function createArray<T>(size: number, fill: T): T[] { return Array(size).fill(fill); } const nums = createArray(3, 0); // number[]

Real-world usage

  • React: useRef<HTMLDivElement>() returns RefObject<HTMLDivElement>, exact DOM type
  • React state: useState<User | null>(null) is typed from the start, not any
  • TanStack Query: useQuery<User[]> propagates types through every selector
  • Lodash types: _.map<T, R>(arr: T[], fn: (x: T) => R): R[] links input and output types
  • Express: Request<P, ResBody, ReqBody> for typed route handlers

Follow-up questions

Q: What is the difference between T extends object and T extends {}?
A: T extends {} matches everything except null and undefined, including primitives. T extends object also excludes primitives. For spread operations you want T extends object.

Q: Write a function that picks properties from an object by key array.
A:

typescript
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> { return keys.reduce((acc, k) => ({ ...acc, [k]: obj[k] }), {} as Pick<T, K>); }

K extends keyof T ensures only valid keys are passed. TypeScript catches pick(user, ['salary']) if salary is not on the type.

Q: How are TypeScript generics different from Java generics at runtime?
A: TypeScript generics are fully erased at compile time, the JS output has no type info at all. Java has reified generics in some cases, allowing runtime checks like instanceof. That is not possible in TypeScript.

Q: What does infer do inside conditional types?
A:

typescript
type UnpackPromise<T> = T extends Promise<infer U> ? U : T; // UnpackPromise<Promise<string>> = string // UnpackPromise<number> = number

infer U tells TypeScript to extract the inner type from Promise<...> and bind it to U for the true branch. This is how utility types like Awaited<T> are built.

Q: Junior vs senior on bounded generics: constrain to only strings and numbers.
A: Junior writes T extends string | number and that works. A senior knows the limits: if you need different behavior per type, overloads or conditional types (T extends string ? string : number) give you more control.

Examples

Basic: identity function with type inference

typescript
function identity<T>(arg: T): T { return arg; } const num = identity(42); // T inferred as number const str = identity('hello'); // T inferred as string const obj = identity({ id: 1 }); // T inferred as { id: number } // num.toUpperCase(); // Compile error, TypeScript knows it's number console.log(num.toFixed(2)); // "42.00"

TypeScript infers T from the argument. Writing identity<number>(42) works but is not required.

Intermediate: typed property access with two type parameters

typescript
function getProp<T, K extends keyof T>(obj: T, key: K): T[K] { return obj[key]; } interface Person { name: string; age: number; } const person: Person = { name: 'Alice', age: 30 }; console.log(getProp(person, 'name')); // "Alice", return type is string console.log(getProp(person, 'age')); // 30, return type is number // getProp(person, 'salary'); // Compile error: 'salary' is not in Person

Two type parameters working together: T is the object, K is constrained to keys of T. The return type T[K] is the exact property type. No casting, no any.

Advanced: generic data fetching hook

typescript
// Pattern common in TanStack Query and custom fetch wrappers function useData<T>(url: string): { data: T | null; loading: boolean } { const [data, setData] = useState<T | null>(null); const [loading, setLoading] = useState(true); useEffect(() => { fetch(url) .then(res => res.json()) .then(setData) .finally(() => setLoading(false)); }, [url]); return { data, loading }; } interface User { id: number; name: string; } // T = User[], so data is User[] | null const { data: users, loading } = useData<User[]>('/api/users'); if (users) { console.log(users[0].name); // Safe, TypeScript knows the shape }

The hook has no idea about User. The caller decides the type at use. Every project I have worked on with a custom fetch wrapper eventually reached this pattern: add one <T> and the whole component tree gets typed automatically.

Short Answer

Interview ready
Premium

A concise answer to help you respond confidently on this topic during an interview.

Finished reading?