TypeScript advanced types · 7 / 8
lesson 7

Utility types from scratch

Pick, Omit, Partial, Required, Readonly, Awaited — each one is a one-liner once you've seen the building blocks.

~ 17 min read·lesson 7 of 8
0 / 8

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.

partial.ts
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.

required.ts
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.

readonly.ts
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?

check your understanding
Which one-line definition matches 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.

pick.ts
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.

omit.ts
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.

Tip

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.

awaited.ts
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.

Watch out

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.

check your understanding
Why does Pick<T, K> need the constraint K extends keyof T?
check your understanding
Given Exclude<U, X> = U extends X ? never : U, what is Exclude<"a" | "b" | "c", "b">?

Try it yourself

check your understanding
Write Mutable<T> — strips readonly from every field. Which is right?
check your understanding
You build Pick<T, K> = { [P in K]: T[P] }. Why does P in K work — isn't K just a union of strings?
check your understanding
Looking at the family — Partial, Required, Readonly, Pick, Omit — what's the single underlying technique?
← prevnext lesson →
KeepLearningcertificate
for completing
TypeScript advanced types
0 of 8 read