Model Types and Collections

TypeScript Pick vs Extract: Select Properties or Union Variants

Learn when Pick selects properties, when Extract retains union variants, and how to combine them for variant-specific fields.

Editorial illustration for TypeScript Pick vs Extract on Unions: Select Keys or Variants?

If you need a few properties, use Pick. If you need a particular member of a union, use Extract. The distinction matters when an object union has fields that exist only on some members: selecting a field and selecting the member that owns it are two different operations.

Consider two application events. Both have a kind, but their payloads differ:

type Event =
  | { kind: "click"; x: number; y: number }
  | { kind: "key"; key: string };

type EventKind = Pick<Event, "kind">;
// { kind: "click" | "key" }

type ClickEvent = Extract<Event, { kind: "click" }>;
// { kind: "click"; x: number; y: number }

const eventKind: EventKind = { kind: "key" }; // Valid
// const badKind: EventKind = { kind: "submit" }; // Invalid: not an Event kind

const click: ClickEvent = { kind: "click", x: 12, y: 20 }; // Valid
// const incompleteClick: ClickEvent = { kind: "click", x: 12 };
// Invalid: the retained click variant also requires y

EventKind is one property-shaped type. Its kind can have either value, but it has no payload fields. In particular, { kind: "click" } fits EventKind; it does not fit ClickEvent. Extract retains the matching union member rather than building a new object with just the fields in its second argument. This is the practical difference between Pick and Extract for a discriminated union. See the Pick and Extract examples for the property-versus-member distinction.

Select the variant before selecting its fields

Suppose a caller needs only the click event’s kind and x coordinate. It is tempting to write Pick<Event, "x">, but x is not a key available across Event: the key event has key instead. Ordinary Pick on a union does not first pick a branch for you. Examples of Pick over union types show both the union-valued property result and the rejected non-common key.

First retain the click branch; then project its properties:

// type XOnly = Pick<Event, "x">;
// Invalid: x is not a key shared by the Event members

type ClickLocation = Pick<
  Extract<Event, { kind: "click" }>,
  "kind" | "x"
>;
// { kind: "click"; x: number }

const location: ClickLocation = { kind: "click", x: 12 }; // Valid
// const wrongLocation: ClickLocation = { kind: "key", x: 12 };
// Invalid: ClickLocation requires kind: "click"

The order expresses the request precisely. Extract answers which event?; Pick then answers which fields of that event? ClickLocation does not require y, while ClickEvent does. This is useful when a function needs a smaller typed view of a known variant, without defining a second, independent copy of those field types.

The Extract target is a test, not the result

Extract<T, U> tests each union member of T for assignability to U and keeps the members that match. Its target shape need not list every field you want to retain. { kind: "click" } matches the click member above even though that member also has x and y; those fields remain in the result. The conditional-type definition of Extract makes that behavior visible: a matching member returns itself, while a nonmatching member becomes never.

A target can also match more than one member. With this two-member union, a target that accepts either discriminator retains both complete events:

type ClickOrKey = Extract<Event, { kind: "click" | "key" }>;
// The full click | key union, including each member's payload

const keyEvent: ClickOrKey = { kind: "key", key: "Enter" }; // Valid
// const missingKey: ClickOrKey = { kind: "key" };
// Invalid: the retained key variant still requires key

That is why the second argument should describe the members you intend to match, not the output fields you hope to see. If your requested result is “all events matching this discriminator,” choose Extract. If it is “these properties from an already selected event,” choose Pick.

Types do not reshape incoming objects

Neither operation removes fields from a JavaScript object. Assigning click to a variable typed as ClickLocation would not delete its y property; the narrower type only describes which fields that variable exposes to static checking. Likewise, declaring external data to be ClickEvent does not check that an incoming value actually has numeric x and y fields. At an external-data boundary, perform runtime checks before relying on those fields. The type utilities help describe the result you want the checker to work with; they do not perform that check for you.

Find a note

Search by topic, title, or keyword.