
Mastering TypeScript Generics
Stop using 'any'. How to build reusable, type-safe components using Generics. Advanced patterns with extends, keyof, and infer.

Stop using 'any'. How to build reusable, type-safe components using Generics. Advanced patterns with extends, keyof, and infer.
Why does my server crash? OS's desperate struggle to manage limited memory. War against Fragmentation.

Two ways to escape a maze. Spread out wide (BFS) or dig deep (DFS)? Who finds the shortest path?

A comprehensive deep dive into client-side storage. From Cookies to IndexedDB and the Cache API. We explore security best practices for JWT storage (XSS vs CSRF), performance implications of synchronous APIs, and how to build offline-first applications using Service Workers.

Fast by name. Partitioning around a Pivot. Why is it the standard library choice despite O(N²) worst case?

any Hell to Type Safety HeavenI made a terrible mistake when I first adopted TypeScript in my project. I sprinkled any everywhere. Every time I saw a red squiggly line, I just slapped an any on it. Problem solved, right? Wrong.
function getData(data: any): any {
return data;
}
function processUser(user: any) {
console.log(user.nmae); // typo, but no error
return user;
}
The first week was bliss. No more compiler errors. I was shipping features fast. Then production happened. The app crashed. user.nmae was undefined. I had typed nmae instead of name, and TypeScript didn't catch it because I used any.
That's when I understood. any is an off switch for TypeScript's protection. If you use any, you might as well write JavaScript. You get all the overhead of TypeScript with none of the benefits.
But realistically, sometimes you need to accept "any type". API wrappers, caching utilities, reusable components. How do you handle those without any?
Generics. They let you write flexible code that works with multiple types while preserving type safety. It was hard at first, but now I can't imagine writing TypeScript without generics. They're that powerful.
T?"I first saw generics in React's documentation.
function useState<T>(initialValue: T): [T, (value: T) => void] {
// ...
}
"What the hell is T?" That was my first reaction. The angle brackets (<>) looked alien. A single letter T seemed cryptic. It felt like reading a math equation.
But as I kept reading code, a pattern emerged. When you write useState<number>(0), T becomes number. When you write useState<string>(""), T becomes string.
It clicked. Generics are "passing types as parameters". Just like you pass values as arguments to functions, you pass types as arguments to generics. Generics are like "functions for types".
The metaphor that made it stick for me was a baking mold (like a fish-shaped pastry mold).
Box<T> is the mold.T is what goes into the mold. It could be red bean paste or custard cream.Box<number> is a box containing a number. Box<string> is a box containing a string.One mold, multiple pastries by changing the ingredient. That's the essence of generics.
// Generic interface
interface Box<T> {
item: T;
}
const numberBox: Box<number> = { item: 123 };
const stringBox: Box<string> = { item: "hello" };
const userBox: Box<User> = { item: { id: 1, name: "Ratia" } };
You reuse the same Box structure while changing only the type. If you used any, TypeScript wouldn't know what's inside item. But with generics, numberBox.item is correctly inferred as number.
The simplest form is a generic function. Let's write an identity function that returns whatever you pass in.
// Bad: using any (❌)
function identityBad(arg: any): any {
return arg;
}
const result1 = identityBad(123); // result1's type: any (type info lost)
console.log(result1.toUpperCase()); // No error! Crashes at runtime
// Good: using generics (✅)
function identity<T>(arg: T): T {
return arg;
}
const result2 = identity(123); // result2's type: number (type info preserved)
console.log(result2.toUpperCase()); // Compile error! number has no toUpperCase
The difference is clear. any throws away type information. Generics preserve it. Type inference happens automatically. When you write identity(123), TypeScript infers T as number.
You can use generics in interfaces too. This is especially useful for common structures like API responses.
// Generic interface
interface ApiResponse<T> {
code: number;
message: string;
data: T; // This part varies
timestamp: number;
}
// User info
interface User {
id: number;
name: string;
email: string;
}
// Product info
interface Product {
id: number;
title: string;
price: number;
}
// Type-safe API functions
async function fetchUser(): Promise<ApiResponse<User>> {
const response = await fetch('/api/user');
return response.json();
}
async function fetchProduct(id: number): Promise<ApiResponse<Product>> {
const response = await fetch(`/api/product/${id}`);
return response.json();
}
// Usage
const userResponse = await fetchUser();
console.log(userResponse.data.name); // ✅ Autocomplete works!
console.log(userResponse.data.price); // ❌ Error! User has no price
const productResponse = await fetchProduct(123);
console.log(productResponse.data.price); // ✅ Works
After introducing this pattern to my project, API response handling became so much cleaner. The response structure is consistent, so you don't have to retype code, message, data every time. Just swap out the data part with a generic.
You can use generics in classes too. Let's build a simple Stack data structure.
class Stack<T> {
private items: T[] = [];
push(item: T): void {
this.items.push(item);
}
pop(): T | undefined {
return this.items.pop();
}
peek(): T | undefined {
return this.items[this.items.length - 1];
}
isEmpty(): boolean {
return this.items.length === 0;
}
size(): number {
return this.items.length;
}
}
// Number stack
const numberStack = new Stack<number>();
numberStack.push(1);
numberStack.push(2);
numberStack.push(3);
console.log(numberStack.pop()); // 3
// String stack
const stringStack = new Stack<string>();
stringStack.push("hello");
stringStack.push("world");
console.log(stringStack.peek()); // "world"
// Custom object stack
interface Task {
id: number;
title: string;
done: boolean;
}
const taskStack = new Stack<Task>();
taskStack.push({ id: 1, title: "Learn Generics", done: true });
taskStack.push({ id: 2, title: "Build Project", done: false });
Now Stack can hold any type, but type safety is maintained. If you write numberStack.push("string"), you get a compile error.
T with extendsSometimes generics are too flexible. You run into "I can't accept just anything..." situations.
For example, let's say you want to write a function that logs the length of arrays or strings.
// This doesn't work
function logLength<T>(arg: T): void {
console.log(arg.length); // ❌ Error! No guarantee T has length
}
TypeScript doesn't know what T is, so it can't confirm T has a length property. This is where you need constraints.
extends to Constrain// T must have a length property
interface Lengthwise {
length: number;
}
function logLength<T extends Lengthwise>(arg: T): void {
console.log(arg.length); // ✅ Now it works
}
logLength("hello"); // OK (string has length)
logLength([1, 2, 3]); // OK (array has length)
logLength({ length: 10, value: 3 }); // OK (has length property)
logLength(10); // ❌ Error! number has no length
T extends Lengthwise means "T must at least have the shape of Lengthwise". This restricts T's freedom while guaranteeing the properties you need.
You don't have to use just one generic parameter. You can use T, U, K, etc., simultaneously.
// Return two values as a tuple
function pair<T, U>(first: T, second: U): [T, U] {
return [first, second];
}
const result1 = pair(1, "hello"); // [number, string]
const result2 = pair(true, { name: "Ratia" }); // [boolean, { name: string }]
This pattern is common when building data structures like Map, where keys and values have different types.
keyof with Generics: Type-Safe Object AccessWhen you want to safely access object properties, combining keyof with generics is incredibly powerful.
// Bad: using any (❌)
function getPropertyBad(obj: any, key: string): any {
return obj[key]; // Typos go unnoticed
}
const user = { name: "Ratia", age: 30 };
getPropertyBad(user, "height"); // Returns undefined, no error
"height" isn't a key in the user object, but TypeScript doesn't catch this. You get undefined at runtime, which causes errors later when you try to use it.
keyof// Good: using generics + keyof (✅)
function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
return obj[key];
}
const user = { name: "Ratia", age: 30 };
const name = getProperty(user, "name"); // ✅ name's type: string
const age = getProperty(user, "age"); // ✅ age's type: number
const height = getProperty(user, "height"); // ❌ Compile error!
keyof T is a union type of all keys in the T object. For user, keyof typeof user is "name" | "age". When you write K extends keyof T, it means K must be one of T's keys.
And the return type is precise too. T[K] means "the type of the value corresponding to key K in object T". user["name"] is string, and user["age"] is number. This is inferred automatically.
When I first saw this pattern, it blew my mind. It completely prevents typos. Now if I write getProperty(user, "nmae"), my IDE immediately underlines it in red.
pick FunctionLet's build a function that picks specific keys from an object.
function pick<T, K extends keyof T>(obj: T, keys: K[]): Pick<T, K> {
const result = {} as Pick<T, K>;
keys.forEach(key => {
result[key] = obj[key];
});
return result;
}
interface User {
id: number;
name: string;
email: string;
password: string;
}
const user: User = {
id: 1,
name: "Ratia",
email: "ratia@example.com",
password: "secret123"
};
// Pick only public info, excluding password
const publicUser = pick(user, ["id", "name", "email"]);
// publicUser's type: { id: number; name: string; email: string; }
console.log(publicUser.name); // ✅ Works
console.log(publicUser.password); // ❌ Error! password isn't in Pick
I use this pattern a lot when building products. When sending API responses to the frontend, you can use pick or omit functions to exclude sensitive info like passwords in a type-safe way.
infer: Advanced Type InferenceConditional types are like if-else at the type level. And the infer keyword is like saying "TypeScript, please figure out what type this is for me."
This was the hardest part for me at first. It felt like programming at the type level. But once I understood it, I realized how powerful it is.
T extends U ? X : Ytype IsString<T> = T extends string ? "yes" : "no";
type A = IsString<string>; // "yes"
type B = IsString<number>; // "no"
type C = IsString<"hello">; // "yes" (literals are subtypes of string)
"If T extends string, return 'yes', otherwise 'no'."
infer: Extracting the Type Inside a PromiseThe most common example is extracting the inner type of a Promise.
// Extract T from Promise<T>
type Unwrap<T> = T extends Promise<infer U> ? U : T;
type A = Unwrap<Promise<string>>; // string
type B = Unwrap<Promise<number>>; // number
type C = Unwrap<string>; // string (if not a Promise, return as-is)
// Real-world example
async function fetchUser(): Promise<User> {
// ...
}
type UserType = Unwrap<ReturnType<typeof fetchUser>>; // User
infer U means "figure out what type is here and call it U". If Promise<string> comes in, TypeScript infers U as string.
At first, this felt like magic. Now I use it regularly, especially when extracting return types from library functions.
type Parameters<T> = T extends (...args: infer P) => any ? P : never;
function greet(name: string, age: number): void {
console.log(`Hello, ${name}. You are ${age} years old.`);
}
type GreetParams = Parameters<typeof greet>; // [string, number]
infer P extracts the function's parameter types as a tuple. This is actually a built-in TypeScript utility type, but understanding how to build it yourself helps you grasp the principle.
type ElementType<T> = T extends (infer U)[] ? U : T;
type A = ElementType<string[]>; // string
type B = ElementType<number[]>; // number
type C = ElementType<User[]>; // User
Extracts the element type from an array type. I use this often too.
Sometimes the built-in utility types aren't enough. You need to build your own.
DeepPartial<T>: Making Nested Objects Optional Tootype DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};
interface Config {
database: {
host: string;
port: number;
credentials: {
username: string;
password: string;
};
};
cache: {
enabled: boolean;
ttl: number;
};
}
type PartialConfig = DeepPartial<Config>;
const config: PartialConfig = {
database: {
host: "localhost" // port, credentials optional
}
// cache also optional
};
Partial only makes the top level optional. If you want to make nested object properties optional too, you need DeepPartial.
React is where I really felt the power of generics, especially when building custom hooks.
interface QueryResult<T> {
data: T | null;
loading: boolean;
error: Error | null;
refetch: () => void;
}
function useQuery<T>(url: string): QueryResult<T> {
const [data, setData] = useState<T | null>(null);
const [loading, setLoading] = useState(true);
const [error, setError] = useState<Error | null>(null);
const fetchData = async () => {
try {
setLoading(true);
const response = await fetch(url);
const json = await response.json();
setData(json);
} catch (err) {
setError(err as Error);
} finally {
setLoading(false);
}
};
useEffect(() => {
fetchData();
}, [url]);
return { data, loading, error, refetch: fetchData };
}
// Usage
interface User {
id: number;
name: string;
}
function UserProfile({ userId }: { userId: number }) {
const { data, loading, error } = useQuery<User>(`/api/users/${userId}`);
if (loading) return <div>Loading...</div>;
if (error) return <div>Error: {error.message}</div>;
if (!data) return null;
return <div>{data.name}</div>; // ✅ data.name is correctly typed as string
}
Thanks to generics, when you write useQuery<User>, the returned data is typed as User | null. You get autocomplete and typo prevention.
When I first saw generics, they were intimidating. The angle brackets looked foreign, and T was confusing. But now I can't write TypeScript without them.
any. You keep flexibility while maintaining type safety.Patterns I use most in real work:
ApiResponse<T>useQuery<T>, useForm<T>pick, omit, getPropertyList<T>, Table<T>Mastering generics unlocks TypeScript's true power. It's hard at first, but keep using them and they become natural. And once you're comfortable with them, you can never go back to any.
T, U, K, etc.extends): Forcing a generic type T to follow a specific shape.keyof Operator: Extracts all keys of an object type as a union type.T extends U ? X : Y) at the type level.infer Keyword: Used in conditional types to deduce and assign a type to a variable.Partial, Pick, Omit, etc.).A | B. A or B.A & B. Has properties of both A and B.any and unknown?Both can accept any value, but unknown is safer.
any: Completely disables type checking. You can call any method. Runtime error risk.
let value: any = "hello";
value.toUpperCase(); // OK
value.foo.bar.baz(); // OK (no compile error, crashes at runtime)
unknown: Type is unknown. Requires type checking before use.
let value: unknown = "hello";
value.toUpperCase(); // ❌ Error! Can't call methods on unknown
if (typeof value === "string") {
value.toUpperCase(); // ✅ OK (after type guard)
}
Rule: If you're unsure, use unknown instead of any. It's safer.
any?any: Completely loses type information.
function identity(arg: any): any {
return arg;
}
const result = identity(123); // result's type: any
Generics: Preserves type information.
function identity<T>(arg: T): T {
return arg;
}
const result = identity(123); // result's type: number
Use generics in these situations:
any, but don't want to lose type safety.infer?Mainly when building libraries or utility types. Rarely used in regular application code. Utility types like ReturnType and Parameters use infer internally.
Use cases:
Almost the same, but some differences:
Declaration Merging:
Expressiveness:
|, intersections &, tuples, etc.).Extension:
extends keyword.&).Rule: Use interface for object types, type for unions or complex types. Libraries prefer interface because they need declaration merging.