Abhishek Chadha

TypeScript: Extract and Partial

ProgrammingTypeScript

February 8, 2026 · 8 months ago

My OCD compels me to remove all TypeScript squiggles from my code.

Two utilities that regularly help me realize typechecking nirvana are Extract and Partial. They solve different problems, but both let us derive types from types we already have instead of duplicating code and hoping it remains synchronized.

Extracting part of a union

Suppose an API returns one of two responses:

example.ts
1type Response = 2 | { 3 error: null; 4 data: { 5 name: string; 6 age: number; 7 }; 8 } 9 | { 10 error: string; 11 };

A successful response might look like this:

example.ts
1{ 2 error: null, 3 data: { 4 name: "John", 5 age: 30, 6 }, 7}

An unsuccessful response might look like this:

example.ts
1{ 2 error: "Something went wrong", 3}

Because Response is a union, it represents both possibilities. Sometimes, however, we need the type of only one branch.

That is what Extract is for:

example.ts
1type SuccessResponse = Extract<Response, { error: null }>; 2type ErrorResponse = Extract<Response, { error: string }>;

SuccessResponse is now equivalent to:

example.ts
1type SuccessResponse = { 2 error: null; 3 data: { 4 name: string; 5 age: number; 6 }; 7};

And ErrorResponse is equivalent to:

example.ts
1type ErrorResponse = { 2 error: string; 3};

The first argument to Extract is the union we want to search. The second argument describes the members we want to retain.

Conceptually, TypeScript checks every member of the union:

example.ts
1type Extract<T, U> = T extends U ? T : never;

Members assignable to U are kept. Everything else becomes never and disappears from the resulting union.

Extract matches types, not example values

It is tempting to write this:

example.ts
1type ErrorResponse = Extract<Response, { error: "Something went wrong" }>;

But that produces never.

Our error branch contains error: string. It promises that the property can contain any string, so the entire branch is not assignable to a type requiring one particular string literal.

Use { error: string } when the API can return arbitrary error messages:

example.ts
1type ErrorResponse = Extract<Response, { error: string }>;

If the API has a fixed set of errors, model those errors as literals:

example.ts
1type Response = 2 | { 3 error: null; 4 data: { 5 name: string; 6 age: number; 7 }; 8 } 9 | { 10 error: "NOT_FOUND" | "UNAUTHORIZED"; 11 }; 12 13type NotFoundResponse = Extract<Response, { error: "NOT_FOUND" }>;

There is a subtlety here: the error branch above combines two error values into a single union member. To extract each error independently, give each one its own member:

example.ts
1type Response = 2 | { 3 error: null; 4 data: { 5 name: string; 6 age: number; 7 }; 8 } 9 | { 10 error: "NOT_FOUND"; 11 } 12 | { 13 error: "UNAUTHORIZED"; 14 }; 15 16type NotFoundResponse = Extract<Response, { error: "NOT_FOUND" }>;

Now NotFoundResponse resolves to exactly the branch we expect.

Narrowing the response at runtime

Extract is useful when declaring related types, but we often do not need it merely to access a response. TypeScript can narrow a discriminated union based on a runtime check:

example.ts
1function handleResponse(response: Response) { 2 if (response.error === null) { 3 console.log(response.data.name); 4 return; 5 } 6 7console.error(response.error); 8}

Inside the first branch, TypeScript knows that response is the successful response. After that branch returns, it knows that the remaining response is the error case.

The important part is that both members share a property whose possible values distinguish them. Here, that property is error.

This works best with strictNullChecks enabled. Without it, null is assignable much more broadly and is less useful as a discriminator.

Making properties optional with Partial

Consider the data returned by the successful response:

example.ts
1type User = { 2 name: string; 3 age: number; 4};

If we are building an update endpoint, callers might update only one field:

example.ts
1updateUser({ 2 name: "Jane", 3});

We could create another type manually:

example.ts
1type UserUpdate = { 2 name?: string; 3 age?: number; 4};

That works, but it duplicates the properties in User. Every time User changes, we must remember to update UserUpdate too.

Partial derives the same type for us:

example.ts
1type UserUpdate = Partial<User>;

It is equivalent to:

example.ts
1type UserUpdate = { 2 name?: string; 3 age?: number; 4};

Its implementation is roughly:

example.ts
1type Partial<T> = { 2 [Property in keyof T]?: T[Property]; 3};

TypeScript iterates over every property in T and marks it as optional.

We can now use it in our update function:

