TypeScript Survival Guide

Favorite typescript videos

059 — You are a Junior Dev if You Don’t Know These 18 TypeScript Utility Types

The video works through all 18 utility types against one base `User` type (`id`, `name`, `age`, `address`), motivated by three functions that each want a different slice of it: `createUser` wants everything except `id`, `updateUser` wants any subset, and `renderUserDetails` wants only `name` and `age`. `Pick` and `Omit` carve out exact subsets; `Partial` and `Required` flip optionality; `Readonly`

Video: 059 · You are a Junior Dev if You Don’t Know These 18 TypeScript Utility Types · watch · ~22:09 · Web Dev Simplified

Source: this is a written companion to the video. Part of the Favorite TypeScript Videos curated path.

The video works through all 18 utility types against one base User type (id, name, age, address), motivated by three functions that each want a different slice of it: createUser wants everything except id, updateUser wants any subset, and renderUserDetails wants only name and age. Pick and Omit carve out exact subsets; Partial and Required flip optionality; Readonly freezes one level; Record builds keyed objects; Extract/Exclude narrow unions; ReturnType/Parameters/ConstructorParameters derive types from functions and classes; NonNullable strips null/undefined; Awaited unwraps promises; and four string types (Uppercase/Lowercase/Capitalize/ Uncapitalize) rewrite casing. The refrain throughout: derive every type from one base so a single change propagates everywhere. A light/dark-mode cheat sheet with all examples is linked in the description.

The setup — one base type, three mismatched consumers

type User = {
	id: string;
	name: string;
	age: number;
	address: { street: string; city: string };
};

Creating a user should not require an id — the database generates that. Updating a user should not require every property. And rendering user details only needs name and age, yet the signature demands a full User.

Pick and Omit — exact subsets

function renderUserDetails(user: Pick<User, "name" | "age">) {
	/* ... */
}
function createUser(user: Omit<User, "id">) {
	/* ... */
}

Pick takes the source object type plus a union of keys to keep — Pick<User, "name" | "age"> is the name-and-age subset. Omit is the exact opposite: Omit<User, "id"> is every user property except the ID. Hand-writing the subset also works, but breaks the single-source rule — change name or age on the base later and a derived Pick follows automatically.

That is the whole idea behind utility types: one base type, with every consumer deriving from it so a single change propagates everywhere.

Partial and Required — flipping optionality

function updateUser(user: Partial<User>) {
	/* any subset of fields */
}
function createUserWithAddress(user: Required<User>) {
	/* address mandatory */
}

Partial<User> takes one generic parameter and makes every key optional — pass as many or as few fields as wanted. Required does the exact opposite, forcing every normally-optional property to be passed — demonstrated by making an optional address mandatory.

Readonly — shallow immutability

The JS parallel is Object.freeze, which locks an object against modification. Readonly<User> prepends readonly to every property — but only one level deep, so address.street stays writable while id, name, age, and address itself cannot be reassigned.

Record — objects with fixed or open keys

type T = Record<string, User>; // any string key → User
type Roles = Record<"admin" | "user", { test: string }>; // forced keys
type Open = Record<PropertyKey, { test: string }>; // any valid key

Record takes keys first, value type second. With a union of keys the object always has both — omitting admin or user errors; PropertyKey covers anything valid as a JavaScript key.

Extract and Exclude — narrowing unions

type Role = "admin" | "user" | "moderator";
type T1 = Extract<Role, "admin" | "moderator">; // "admin" | "moderator"
type T2 = Exclude<Role, "user">; // "admin" | "moderator"

Extract keeps only members present in both unions — their intersection. Exclude is the opposite, removing every member named in the second union; the everyday form passes literal members to drop, handy for enum-adjacent unions.

ReturnType and Parameters — types from functions

function getUser(id: string) {
	return { name: "Kyle", id };
}
type T = ReturnType<typeof getUser>; // { name: string; id: string }
type P = Parameters<typeof getUser>; // [id: string]

Since getUser is a value, not a type, typeof getUser recovers its type for ReturnType. Adding a property (e.g. age) to the returned object propagates automatically. Parameters returns a tuple of all parameters — [string, number] for (id: string, age: number). The killer use is wrappers: a getUserWrapper re-passing id links the two signatures, so deriving the outer param via Parameters<typeof getUser>[0] keeps them in sync. The headline ReturnType case is libraries: when a library function returns a type it never exports, ReturnType recovers it for use in calling code.

ConstructorParameters (and why InstanceType is pointless here)

Parameters rejects constructors, so classes get ConstructorParameters — the same idea, but working on new. InstanceType on a class is noted only to dismiss: it returns exactly the class’s own type, so there is no point writing it out.

NonNullable vs Required

type T = NonNullable<string | null | undefined>; // string

NonNullable takes one type and strips null and undefined from it. It differs from Required: Required drops the ? but keeps an explicit | null — it forces presence, not non-nullability. To remove nullability itself, use NonNullable.

Awaited — unwrapping promises

async function getUser() {
	return Promise.resolve({ name: "Kyle", id: "1" });
}
type T = Awaited<ReturnType<typeof getUser>>; // { name: string; id: string }, not Promise<...>

Awaited takes a single type and strips every level of promise nesting, leaving whatever the promise resolves to. The standard pattern is Awaited<ReturnType<...>> on async functions — usually the resolved value is what matters, not the promise.

String transforms — Uppercase/Lowercase/Capitalize/Uncapitalize

Lowercase/Uppercase rewrite the whole literal; Uncapitalize/Capitalize rewrite only the first letter. Seemingly useless until metaprogramming — converting PascalCase type names into camelCase keys is the perfect use case.

still from You are a Junior Dev if You Don’t Know These 18 TypeScript Utility Types

still from You are a Junior Dev if You Don’t Know These 18 TypeScript Utility Types

still from You are a Junior Dev if You Don’t Know These 18 TypeScript Utility Types

still from You are a Junior Dev if You Don’t Know These 18 TypeScript Utility Types

Transcript corrections applied above: ID (not IED); pass a Partial (not parse a partial); Readonly utility type (not read only property / read only type); boolean (not booin); enums (not enumbs); prepends readonly (not prepins read only); strip out (not strap out); PropertyKey (not property key (fancy type)).

Prev: 058 — Introduction To React #2 | JSX · Next: 060 — Learn Advanced TypeScript In 25 Minutes