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:
anythrows away type info; generics pass the exact type through - TypeScript infers
Tautomatically 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 objectnarrow what types are allowed
Quick example
// 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 itT 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
// 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
// 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
// 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>()returnsRefObject<HTMLDivElement>, exact DOM type - React state:
useState<User | null>(null)is typed from the start, notany - 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:
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:
type UnpackPromise<T> = T extends Promise<infer U> ? U : T;
// UnpackPromise<Promise<string>> = string
// UnpackPromise<number> = numberinfer 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
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
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 PersonTwo 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
// 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 readyA concise answer to help you respond confidently on this topic during an interview.