#956β€’Hard

DeepPick

A Pick that understands dot paths like friend.family.name, building one nested pick per path and folding them into an intersection via contravariant inference.

Pick stops at the first level. DeepPick<T, Paths> accepts a union of dot-separated path strings like 'friend.family.name' and picks each one out of a nested object, returning an intersection of one small object per path. Splitting and walking the paths is template literal inference plus recursion. Combining the per-path results is the union-to-intersection trick built on contravariant infer positions.

For example:

type obj = {
  name: 'hoge', 
  age: 20,
  friend: {
    name: 'fuga',
    age: 30,
    family: {
      name: 'baz',  
      age: 1 
    }
  }
}
 
type T1 = DeepPick<obj, 'name'>   // { name : 'hoge' }
type T2 = DeepPick<obj, 'name' | 'friend.name'>  // { name : 'hoge' } & { friend: { name: 'fuga' }}
type T3 = DeepPick<obj, 'name' | 'friend.name' |  'friend.family.name'>  // { name : 'hoge' } &  { friend: { name: 'fuga' }} & { friend: { family: { name: 'baz' }}}
 

Challenge Instructions: DeepPick

Hard

Implement a type DeepPick, that extends Utility types Pick. A type takes two arguments.

For example:

type obj = {
name: 'hoge',
age: 20,
friend: {
name: 'fuga',
age: 30,
family: {
name: 'baz',
age: 1
}
}
}
 
type T1 = DeepPick<obj, 'name'>   // { name : 'hoge' }
type T2 = DeepPick<obj, 'name' | 'friend.name'>  // { name : 'hoge' } & { friend: { name: 'fuga' }}
type T3 = DeepPick<obj, 'name' | 'friend.name' |  'friend.family.name'>  // { name : 'hoge' } &  { friend: { name: 'fuga' }} & { friend: { family: { name: 'baz' }}}
 

View on GitHub: https://tsch.js.org/956

Change the following code to make the test cases pass (no type check errors).

πŸ‘‹ Lifetime-License is leaving on August 10, 2026

Get it now for $29

One-time payment. Lifetime access to all pro challenges.

Loading...

Detailed Explanation

Two types, one per sub-problem:

type PickByPath<T, Path> = Path extends `${infer Head}.${infer Rest}`
  ? Head extends keyof T
    ? { [K in Head]: PickByPath<T[Head], Rest> }
    : never
  : Path extends keyof T
    ? { [K in Path]: T[K] }
    : unknown
 
type DeepPick<T, PathUnion> = (
  PathUnion extends string ? (picked: PickByPath<T, PathUnion>) => void : never
) extends (picked: infer Intersection) => void
  ? Intersection
  : never

The problem decomposes into two sub-problems: resolve one path, and combine the results of many paths into an intersection. One helper type each.

Sub-problem 1: resolving a single path

PickByPath<T, Path> handles exactly one path string. The template literal pattern `${infer Head}.${infer Rest}` asks whether the path contains a dot. Inference here is lazy, meaning Head matches the shortest possible prefix, so the string splits at the first dot:

// Path = 'friend.family.name'
// Head = 'friend'
// Rest = 'family.name'

If there is a dot, we check Head extends keyof T and wrap one layer of object around a recursive call: { [K in Head]: PickByPath<T[Head], Rest> }. Each recursion step peels off one path segment and one level of nesting. Tracing 'friend.family.name' against the example object:

PickByPath<obj, 'friend.family.name'>
// β†’ { friend: PickByPath<obj['friend'], 'family.name'> }
// β†’ { friend: { family: PickByPath<..., 'name'> } }
// β†’ { friend: { family: { name: 'baz' } } }

If there's no dot left, we've reached the leaf: Path extends keyof T picks the single property with { [K in Path]: T[K] }, a one-key Pick. And if the leaf segment isn't a key of T at all (the tests probe this with the empty path ''), we return unknown. That is a deliberate choice: unknown is the identity element of intersection (X & unknown is just X), so a bad path contributes nothing to the final result. Note the asymmetry between the two failure branches: a miss in the middle of a dotted path returns never instead, a fallback the tests never actually exercise, since every dotted path they use is valid.

Sub-problem 2: from a union of picks to an intersection

Feeding the whole PathUnion through PickByPath gives you a union like { a: number } | { obj: { e: string } }, but the challenge demands an intersection. Unions can't be converted to intersections by any direct operator; instead the solution uses TypeScript's inference variance rules.

The wrapper works in two moves:

[object Object]

Because PathUnion appears naked to the left of extends, this is a distributive conditional type: it runs once per union member. Each member gets wrapped in a function type, producing a union of functions:

// For 'a' | 'obj.e':
((picked: { a: number }) => void) | ((picked: { obj: { e: string } }) => void)

Then the second conditional infers a single parameter type from that union of functions:

[object Object]

Note which side of extends the union sits on: the left. For the check to succeed, every member of the union must be assignable to the single candidate (picked: infer Intersection) => void. Function parameters are contravariant, so a function that accepts a broader input can safely stand in for one that accepts a narrower input, and each member (picked: A) => void is assignable to the candidate only if Intersection is assignable to A. That forces Intersection to be assignable to each member's parameter type, and TypeScript infers the largest type satisfying all of those constraints at once: the intersection, { a: number } & { obj: { e: string } }. This is the classic UnionToIntersection pattern, inlined.

The function wrapper is not decoration; the contravariant inference is the mechanism. Without wrapping each member in a function parameter, there is no contravariant infer position, and inferring from the union would hand you the union back. The wrapper also buys a bonus: raw unions eagerly simplify ({ a: number } | unknown collapses to just unknown), but tucked into separate function parameters the members never form a naked union, which is why the 'a' | '' test produces { a: number } & unknown instead of plain unknown.

Edge cases the tests cover

Both halves outlive this challenge: template literal splitting shows up in every path-based utility type, and the contravariant fold is the standard way to turn a union into an intersection.

This challenge is originally from here.

Share this challenge

Learn the Concepts