When you start working with TypeScript, you’ll soon come across Generics and Utility Types.
They may sound complicated, but they’re actually tools that make your code more reusable, flexible, and type-safe.
Let’s break them down step-by-step with examples that are easy to follow.
1. What Are Generics?
Imagine you’re writing a function that takes a value and returns it — kind of like an “identity” function.
Without generics, you’d have to write the same function multiple times for different types:
function returnNumber(value: number): number {
return value;
}
function returnString(value: string): string {
return value;
}
That’s a lot of repetition!
Instead, we can use generics to make this function reusable for any type.
Example:
function identity<T>(value: T): T {
return value;
}
Now you can use it for any type:
identity<number>(100); // returns number
identity<string>("Hello"); // returns string
Here, T is a type variable — a placeholder for any type.
When you call the function, TypeScript replaces T with the actual type you pass in.
2. Generics with Arrays and Interfaces
Generics are not just for functions — you can use them in interfaces, classes, and types too.
Example: Generic Array
function getFirst<T>(arr: T[]): T {
return arr[0];
}
const numbers = getFirst([10, 20, 30]); // number
const names = getFirst(["Heartin", "Sneha"]); // string
Example: Generic Interface
interface Box<T> {
content: T;
}
const numberBox: Box<number> = { content: 42 };
const stringBox: Box<string> = { content: "Hello" };
The benefit?
You don’t need separate interfaces for NumberBox, StringBox, etc. — one generic interface covers all.
3. Generic Constraints
Sometimes, you want to make sure your generic type has certain properties.
For example, you might only want to work with objects that have a length property (like arrays or strings).
You can do that using constraints.
Example:
function logLength<T extends { length: number }>(item: T): void {
console.log(item.length);
}
logLength("Hello"); // works
logLength([1, 2, 3]); // works
// logLength(10); ❌ Error: number doesn’t have 'length'
The keyword extends restricts T to types that have a length property.
4. What Are Utility Types?
TypeScript includes several built-in utility types that make working with objects and types easier.
They save you from writing repetitive or complex type definitions.
Let’s look at the most common ones.
🔹 Partial<Type>
Makes all properties optional.
interface User {
name: string;
age: number;
}
const updateUser = (user: Partial<User>) => {
console.log(user);
};
updateUser({ name: "Heartin" }); // ✅ age is optional now
🔹 Required<Type>
Makes all properties mandatory.
interface User {
name?: string;
age?: number;
}
const user: Required<User> = { name: "Sneha", age: 35 }; // both required
🔹 Readonly<Type>
Prevents properties from being changed.
interface Config {
appName: string;
}
const config: Readonly<Config> = { appName: "BuddyTutor" };
// config.appName = "NewApp"; ❌ Error: Cannot assign to read-only property
🔹 Pick<Type, Keys>
Selects specific properties from a type.
interface User {
name: string;
age: number;
email: string;
}
type BasicUser = Pick<User, "name" | "email">;
const user: BasicUser = { name: "Heartin", email: "heartin@example.com" };
🔹 Omit<Type, Keys>
Does the opposite of Pick — removes specific properties.
type UserWithoutEmail = Omit<User, "email">;
const user: UserWithoutEmail = { name: "Sneha", age: 35 };
🔹 Record<Keys, Type>
Creates an object type where all keys have the same value type.
type Roles = "admin" | "editor" | "viewer";
const permissions: Record<Roles, boolean> = {
admin: true,
editor: true,
viewer: false
};
🔹 ReturnType<Type>
Gets the return type of a function.
function getAge() {
return 35;
}
type AgeType = ReturnType<typeof getAge>; // number
5. Combining Generics with Utility Types
You can mix generics and utility types to create powerful and reusable code.
Example:
function updateRecord<T extends object>(record: T, fields: Partial<T>): T {
return { ...record, ...fields };
}
const user = { name: "Heartin", age: 35 };
const updatedUser = updateRecord(user, { age: 36 });
console.log(updatedUser); // { name: "Heartin", age: 36 }
Here:
Tcan be any object.Partial<T>lets you update only some fields.
6. Quick Summary
| Concept | What It Does | Example |
|---|---|---|
| Generic | Creates reusable code that works with any type | function identity<T>(value: T): T |
| Partial | Makes all fields optional | Partial<User> |
| Pick | Keeps only selected fields | Pick<User, "name"> |
| Omit | Removes selected fields | Omit<User, "email"> |
| Readonly | Prevents modification | Readonly<User> |
| Record | Creates a map-like object | Record<string, number> |
| ReturnType | Extracts return type of function | ReturnType<typeof fn> |
Final Thoughts
Generics and utility types make your TypeScript code cleaner, smarter, and reusable.
Generics help your code work with multiple types, while utility types help you transform and manage complex objects easily.
If you’re building large applications, mastering these two features will make your code more maintainable and type-safe.