example.ts
1function updateUser(update: Partial<User>) { 2 // Send only the supplied properties to the API. 3} 4 5updateUser({ name: "Jane" }); 6updateUser({ age: 31 }); 7updateUser({});

We still get useful errors for unknown properties or incorrect values:

example.ts
1updateUser({ email: "jane@example.com" }); 2// ^^^^^ Property does not exist 3 4updateUser({ age: "thirty-one" }); 5// ^^^ Type 'string' is not assignable to type 'number'

Partial relaxes which properties are required. It does not relax the type of each property.

Partial is shallow

Now suppose our user contains nested properties:

example.ts
1type User = { 2 name: string; 3 profile: { 4 age: number; 5 address: { 6 city: string; 7 country: string; 8 }; 9 }; 10};

Applying Partial makes only the top-level properties optional:

example.ts
1type UserUpdate = Partial<User>;

Conceptually, the result is:

example.ts
1type UserUpdate = { 2 name?: string; 3 profile?: { 4 age: number; 5 address: { 6 city: string; 7 country: string; 8 }; 9 }; 10};

profile may be omitted, but if it is supplied, all of its original properties are still required:

example.ts
1const update: Partial<User> = { 2 profile: { 3 age: 31, 4 // Error: address is missing 5 }, 6};

This is intentional. Partial changes one level of a type and stops there.

Creating DeepPartial

If an update can target values at any depth, we can define a recursive DeepPartial:

example.ts
1type DeepPartial<T> = { 2 [Property in keyof T]?: T[Property] extends object 3 ? DeepPartial<T[Property]> 4 : T[Property]; 5};

Now nested updates work:

example.ts
1const update: DeepPartial<User> = { 2 profile: { 3 address: { 4 city: "Delhi", 5 }, 6 }, 7};

At each level, DeepPartial checks whether a property is an object. If it is, the utility applies itself recursively. Otherwise, it leaves the value type unchanged.

The naïve DeepPartial has limits

In TypeScript, object includes more than plain objects. Arrays, functions, maps, sets, and dates are objects too.

That means the short implementation can produce surprising types for values such as:

example.ts
1type Example = { 2 createdAt: Date; 3 tags: string[]; 4};

A more practical version can handle arrays separately and leave functions alone:

example.ts
1type DeepPartial<T> = T extends (...args: any[]) => unknown 2 ? T 3 : T extends readonly (infer Item)[] 4 ? readonly DeepPartial<Item>[] 5 : T extends object 6 ? { 7 [Property in keyof T]?: DeepPartial<T[Property]>; 8 } 9 : T;

Whether that is the correct implementation depends on the application.

For example, should a Date remain a Date? Should arrays be replaced as a whole, or should their elements become partial? Should Map and Set receive special handling?

There is no universal definition of “deep partial.” The type should reflect the update behavior of the system that consumes it.

For a small object made from JSON-like data, the simple version is often sufficient:

example.ts
1type DeepPartial<T> = { 2 [Property in keyof T]?: T[Property] extends object 3 ? DeepPartial<T[Property]> 4 : T[Property]; 5};

For a reusable library, the edge cases deserve more attention.

Combining Extract and Partial

Utility types become especially useful when we compose them.

Starting with the API response:

example.ts
1type Response = 2 | { 3 error: null; 4 data: { 5 name: string; 6 age: number; 7 }; 8 } 9 | { 10 error: string; 11 };

We can extract the successful branch:

example.ts
1type SuccessResponse = Extract<Response, { error: null }>;

Then retrieve its data type:

example.ts
1type User = SuccessResponse["data"];

And derive an update type from it:

example.ts
1type UserUpdate = Partial<User>;

The complete chain is:

example.ts
1type SuccessResponse = Extract<Response, { error: null }>; 2type User = SuccessResponse["data"]; 3type UserUpdate = Partial<User>;

Now UserUpdate always follows the API’s successful data shape. If the API response changes, we do not have to hunt down a separately maintained copy.

We can also write the composition directly:

example.ts
1type UserUpdate = Partial<Extract<Response, { error: null }>["data"]>;

I usually prefer the intermediate names. They are easier to read, inspect in an editor, and reuse elsewhere. Fewer lines are not always fewer problems.

A note about naming API responses

Response is also the name of the built-in Fetch API response type. Depending on where the type is declared, using the same name can be confusing or cause a collision.

A more specific name is usually clearer:

example.ts
1type GetUserResponse = 2 | { 3 error: null; 4 data: { 5 name: string; 6 age: number; 7 }; 8 } 9 | { 10 error: string; 11 }; 12 13type GetUserSuccess = Extract<GetUserResponse, { error: null }>; 14type GetUserError = Extract<GetUserResponse, { error: string }>;

Specific names cost a few extra characters and save quite a bit of guessing.

The general idea

Extract selects members from a union:

example.ts
1type Selected = Extract<Union, MatchingShape>;

Partial makes every top-level property optional:

example.ts
1type OptionalProperties = Partial<ObjectType>;

DeepPartial is a custom recursive type for cases where nested properties must also become optional:

example.ts
1type DeepPartial<T> = { 2 [Property in keyof T]?: T[Property] extends object 3 ? DeepPartial<T[Property]> 4 : T[Property]; 5};

The larger lesson is to derive types instead of repeating them.

Duplicated types slowly drift apart. Derived types cannot forget what they came from—and that means fewer TypeScript squiggles without pretending the errors do not exist.

One caveat: recursive utilities like DeepPartial can get expensive for the type checker. Declare the resulting type once—type UserUpdate = DeepPartial<User>—and reuse that alias, instead of writing DeepPartial<User> inline at every call site. TypeScript then expands the recursion once and reuses the result.