Understanding TypeScript Generics: What Does T Mean?
"My Brain Froze When I Saw <T>"
Learning TypeScript was fine until functions.
Then came the angle brackets <T>, turning code into alien hieroglyphs.
function echo<T>(arg: T): T {
return arg;
}
"Can't I just use any? Why complicate things?"
I labeled Generics as "only for wizards" and avoided them.
But libraries and real-world code were full of them. I had to face it.
What Confused Me Initially? (The Fear of Ambiguity)
I thought "Typing" meant "Being Explicit."
string, number. That felt safe.
Generics said, "We don't know what's coming, but it's not any."
"If you don't know, how can you check it? Isn't that just any?"
I couldn't accept this contradiction.
The 'Aha!' Moment (Transparent Sticker Analogy)
I understood it as "Vending Machine & Transparent Sticker."
any: A Black Box. You don't see what's inside. You put in an apple, but pull out a brick. (Dangerous)- Generic (
<T>): A Transparent Bag. You don't know what goes in yet, but "Whatever you put in, you clearly see as is." Put in an apple, you see a red apple.
// any: Input is free, but Output is unknown (Risky)
function anyBox(item: any): any {
return item;
}
const box1 = anyBox(10); // box1 is 'any'.
// Generic: Type is captured at input moment (Safe)
function genericBox<T>(item: T): T {
return item;
}
const box2 = genericBox(10); // box2 is 'number'! (TS inferred it)
Generics are "Passing types as arguments variables at usage time." It's just a "Type Variable."
The Fix: 3 Steps to Master Generics
Step 1: Functions (API Calls)
Most common use case: API Fetch wrapper. The function doesn't know the response shape, but the caller does.
async function fetchJson<T>(url: string): Promise<T> {
const response = await fetch(url);
return response.json();
}
// 'Inject' Type T when using it
interface User { name: string }
const user = await fetchJson<User>('/api/user');
// Now user.name autocompletes!
Step 2: Constraints (extends)
Sometimes T is too broad.
"I need at least a length property!"
// T MUST extend { length: number } (like string, array)
function logLength<T extends { length: number }>(arg: T) {
console.log(arg.length);
}
logLength("hello"); // OK (string has length)
logLength([1, 2]); // OK (array has length)
logLength(10); // Error! (number has no length)
extends is the key. It guarantees Safe Freedom.
Step 3: React Components (Select Box)
Generics are essential for reusable components.
interface SelectProps<T> {
options: T[];
value: T;
onChange: (value: T) => void;
}
// <T,> : Comma prevents arrow function JSX syntax confusion
const Select = <T,>({ options, value, onChange }: SelectProps<T>) => {
return ( ... );
};
// Usage: String Select
<Select<string> options={["A", "B"]} value="A" ... />
// Usage: Number Select
<Select<number> options={[1, 2]} value=1 ... />
Without Generics, you'd be maintaining StringSelect, NumberSelect, or drowning in any.
Deep Dive: Utility Types Secret
Partial<T>, Pick<T, K>, Record<K, T>...
These built-in Types are just Generics under the hood.
// How Partial is actually defined
type MyPartial<T> = {
[P in keyof T]?: T[P];
};
"Loop (in) over all keys (keyof T) and add a question mark (?)."
Once you get Generics, you can create your own Utility Types ("Type Gymnastics").
7. Deep Dive: The infer Keyword (Unpacking Types)
The final boss of Generics: infer.
Used in Conditional Types, it means "Figure out this type and store it in a variable."
This is how the ReturnType utility is built:
// If T is a function, infer its Return Type as R, and give me R. Else any.
type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : any;
function getUser() { return { name: "Kim", age: 30 }; }
// UserType automatically becomes { name: string, age: number }
type UserType = MyReturnType<typeof getUser>;
Mastering infer unlocks the ability to read complex library source code.
You can create magic like "Extract the return type of function A and use it as the argument type for function B."
8. Case Study: Type-Safe JSON Parser (Recursive Generics)
I was building an internal Axios wrapper. API responses always had nested structures. Simple generics weren't enough.
The Problem
Deeply nested objects lost their type info.
The Fix: Recursive Generics
Types can reference themselves.
type JsonValue = string | number | boolean | null | JsonArray | JsonObject;
interface JsonObject { [key: string]: JsonValue; } // References JsonValue
interface JsonArray extends Array<JsonValue> {} // References JsonValue
// Usage
const data: JsonObject = {
user: {
posts: [ { id: 1, title: "Hello" } ] // Fully typed heavily nested data
}
};
This ensures data.user.posts[0].title is fully typed and autocompleted.
Generics are a language to describe Structure.
9. Tip: Naming Generics (Stop using T)
T, U, V... It looks like algebra. It's confusing.
Use meaningful names for readability.
T->TDataR->TResultP->TPropsE->TError
function fetchAPI<TData, TError>(url: string) { ... }
Your teammates will thank you.
10. Deep Dive: Advanced Constraints (keyof)
Want to write a safe getProperty function? Use keyof.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const user = { name: "Alice", age: 25 };
getProperty(user, "name"); // OK
getProperty(user, "email"); // Error: Argument "email" is not assignable to "name" | "age".
K extends keyof T translates to "K must be one of the keys of T".
This prevents accessing undefined properties at compile time.
11. Case Study: Implementing Pick & Omit
Classic gotcha.
// 1. Mapped Type: Iterate only over K
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
// 2. Use Exclude to subtract K from all keys
type MyOmit<T, K extends keyof T> = MyPick<T, Exclude<keyof T, K>>;
Understanding this enables you to craft types like PublicUserProfile (Omit password/email) effortlessly.