Generics & Utility Types
Generics
Generics allow you to write reusable, type-safe code that works with any type. Instead of hardcoding a type, you use a placeholder <T> that gets filled in when the function or type is used.
Generics are widely used in API response wrappers, utility types like Partial<T> and Pick<T, K>, React component props, and libraries like Axios, React Query, and Zod.
Basic Example — DOM Querying
// Without generic — returns Element | null (not useful)
const input = document.querySelector('.input');
// With generic — returns HTMLInputElement | null (fully typed)
const input = document.querySelector<HTMLInputElement>('.input');
input?.value; // TypeScript knows .value exists
Generic Functions
function identity<T>(value: T): T {
return value;
}
identity<string>("hello"); // → "hello"
identity<number>(42); // → 42
Generic with Constraints
Use extends to restrict what types T can be.
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
const person = { name: "Ajay", age: 23 };
getProperty(person, 'name'); // yes
getProperty(person, 'email'); // Error: email does not exist on person
Example — Array to Object Converter
function atoO<T>(arr: [string, T][]) {
const obj: { [index: string]: T } = {};
arr.forEach(([key, value]) => {
obj[key] = value;
});
return obj;
}
const arr: [string, number][] = [['k1', 23], ['k2', 324], ['k3', 342]];
const res = atoO(arr);
console.log(res); // { k1: 23, k2: 324, k3: 342 }
T is inferred as number from the input — no need to specify it manually.
Async Functions
An async function always returns a Promise. TypeScript lets you type the resolved value using Promise<T>.
Example 1 — Basic Promise
function wait(duration: number): Promise<string> {
return new Promise<string>((resolve) => {
setTimeout(() => resolve('hi'), duration);
});
}
wait(100).then(value => {
console.log(value); // 'hi'
});
Example 2 — async/await with typed return
async function fetchUserName(id: number): Promise<string> {
const response = await fetch(`https://api.example.com/users/${id}`);
const data = await response.json();
return data.name;
}
const name = await fetchUserName(1);
console.log(name); // "Ajay"
Example 3 — Typed object response
type User = {
id: number;
name: string;
email: string;
};
async function getUser(id: number): Promise<User> {
const response = await fetch(`https://api.example.com/users/${id}`);
const user: User = await response.json();
return user;
}
const user = await getUser(1);
console.log(user.name); // TypeScript knows .name exists
Example 4 — Error handling with try/catch
async function fetchData(url: string): Promise<string | null> {
try {
const response = await fetch(url);
if (!response.ok) throw new Error('Request failed');
return await response.text();
} catch (error) {
console.error(error);
return null;
}
}
Promise<string | null> is a clean pattern to represent success or failure without throwing.
Example 5 — Promise.all with typed results
async function loadDashboard(userId: number) {
const [user, posts] = await Promise.all([
fetch(`/api/users/${userId}`).then(r => r.json()) as Promise<User>,
fetch(`/api/posts?userId=${userId}`).then(r => r.json()) as Promise<Post[]>,
]);
return { user, posts };
}
Promise.all runs all requests in parallel — much faster than awaiting them one by one.
Example 6 — Void async
async function logActivity(action: string): Promise<void> {
await fetch('/api/log', {
method: 'POST',
body: JSON.stringify({ action }),
});
}
Key Rules
| Rule | Detail |
|---|---|
async always returns a Promise | Even plain return values get wrapped |
| Type the resolved value | Promise<string> not Promise<Promise<string>> |
await inside async only | Or at the top level of a module |
Prefer async/await over .then() | Cleaner and easier to debug |
Pick and Omit
Both Pick and Omit let you derive a new type from an existing one — useful for keeping types DRY across your app.
Pick<T, K> — keep only what you need
Whitelisting — specify the keys you want to keep.
type User = {
id: number;
name: string;
email: string;
password: string;
createdAt: Date;
};
type PublicUser = Pick<User, 'id' | 'name' | 'email'>;
const user: PublicUser = {
id: 1,
name: 'Ajay',
email: 'ajay@gmail.com',
// password — not part of PublicUser
};
Omit<T, K> — remove what you don't need
Blacklisting — specify the keys you want to remove.
type UserWithoutPassword = Omit<User, 'password'>;
const safeUser: UserWithoutPassword = {
id: 1,
name: 'Ajay',
email: 'ajay@gmail.com',
createdAt: new Date(),
// password — omitted
};
When to use which
| Situation | Use |
|---|---|
| You want 2–3 fields from a large type | Pick — less to write |
| You want to drop 1–2 fields from a large type | Omit — less to write |
| Building a form input type (no id, no timestamps) | Omit |
| Building a public API response (only safe fields) | Pick |
Real-world example — form vs DB type
type Post = {
id: number;
title: string;
content: string;
authorId: number;
createdAt: Date;
updatedAt: Date;
};
// User provides only these when creating
type CreatePostInput = Pick<Post, 'title' | 'content' | 'authorId'>;
// User can only update these fields
type UpdatePostInput = Omit<Post, 'id' | 'authorId' | 'createdAt' | 'updatedAt'>;
If you're writing Pick with most of the keys, switch to Omit — and vice versa. Always pick the shorter one.
Partial and Required
Partial<T> — make all fields optional
Useful when updating a record — you don't want to pass every field every time.
type User = {
name: string;
email: string;
age: number;
};
function updateUser(id: number, fields: Partial<User>) {
// fields can have any combination of name, email, age
}
updateUser(1, { name: 'Ajay' }); // works
updateUser(1, { email: 'a@gmail.com' }); // works
updateUser(1, { name: 'Ajay', age: 23 }); // works
Required<T> — make all fields mandatory
Opposite of Partial — strips all ? and forces every field to be present.
type Config = {
host?: string;
port?: number;
debug?: boolean;
};
// Before saving to DB, ensure everything is set
function saveConfig(config: Required<Config>) {
// host, port, debug — all must be provided
}
saveConfig({ host: 'localhost', port: 3000, debug: false }); // works
saveConfig({ host: 'localhost' }); // Error: port and debug are missing
You can combine Partial and Required with Pick and Omit to make only specific fields required and the rest optional:
and these are 1 level modifiers.
// host is required, everything else is optional
type StrictHost = Required<Pick<Config, 'host'>> & Partial<Omit<Config, 'host'>>;
const c: StrictHost = { host: 'localhost' }; // works
This pattern is very handy for configs and form types where some fields are always mandatory but the rest are optional.
ReturnType and Parameters
ReturnType<T> — extract what a function returns
function getUser() {
return { id: 1, name: 'Ajay', email: 'ajay@gmail.com' };
}
type User = ReturnType<typeof getUser>;
// { id: number; name: string; email: string }
No need to define a separate User type — just derive it from the function.
Parameters<T> — extract the parameters of a function
function createPost(title: string, content: string, authorId: number) {
// ...
}
type PostParams = Parameters<typeof createPost>;
// [title: string, content: string, authorId: number]
// Access individual param types
type TitleType = PostParams[0]; // string
type AuthorIdType = PostParams[2]; // number
Using both together
async function fetchUser(id: number, token: string) {
const response = await fetch(`/api/users/${id}`);
return response.json() as Promise<{ id: number; name: string }>;
}
type FetchUserParams = Parameters<typeof fetchUser>;
// [id: number, token: string]
type FetchUserReturn = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
ReturnType and Parameters are great when working with third-party functions where you don't have access to the original types — just derive them directly from the function.
Record
Record<K, V> — map keys to a value type
Record creates an object type where all keys are of type K and all values are of type V.
type Role = 'admin' | 'editor' | 'viewer';
type Permissions = Record<Role, boolean>;
const permissions: Permissions = {
admin: true,
editor: true,
viewer: false,
};
With object values
type UserInfo = {
name: string;
age: number;
};
type UserMap = Record<string, UserInfo>;
const users: UserMap = {
u1: { name: 'Ajay', age: 23 },
u2: { name: 'Ram', age: 25 },
};
Real-world — caching API responses
type Cache = Record<string, unknown>;
const cache: Cache = {};
function setCache(key: string, value: unknown) {
cache[key] = value;
}
Record<string, unknown> is the correct type for a plain object with unknown values — prefer it over {} or object.
Readonly (Utility Type)
Readonly<T> makes all properties of a type immutable — you can't reassign any field after the object is created.
type User = {
id: number;
name: string;
email: string;
};
const user: Readonly<User> = {
id: 1,
name: 'Ajay',
email: 'ajay@gmail.com',
};
user.name = 'Ram'; // Error: cannot assign to 'name' because it is read-only
Real-world — config objects
const config: Readonly<{
apiUrl: string;
timeout: number;
}> = {
apiUrl: 'https://api.example.com',
timeout: 3000,
};
config.apiUrl = 'https://other.com'; // Error
Just like Partial and Required, Readonly is shallow — nested objects are still mutable.
Awaited
Awaited<T> unwraps the resolved type of a Promise — useful when you want the inner type without calling the function.
type A = Awaited<Promise<string>>;
// string
type B = Awaited<Promise<Promise<number>>>;
// number — unwraps nested promises too
Real-world — extracting return type of async functions
async function fetchUser() {
return { id: 1, name: 'Ajay' };
}
type User = Awaited<ReturnType<typeof fetchUser>>;
// { id: number; name: string }
Awaited<ReturnType<typeof fn>> is the standard pattern to derive the resolved type from any async function — no need to define the type separately.