Yuzhe's Blog

yuzhes

PickByType

PickByType

Challenge Link

Challenge

From T, pick a set of properties whose type are assignable to U.

type OnlyBoolean = PickByType<{
  name: string
  count: number
  isReadonly: boolean
  isEnable: boolean
}, boolean>
// expected: { isReadonly: boolean; isEnable: boolean }

Solution

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K]
}

A key remapping mapped type: iterate over every key in T, and remap any key whose value type doesn’t extend U to never (which drops the property entirely).

Breaking It Down

Key remapping with as

The as clause in a mapped type lets you transform the key:

type M = {
  [K in keyof T as <new key expression>]: T[K]
}

When the expression produces never, TypeScript omits that property from the result. This is the idiomatic way to filter properties by any predicate.

The filter predicate

as T[K] extends U ? K : never

For each key K:

With T = { name: string; count: number; isReadonly: boolean; isEnable: boolean } and U = boolean:

KeyT[K]Extends boolean?Result
namestringdropped
countnumberdropped
isReadonlybooleankept
isEnablebooleankept

Deep Dive

Alternative: Pick + Extract approach

Before key remapping was available (pre TS 4.1), the pattern was:

type PickByType<T, U> = Pick<T, {
  [K in keyof T]: T[K] extends U ? K : never
}[keyof T]>

Step 1 — Build an intermediate type where each key maps to itself if the value type matches, or never if not:

{ name: never; count: never; isReadonly: 'isReadonly'; isEnable: 'isEnable' }

Step 2 — Index with [keyof T] to create a union of all values, which automatically collapses never:

never | never | 'isReadonly' | 'isEnable'
// = 'isReadonly' | 'isEnable'

Step 3 — Pass that union to Pick to extract the matching properties.

This is more verbose but works on TS < 4.1 and helps illustrate why never is useful for filtering unions.

Alternative: Conditional + Intersection

type PickByType<T, U> = {
  [K in keyof T as T[K] extends U ? K : never]: T[K] extends U ? T[K] : never
}

Redundant filtering in the value position, but sometimes used for clarity. Stick with the clean version.

Comparison with OmitByType

The mirror challenge 2852-OmitByType drops properties whose type matches U:

// PickByType — keep matching
[K in keyof T as T[K] extends U ? K : never]: T[K]

// OmitByType — drop matching
[K in keyof T as T[K] extends U ? never : K]: T[K]

Just flip the ternary branches. Remembering this relationship makes both challenges trivial.

Variance considerations

extends U is a subtype check, not an equality check. If you want exact type matching:

type PickByType<T, U> = {
  [K in keyof T as [T[K]] extends [U] ? [U] extends [T[K]] ? K : never : never]: T[K]
}

The double-extends pattern [A] extends [B] ? [B] extends [A] ? ... approximates type equality by checking both directions (wrapped in a tuple to prevent distribution). For most use cases the simple extends is sufficient.

Key Takeaways