TypeScript ships utility types that cover most everyday transformations: Partial<T> (all optional — perfect for update payloads), Required<T>, Pick<T, K> and Omit<T, K> (subset of keys), Record<K, V> (dictionary), ReturnType<F>, Parameters<F> and Awaited<T>.
Derive types from a single source of truth instead of repeating them. If the User interface changes, Omit<User, "id"> updates automatically everywhere.
The satisfies operator checks that a value matches a type without widening it, so you keep precise literal types (and autocomplete) while still getting validation.
interface User {
id: number;
name: string;
email: string;
passwordHash: string;
createdAt: Date;
}
type NewUser = Omit<User, "id" | "createdAt">; // input for create
type UserUpdate = Partial<Pick<User, "name" | "email">>;
type PublicUser = Omit<User, "passwordHash">; // safe to send to clients
function toPublic({ passwordHash: _omit, ...rest }: User): PublicUser {
return rest;
}
async function loadUser(id: number) {
return { id, name: "Amina", email: "amina@example.com", passwordHash: "x", createdAt: new Date() };
}
type Loaded = Awaited<ReturnType<typeof loadUser>>; // the resolved object type
type Theme = "light" | "dark";
const palette = {
light: { bg: "#ffffff", text: "#1B2435" },
dark: { bg: "#0E1826", text: "#E7ECF6" },
} satisfies Record<Theme, { bg: string; text: string }>;
const draft: NewUser = { name: "Juma", email: "juma@example.com", passwordHash: "h" };
const patch: UserUpdate = { email: "new@example.com" };
loadUser(1).then((u: Loaded) => {
console.log(toPublic(u), draft.name, patch, palette.dark.bg);
});Key points
- Derive related types (create, update, public) from one base type.
Omitsensitive fields to build types that are safe to expose.satisfiesvalidates a value while preserving its exact inferred type.
Exercise
Given a Product interface, derive CreateProductInput, UpdateProductInput and ProductSummary (id, name, price only). Write a const route table with satisfies Record<string, { method: "GET" | "POST"; auth: boolean }>.
Show solution
Try the exercise yourself first — then compare your approach with this one.
Each input type is derived from the one Product interface, so changing Product updates them all. The route table uses satisfies: every entry is checked against the allowed shape, yet routes keeps its exact keys, so a typo like routes["/prodcts"] is still caught.
interface Product {
id: number;
name: string;
price: number;
stock: number;
createdAt: Date;
}
type CreateProductInput = Omit<Product, "id" | "createdAt">;
type UpdateProductInput = Partial<CreateProductInput>;
type ProductSummary = Pick<Product, "id" | "name" | "price">;
const routes = {
"/products": { method: "GET", auth: false },
"/products/new": { method: "POST", auth: true },
} satisfies Record<string, { method: "GET" | "POST"; auth: boolean }>;
const input: CreateProductInput = { name: "Geometry set", price: 4500, stock: 40 };
const patch: UpdateProductInput = { price: 4000 };
const summary: ProductSummary = { id: 1, name: input.name, price: patch.price ?? input.price };
console.log(summary, routes["/products/new"].auth); // { id: 1, name: 'Geometry set', price: 4000 } true