Utility types from scratch
Pick, Omit, Partial, Required, Readonly, Awaited — each one is a one-liner once you've seen the building blocks.
Every utility type that ships with TypeScript is a few lines of code you can now write yourself. Reimplementing them isn't busywork — each one cements a piece of the previous lessons. Once you can name Partial as a mapped type that adds the optional modifier, you'll never have to memorize the list again.
Partial and Required
Partial<T> makes every field of T optional. Mapped type, single modifier.
type MyPartial<T> = {
[K in keyof T]?: T[K];
};
type User = { id: number; name: string };
type Draft = MyPartial<User>;
// { id?: number; name?: string }The loop walks every key. The ? marker after the bracket adds optionality to each field of the result. The value type stays as T[K] — whatever the original held.
Required<T> is the mirror image. The -? prefix strips optionality.
type MyRequired<T> = {
[K in keyof T]-?: T[K];
};
type Form = { name?: string; age?: number };
type Submitted = MyRequired<Form>;
// { name: string; age: number }Same shape, opposite modifier. Reading them side by side is the fastest way to lock in the +/- rule from lesson 3.
Readonly
Same template, different modifier.
type MyReadonly<T> = {
readonly [K in keyof T]: T[K];
};
type Frozen = MyReadonly<{ id: number; name: string }>;
// { readonly id: number; readonly name: string }Three utilities, three modifier configurations of the same mapped type. That's the whole pattern — the Partial, Required, and Readonly family is just which modifier do I want, with what sign?
Required<T>?Pick
Pick<T, K> keeps only the keys you list. The first parameter is the source object type; the second is a union of keys to keep.
type MyPick<T, K extends keyof T> = {
[P in K]: T[P];
};
type User = { id: number; name: string; email: string };
type Public = MyPick<User, "id" | "name">;
// { id: number; name: string }Two pieces. The constraint K extends keyof T says the key list has to be a subset of the source's keys. The mapped type then loops over K directly — not keyof T. Looping over K is what restricts the result to just the picked keys.
The mapped-type loop doesn't have to walk keyof T. It can walk any union of valid keys. That insight unlocks several utilities at once.
Omit
Omit<T, K> is the opposite of Pick. The trick is computing all keys except the listed ones, then picking those.
type MyExclude<U, X> = U extends X ? never : U;
type MyOmit<T, K extends keyof any> =
MyPick<T, MyExclude<keyof T, K>>;
type User = { id: number; name: string; email: string };
type WithoutEmail = MyOmit<User, "email">;
// { id: number; name: string }Two helpers, both small. Exclude<U, X> uses a distributive conditional from lesson 1: split U into its members, drop any that match X, union the rest. Omit then picks every remaining key from T.
The constraint on K is keyof any (which is string | number | symbol) instead of keyof T. That matches the lib version — Omit is more lenient about the keys you pass in, so a typo doesn't error, it just fails to remove anything. Whether that's a feature or a footgun is debated.
Build complex utility types out of small ones. Omit only takes one new line because Pick and Exclude already exist. Reuse beats reinvention every time.
Awaited
A simplified Awaited<T> recursively unwraps promises. Lesson 2 introduced it; here's the polished version.
type MyAwaited<T> = T extends Promise<infer U> ? MyAwaited<U> : T; type A = MyAwaited<Promise<string>>; // string type B = MyAwaited<Promise<Promise<number>>>; // number type C = MyAwaited<string>; // string
The conditional matches a promise shape. If it matches, recurse on the inner type. If not, hand the type back. The lib version is fancier — it handles thenables (objects with a then method) and null/undefined edge cases — but the core idea is exactly this.
The lib's Awaited is much hairier than the version above. It handles thenables, the strict null case, and infinite-recursion guards. For app code, the simple version is fine; for library code, prefer the built-in.
Pick<T, K> need the constraint K extends keyof T?Exclude<U, X> = U extends X ? never : U, what is Exclude<"a" | "b" | "c", "b">?Try it yourself
Mutable<T> — strips readonly from every field. Which is right?Pick<T, K> = { [P in K]: T[P] }. Why does P in K work — isn't K just a union of strings?Partial, Required, Readonly, Pick, Omit — what's the single underlying technique?