A Generic Is a Type Parameter — Nothing More
Generics look intimidating because of the angle brackets, but the idea is one you already use every day. A function takes a value as a parameter so it can work with many values. A generic takes a type as a parameter so it can work with many types. That is the whole concept.
The problem they solve is concrete. Suppose you want a function that returns the first item of an array. Typed as (items: any[]) => any, it works with every array and tells the caller nothing — the result is any, so all checking downstream is gone. Typed as (items: string[]) => string, it is safe but only works with strings, and you end up writing the same function five times.
A generic connects the input to the output. function first<T>(items: T[]): T | undefined says: whatever type the array holds, that is the type you get back. Pass a string[] and the result is a string; pass a Student[] and the result is a Student. One implementation, full type information preserved.
The letter T is only a convention, short for Type. You can name it anything, and for anything beyond a trivial helper you should — TItem, TResponse and TKey read far better in a signature with three parameters than T, U and V.
// Loses everything
function firstAny(items: any[]): any {
return items[0];
}
const a = firstAny(["x", "y"]); // any — no checking from here on
// Works for one type only
function firstString(items: string[]): string | undefined {
return items[0];
}
// Generic: one function, type preserved
function first<T>(items: T[]): T | undefined {
return items[0];
}
const name = first(["Ananya", "Rahul"]); // string | undefined
const mark = first([88, 74]); // number | undefined
// A second type parameter for a second relationship
function pair<TKey, TValue>(key: TKey, value: TValue): [TKey, TValue] {
return [key, value];
}
const entry = pair("marks", 88); // [string, number] - Read
<T>aloud as "for some type T".first<T>(items: T[]): T | undefinedbecomes "for some type T, this takes an array of T and returns a T or nothing", which is exactly what it means.
Inference: You Rarely Write the Angle Brackets
You can supply the type argument explicitly, as in first<string>(names), but you almost never need to. TypeScript infers it from the arguments you pass, exactly as it infers ordinary variable types from their values. In practice, calling a generic function looks identical to calling a normal one.
Inference occasionally needs help, and it is worth knowing the two cases. The first is when nothing in the arguments determines the type — a function that only returns a T, with no T in its parameters, has nothing to infer from, and without an explicit type argument the parameter falls back to its constraint or to unknown. The second is when you want a wider or narrower type than the one inferred, for instance a state variable that starts empty but will hold objects later.
The reverse problem also exists: sometimes inference gives you a type that is too specific or too general for what you meant. Hovering over the call in your editor to read the inferred type argument is the quickest way to see what happened before you start guessing.
function first<T>(items: T[]): T | undefined {
return items[0];
}
first([1, 2, 3]); // T inferred as number
first<string>(["a", "b"]); // explicit — same result, more typing
// React's useState is a generic function you already use
const [count, setCount] = useState(0); // T inferred as number
const [user, setUser] = useState<Student | null>(null);
// Without the explicit argument, T would be inferred as null
// and setUser could never be given a Student.
// Nothing to infer from: the type argument must be supplied
function makeEmpty<T>(): T[] {
return [];
}
const names = makeEmpty<string>(); // string[]
const guess = makeEmpty(); // unknown[] Constraints: Requiring Something of T
An unconstrained T could be anything, so inside the function you may not assume anything about it. Try to read item.length from a value of type T and TypeScript refuses, correctly — T might be a number.
A constraint fixes that. <T extends { length: number }> means "T can be any type, as long as it has a numeric length". Inside the function you may now use length, and at the call site anything without one is rejected. Notice that extends here means "is assignable to", not class inheritance — strings, arrays and your own objects all satisfy that constraint because of structural typing.
The most useful constraint in practice is K extends keyof T, which ties one type parameter to another. It lets you write a function that accepts an object and one of its own property names, returns the type of that exact property, and rejects any name that is not a real key. This is how typed helpers for sorting, grouping and picking fields are written, and it is worth reproducing from memory until it feels natural.
Type parameters can also have defaults, written <T = string>, which are used when neither an explicit argument nor inference supplies one.
// Unconstrained: nothing can be assumed about T
function lengthOf<T>(item: T): number {
// return item.length;
// ERROR: Property 'length' does not exist on type 'T'.
return 0;
}
// Constrained: now length is available and enforced
function sizeOf<T extends { length: number }>(item: T): number {
return item.length;
}
sizeOf("Priodemy"); // 8
sizeOf([1, 2, 3]); // 3
sizeOf(42); // ERROR: number does not have 'length'
// Two parameters, related to each other
function getField<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const student = { id: 1, name: "Meera", marks: 91 };
const n = getField(student, "name"); // string
const m = getField(student, "marks"); // number
getField(student, "grade");
// ERROR: Argument of type '"grade"' is not assignable to parameter of type
// 'id' | 'name' | 'marks'.
// Constrained collection helper you will write often
function findById<T extends { id: number }>(items: T[], id: number): T | undefined {
return items.find(item => item.id === id);
} - Constrain as loosely as the function actually needs. Requiring
T extends Studentwhen the body only readsidmakes the helper useless for orders and products —T extends { id: number }works for all three.
Generic Types, Interfaces and Classes
Generics are not limited to functions. An interface or type alias can take type parameters, which is how you describe a container whose contents vary: an API envelope, a paginated list, a cache, a result that is either a value or an error.
This is where generics stop being an exercise and start saving real work. Every endpoint in an application returns the same envelope with a different payload. Without generics you write that envelope once per endpoint and keep twenty copies in step. With one ApiResponse<T>, adding a field to the envelope updates every endpoint at once.
Classes take type parameters in the same way, and the parameter is available throughout the class body. A typed store, a queue, an event emitter — anything that holds values of a type decided by whoever creates it — is naturally a generic class. You will also see generic types used with the built-in collections you already know: Map<string, Student> and Set<number> are generic interfaces from TypeScript's own library, and Promise<T> is the one you use most of all.
// A generic envelope, used by every endpoint
interface ApiResponse<T> {
data: T;
status: number;
requestId: string;
}
type StudentResponse = ApiResponse<Student>;
type CourseListResponse = ApiResponse<{ slug: string; title: string }[]>;
// Generic type alias with a default parameter
type Paged<T, TMeta = { page: number; total: number }> = {
items: T[];
meta: TMeta;
};
// A generic class: the type is chosen by whoever constructs it
class Store<T extends { id: number }> {
private items = new Map<number, T>();
add(item: T): void {
this.items.set(item.id, item);
}
get(id: number): T | undefined {
return this.items.get(id);
}
all(): T[] {
return [...this.items.values()];
}
}
const students = new Store<Student>();
students.add({ id: 1, name: "Ananya", marks: 88 });
const found = students.get(1); // Student | undefined When Not to Use a Generic
Generics are the feature people most enjoy over-using once it clicks, and over-used generics make code harder to read for no benefit. There is a simple test that catches most of the mistakes: a type parameter must appear at least twice in the signature.
The point of a type parameter is to express a relationship — between two parameters, or between a parameter and the return type. If T appears only once, there is no relationship to express, and the generic is doing nothing that a plain type would not do better. Replace it with unknown and a constraint, and the signature becomes both simpler and more honest.
The second common mistake is reaching for a generic when a union would do. If your function only ever handles two known types, string | number is clearer than T extends string | number, easier to read at the call site, and gives better error messages.
The third is generic-by-default: adding type parameters to a helper that has exactly one caller, in case it might be reused later. Write the concrete version. Make it generic on the day you actually need the second type — the change is small, and until then the simpler signature is worth more.
// Pointless: T appears once, so nothing is related to anything
function logIt<T>(value: T): void {
console.log(value);
}
// Simpler and equally safe
function logItBetter(value: unknown): void {
console.log(value);
}
// Pointless: the constraint says everything, T adds nothing
function nameOf<T extends { name: string }>(item: T): string {
return item.name;
}
// Simpler
function nameOfBetter(item: { name: string }): string {
return item.name;
}
// A real generic: T ties the input to the output
function sortBy<T, K extends keyof T>(items: T[], key: K): T[] {
return [...items].sort((a, b) => (a[key] < b[key] ? -1 : 1));
} - Before adding a type parameter, ask what it connects. If you cannot answer in one sentence, the generic is decoration.
The Generic That Lies
One generic appears in nearly every codebase, and it is worth examining closely because it looks like the responsible thing to do while being the opposite. It is the typed fetch helper: function fetchJson<T>(url: string): Promise<T>.
Look at where T comes from. It is not inferred from any argument — url is a string, and a string tells you nothing about the response. The caller writes fetchJson<Student>("/api/students/1") and simply declares what will come back. Inside, res.json() produces any, which is quietly accepted as T. No checking happens anywhere. The generic is a type assertion wearing a nicer suit.
This matters because such a helper spreads confidence through a codebase that has not earned it. Every call site looks fully typed, your editor autocompletes every field, and the first response with a renamed field crashes somewhere far from the fetch. Remember: a type parameter with no runtime component cannot check anything, because nothing in TypeScript checks anything at runtime.
The fix is not to abandon the helper but to make it require proof. Take a validator alongside the URL, run it on the parsed body, and return the narrowed value. The signature is barely longer, T is now genuinely inferred from the guard, and the promise the function makes is one it can keep.
// Looks safe. Checks nothing.
async function fetchJson<T>(url: string): Promise<T> {
const res = await fetch(url);
return res.json(); // any, silently accepted as T
}
const student = await fetchJson<Student>("/api/students/1");
student.name.toUpperCase(); // crashes if the API changed
// Honest version: T is inferred from a guard that actually runs
async function fetchChecked<T>(
url: string,
guard: (value: unknown) => value is T
): Promise<T> {
const res = await fetch(url);
const data: unknown = await res.json();
if (!guard(data)) {
throw new Error(`Unexpected response shape from ${url}`);
}
return data;
}
const safe = await fetchChecked("/api/students/1", isStudent); // Student - This is the same lesson as the very first one, arriving from a different direction. Generics move type information around; they never create it. If the information was a guess at the boundary, it is still a guess ten layers deep.
