PickByType
PickByType
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 : neverFor each key K:
- If
T[K]is assignable toU→ keepK(passes through unchanged) - Otherwise → remap to
never(drops the property)
With T = { name: string; count: number; isReadonly: boolean; isEnable: boolean } and U = boolean:
| Key | T[K] | Extends boolean? | Result |
|---|---|---|---|
name | string | ❌ | dropped |
count | number | ❌ | dropped |
isReadonly | boolean | ✅ | kept |
isEnable | boolean | ✅ | kept |
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
- Key remapping (
asclause) +neveris the cleanest way to filter object properties by type - The pattern
[K in keyof T as T[K] extends U ? K : never]is the idiomatic "filter by value type" mapped type - The older
Pick<T, { [K in keyof T]: ... }[keyof T]>approach still works pre-TS 4.1 PickByTypeandOmitByTypeare exact mirrors — just swapKandneverin the ternary
