TypeScript ships a set of generic types that transform other types. They are ordinary generics — nothing magic — and knowing eight or nine of them removes most of the hand-written duplication from a codebase.
The reason to use them is always the same: a derived type cannot fall out of step with the type it came from, and a hand-copied one always eventually does.
Partial and Required
Partial<T> makes every property optional:
type ProductPatch = Partial<Product>;
// { id?: UUID; name?: string; description?: string; … }Which is exactly the shape of a PATCH payload, or of an update function's argument:
function applyEdits(product: Product, edits: Partial<Product>): Product {
return { ...product, ...edits };
}Required<T> is the inverse, stripping every ?. Less common, but
useful for the "after defaults have been applied" version of an options type:
function resolve(opts: RequestOptions): Required<RequestOptions> { /* … */ }Both are shallow. Nested objects keep their original optionality — a deep version has to be written by hand, and lesson 15 shows how.
Pick and Omit
Two sides of the same coin. Pick keeps the keys you name:
type ProductSummary = Pick<Product, 'id' | 'name' | 'imageUrl'>;Omit removes them:
type ProductDraft = Omit<Product, 'id' | 'createdAt' | 'updatedAt'>;The pizza app uses Omit to stop a caller passing options the wrapper has already
decided — and it is the clearest small example of a utility type doing real work:
export const api = {
get: <T>(path: string, opts?: Omit<RequestOptions, 'method' | 'body'>) =>
request<T>(path, { ...opts, method: 'GET' }),RequestOptions has method, body, auth and
signal. api.get fixes the first two, so its opts parameter is
the remaining two. Passing { method: 'POST' } to api.get is a compile
error, which is the correct outcome and would have been impossible to express with a hand-written
interface that did not track RequestOptions.
One difference worth knowing: Pick requires its keys to exist on
T, while Omit does not. Omit<Product, 'nmae'> compiles
and silently removes nothing. If a rename should break your derived types, Pick is the
safer of the two.
Record
Builds an object type from a key type and a value type:
fieldErrors(): Record<string, string> {
const result: Record<string, string> = {};That is the open-ended form — any string key — which is the index signature from lesson 6 written more briefly.
The far more interesting form is a closed key type:
const STATUS_LABEL: Record<OrderStatus, string> = {
PENDING_PAYMENT: 'Awaiting payment',
PAID: 'Paid',
PREPARING: 'In the oven',
COMPLETED: 'Completed',
CANCELLED: 'Cancelled',
};Now the object must have exactly those five keys — no more, no fewer. Add
'REFUNDED' to OrderStatus and this fails to compile until somebody writes
the label:
Property 'REFUNDED' is missing in type '{ PENDING_PAYMENT: string; … }'
but required in type 'Record<OrderStatus, string>'.This is the same guarantee the never exhaustiveness check gives a
switch, and it is cheaper — one annotation instead of a default branch. Whenever you
have a lookup keyed by a union, type it this way. The alternative fails silently, rendering a blank
label for the new status.
ReturnType and typeof
ReturnType<F> extracts what a function type returns. Paired with
typeof it lets you derive a type from a value, which is where it earns its
place.
The pizza app's Redux store is the example, and the comment above it makes the argument better than I can:
/*
* Types derived FROM the store rather than declared alongside it, so they can never drift out of
* step with the reducers above. Add a slice and RootState grows automatically.
*/
export type RootState = ReturnType<typeof store.getState>;
export type AppDispatch = typeof store.dispatch;Read RootState from the inside out. store.getState is a function
value; typeof store.getState is its type; ReturnType<…> is what it
returns — which is the shape of the whole store, assembled from the four reducers.
Write that shape by hand and you have two descriptions of the same thing. Add a slice and one of them is wrong, with nothing to tell you.
The pre-typed hooks then hang off those two types:
export const useAppDispatch = useDispatch.withTypes<AppDispatch>();
export const useAppSelector = useSelector.withTypes<RootState>();Related: Parameters<F> gives a function's parameters as a tuple, and
Awaited<T> unwraps a promise — Awaited<ReturnType<typeof
fetchOrders>> is "whatever that async function resolves to", which is a common and
genuinely useful combination.
Exclude, Extract and NonNullable
These three operate on unions rather than objects.
type Settled = Exclude<OrderStatus, 'PENDING_PAYMENT'>;
// 'PAID' | 'PREPARING' | 'COMPLETED' | 'CANCELLED'
type Active = Extract<OrderStatus, 'PAID' | 'PREPARING'>;
// 'PAID' | 'PREPARING'
type Url = NonNullable<Product['imageUrl']>;
// string (was string | null)Exclude removes members, Extract keeps them,
NonNullable drops null and undefined. All three are
conditional types over a union, which is why they distribute member by member — the behaviour
mentioned in lesson 8.
NonNullable is the one you will use most, usually after a narrowing check has
already established the value is there.
Readonly
type FrozenCart = Readonly<CartTotals>;Every property becomes readonly. Shallow, like the others, and compile-time only —
Object.freeze is the runtime version and they are unrelated.
The two string ones
Less used but occasionally exactly right: Uppercase, Lowercase,
Capitalize and Uncapitalize transform string literal types.
type Loud = Uppercase<OrderType>; // 'DELIVERY' | 'CARRYOUT' — already loud
type Event = `on${Capitalize<'select' | 'close'>}`; // 'onSelect' | 'onClose'That second one is a template literal type, and it is how libraries generate typed event-handler prop names from a list of events.
Writing the ones TypeScript does not ship
The built-in set is deliberately small. Three more are common enough that most codebases end up writing them, and each is a short exercise in the machinery from the next lesson.
Prettify — the most useful one nobody tells you about. Intersections
and derived types are printed in error messages exactly as they were computed, so a hover can show
you Omit<Product, 'id'> & { tempId: string } rather than the fields. This
flattens it:
type Prettify<T> = {
[K in keyof T]: T[K];
} & {};It does nothing at all to the type — it maps every key to itself. What it changes is how the compiler displays it, because the result is a fresh object type rather than a record of how it was built. Wrap a confusing derived type in it and the tooltip becomes readable.
DeepPartial — because Partial is shallow, and a nested
draft needs the whole tree optional:
type DeepPartial<T> = {
[K in keyof T]?: T[K] extends object ? DeepPartial<T[K]> : T[K];
};Watch out for what counts as an object: arrays, Date and functions all
do, so a naive version makes the elements of an array optional too. Production versions special-case
those, which is a good illustration of why the built-in set stops where it does.
RequireAtLeastOne — for an options object where several fields are
individually optional but at least one must be present:
type RequireAtLeastOne<T, K extends keyof T = keyof T> =
Omit<T, K> & { [P in K]-?: Required<Pick<T, P>> & Partial<Omit<T, P>> }[K];That one is genuinely hard to read, and it is included as an honest example of the ceiling. If your team cannot maintain a type, a runtime check and a comment may serve better. The line between "expressive" and "write-only" is real, and it is closer than enthusiasm suggests.
A caution about overloading the derived
Derived types have one failure mode worth naming. Chain enough of them and an error message becomes a paragraph, and the person reading it cannot tell which link is wrong:
type Thing = Partial<Omit<Pick<Product, 'name' | 'sizes'>, 'sizes'>>;That is { name?: string }, arrived at by three steps that could have been zero. The
rule of thumb: derive when the source type is likely to change and the derivation expresses a real
relationship — a create payload really is "the entity without its server-assigned fields". Do not
derive as a puzzle.
The list worth memorising
| Type | Does | Reach for it when |
|---|---|---|
Partial<T> | all optional | a patch payload, or a form's draft state |
Required<T> | all required | after defaults are applied |
Readonly<T> | all readonly | a value a function must not mutate |
Pick<T, K> | keeps keys | a summary or list-row shape |
Omit<T, K> | drops keys | a create payload, or fixing options |
Record<K, V> | builds an object | a lookup keyed by a union |
Exclude<T, U> | removes union members | a narrowed subset of a status |
Extract<T, U> | keeps union members | the same, from the other side |
NonNullable<T> | drops null/undefined | after a guard |
ReturnType<F> | a function's result | deriving a type from a value |
Parameters<F> | a function's arguments | wrapping a function |
Awaited<T> | unwraps a promise | with ReturnType, on an async function |
If you take one thing from the table, take Record<SomeUnion, T>. It is the
cheapest way to make "we added a case and forgot to handle it somewhere" into a build failure, and
that class of bug is otherwise found by users.
Where they come from
None of these is built into the compiler. Every one is declared in TypeScript's own library file in a few lines of ordinary type syntax, and you can read them:
type Partial<T> = { [K in keyof T]?: T[K] };
type Required<T> = { [K in keyof T]-?: T[K] };
type Pick<T, K extends keyof T> = { [P in K]: T[P] };
type Exclude<T, U> = T extends U ? never : T;
type Omit<T, K extends keyof never> = Pick<T, Exclude<keyof T, K>>;That last line explains the Pick/Omit asymmetry noted earlier.
Omit is Pick over Exclude<keyof T, K>, and
Exclude silently ignores anything not present — so a misspelt key removes nothing
rather than erroring.
The next lesson takes that syntax apart. The point of showing it here is that there is no magic layer: if the built-in set does not have what you need, you can write it in the same language.
One habit that follows from all of this: when you find yourself typing out a shape that already exists somewhere, stop and ask which utility would derive it. Most of the time one will, and the derived version is the one that still compiles after somebody renames a field.
Next
keyof, Mapped and Conditional Types — the machinery every one of these is built from, and how to write your own.