Utility and Advanced Types · Lesson 15 of 17
TypeScript Utility Types
TypeScript ships a set of generic utility types that transform one type into another. Instead of writing a second UserUpdate interface by hand and keeping it in sync, you write Partial<User>. Teams use them constantly for API payloads, form state and component props, so knowing them well removes a lot of duplicated types.
In this lesson
- Shaping objects:
Partial,Required,Readonly,Pick,Omit,Record - Filtering unions:
Exclude,Extract,NonNullable - Reading functions:
ReturnType,Parameters,Awaited - Combining them for real API types
Partial, Required and Readonly
interface User {
id: number;
name: string;
email: string;
phone?: string;
}
function updateUser(user: Readonly<User>, changes: Partial<User>): User { // every field optional
return { ...user, ...changes };
}
const asha: Readonly<User> = { id: 1, name: "Asha", email: "asha@example.com" };
const updated = updateUser(asha, { phone: "98xxxxxx10" });
console.log(updated);
const complete: Required<User> = { ...updated, phone: updated.phone ?? "" }; // every field required
console.log(Object.keys(complete).length);
{ id: 1, name: 'Asha', email: 'asha@example.com', phone: '98xxxxxx10' }
4interface User { id: number; name: string }
const asha: Readonly<User> = { id: 1, name: "Asha" };
asha.name = "Ravi";
const draft: Required<Partial<User>> = { id: 2 };
console.log(asha, draft);
main.ts(3,6): error TS2540: Cannot assign to 'name' because it is a read-only property.
main.ts(4,7): error TS2741: Property 'name' is missing in type '{ id: number; }' but required in type 'Required<Partial<User>>'.Pick, Omit and Record
interface User {
id: number;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
type PublicUser = Omit<User, "passwordHash">; // everything except
type UserPreview = Pick<User, "id" | "name">; // only these
type CreateUserInput = Omit<User, "id" | "createdAt">; // what the client sends
type Role = "admin" | "editor" | "viewer";
const permissions: Record<Role, string[]> = { // a key for every role is required
admin: ["read", "write", "delete"],
editor: ["read", "write"],
viewer: ["read"],
};
function toPublic(user: User): PublicUser {
const { passwordHash, ...rest } = user; // strip the secret before sending
return rest;
}
const input: CreateUserInput = { name: "Ravi", email: "ravi@example.com", passwordHash: "x9f..." };
const saved: User = { id: 7, createdAt: new Date("2026-01-15"), ...input };
const preview: UserPreview = { id: saved.id, name: saved.name };
console.log(Object.keys(toPublic(saved)), preview, permissions.editor);
[ 'id', 'createdAt', 'name', 'email' ] { id: 7, name: 'Ravi' } [ 'read', 'write' ]Derive, don't duplicate
Keep one source type (User) and derive the rest. When a field is added to User, PublicUser and CreateUserInput pick it up automatically, and Record<Role, …> fails to compile until the new role gets an entry.
Filtering unions
type Status = "draft" | "review" | "published" | "archived";
type Live = Extract<Status, "published" | "review">; // keep matching members
type Editable = Exclude<Status, "published" | "archived">; // remove members
type MaybeName = string | null | undefined;
type Name = NonNullable<MaybeName>; // string
const live: Live[] = ["review", "published"];
const editable: Editable[] = ["draft", "review"];
const name: Name = "Asha";
console.log(live, editable, name);
[ 'review', 'published' ] [ 'draft', 'review' ] Asha
ReturnType, Parameters and Awaited
function createOrder(customerId: number, items: { sku: string; qty: number }[], coupon?: string) {
return {
id: `ORD-${customerId}-${items.length}`,
total: items.reduce((sum, i) => sum + i.qty * 100, 0),
coupon: coupon ?? null,
};
}
type Order = ReturnType<typeof createOrder>; // the inferred object type
type OrderArgs = Parameters<typeof createOrder>; // [number, {...}[], string?]
async function fetchOrder(id: string): Promise<Order> {
return { id, total: 250, coupon: null };
}
type FetchedOrder = Awaited<ReturnType<typeof fetchOrder>>; // unwraps the Promise → Order
const args: OrderArgs = [42, [{ sku: "PEN", qty: 3 }], "SAVE10"];
const order: Order = createOrder(...args);
const fetched: FetchedOrder = await fetchOrder("ORD-1");
console.log(order, fetched.total);
{ id: 'ORD-42-1', total: 300, coupon: 'SAVE10' } 250These are handy when a library exports a function but not its types: ReturnType<typeof someFn> gives you the type without copying it.
| Utility | Result |
|---|---|
Partial<T> / Required<T> | all properties optional / required |
Readonly<T> | all properties read-only |
Pick<T, K> / Omit<T, K> | keep / drop the listed keys |
Record<K, V> | object with keys K and values V |
Exclude<U, X> / Extract<U, X> | remove / keep union members |
NonNullable<T> | remove null and undefined |
ReturnType<F> / Parameters<F> | a function's return type / argument tuple |
Awaited<T> | the value inside a Promise |
Practice
Try it yourself
Define
ProductForm: every field ofProductexceptid, all optional, for a half-filled form.Show solution
interface Product { id: number; name: string; price: number; tags: string[] } type ProductForm = Partial<Omit<Product, "id">>; const draft: ProductForm = { name: "Notebook" }; const ready: ProductForm = { name: "Notebook", price: 120, tags: ["stationery"] }; console.log(Object.keys(draft).length, Object.keys(ready).length);1 3
Quick check
Difference between Pick and Omit?
Pick keeps the listed keys; Omit keeps everything except them.
Does Readonly<T> freeze the object at run time?
No – it's compile-time only. Use Object.freeze for run-time protection.
How do you get the resolved type of an async function?
Awaited<ReturnType<typeof fn>>.
Summary
- Utility types derive new types from one source type – no duplicated interfaces.
- Partial/Required/Readonly/Pick/Omit/Record shape objects; Exclude/Extract/NonNullable filter unions.
- ReturnType, Parameters and Awaited read types from functions.
Finished this lesson?
Mark it complete to track your progress, then carry on with the next lesson.