Type a pick(obj).name.age() chain where every step narrows what is left. An accumulator type parameter remembers the picked keys so none can be picked twice.
pick(obj) hands you a value that is two things at once: an object you can keep reaching into, and a function you can call to finish.
Each property you touch is one of the object's keys, and touching it takes that key out of circulation for the rest of the chain. Calling the result with () returns an object made of exactly the keys you visited, in whatever order you visited them. The type has to track that growing set as you go, which is the part the compiler does not do for you.
type Person = {
name: string
id: string
age: number
email: string
}
const obj = {} as Person
const result1 = pick(obj).name.age()
// result1: { name: string, age: number }
const result2 = pick(obj).email.id.name()
// result2: { email: string, id: string, name: string }
const invalid = pick(obj).name.name() // error, name is already taken
const invalid2 = pick(obj).invalid() // error, no such keyImplement a pick function that returns a chainable object allowing you to select properties from an object by chaining property access. When you call the chain with (), it returns an object containing only the picked properties.
The chainable picker should:
()For example:
type Person = {
name: string
id: string
age: number
email: string
}
const obj = {} as Person
// Pick multiple properties by chaining
const result1 = pick(obj).name.age()
// result1: { name: string, age: number }
const result2 = pick(obj).email.id.name()
// result2: { email: string, id: string, name: string }
// You cannot pick the same property twice
const invalid = pick(obj).name.name() // Type error!
// You can only pick existing properties
const invalid2 = pick(obj).invalid() // Type error!View on GitHub: https://tsch.js.org/37790
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type Picker<T> = {}
declare function pick<T>(obj: T): Picker<T>
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type Person = {
name: string
id: string
age: number
email: string
}
type User = {
username: string
password: string
role: 'admin' | 'user'
createdAt: Date
lastLogin: Date
}
const person = {} as Person
const user = {} as User
type cases = [
// Basic single property pick
Expect<Equal<Pick<Person, 'name'>, ReturnType<ReturnType<typeof pick<Person>>['name']>>>,
//Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
The solution in full:
type Picker<T, Picked extends keyof T = never> = {
[K in Exclude<keyof T, Picked>]: Picker<T, Picked | K>
} & (() => Pick<T, Picked>)
declare function pick<T extends Record<string, unknown>>(obj: T): Picker<T>Three lines for the type, one for the function. The interesting part is the second type parameter, which the caller never writes.
pick(obj).name has to be readable as an object (so .age works after it) and callable (so () works after it). A single object type cannot be both, but an intersection can:
[object Object]Anything of type Chain accepts .age and accepts a call. That intersection is the skeleton of the whole solution: the mapped type on the left is what you can still reach for, and the call signature on the right is what you get if you stop here.
Picked is a set of keys carried through the recursion, and it starts empty. The empty union is never, which is why the default is never rather than something like '':
[object Object]Unioning anything with never gives that thing back, so the first step adds a key to an empty set without any special case for the first step.
Two utilities do the bookkeeping. Exclude<keyof T, Picked> lists the keys still available, and Pick<T, Picked> builds the result out of the keys already taken. Evaluate the starting point by hand:
// Picker<Person> is Picker<Person, never>
// Exclude<keyof Person, never> -> 'name' | 'id' | 'age' | 'email'
// Pick<Person, never> -> {}So pick(obj) offers all four keys and, if called right away, returns an empty object. Reach for .name and the mapped type hands back Picker<Person, never | 'name'>:
// Picker<Person, 'name'>
// = { id: ...; age: ...; email: ... } & (() => Pick<Person, 'name'>)name is gone from the property side, and the call signature has grown a key. One more step, .age, and Picked is 'name' | 'age', which is exactly the type the first chained test expects.
Every step moves one key from the Exclude side to the Picked side, and keyof T is finite. After four steps on Person, Exclude<keyof Person, 'name' | 'id' | 'age' | 'email'> is never, a mapped type over never is {}, and the value that is left is only a function. There is nothing more to reach for, so the chain cannot grow further.
Recursion inside a type alias is fine here because the reference to Picker sits in a property position of a mapped type. TypeScript defers that until someone actually indexes into it, so writing Picker<Person> does not expand all twenty-four possible chains at once.
The constraint on pick carries more weight than it looks:
[object Object]T extends object would be too loose, since arrays and functions are objects too. Record<string, unknown> demands a string index signature, and TypeScript grants an implicit one only to object types written as type aliases or literals. Arrays, functions and the primitive wrappers do not get one, so pick([]), pick(() => {}), pick(1) and pick('1') are all rejected, while Person and User pass.
pick(person).name.name() is an error because Exclude already removed name from the property side. The function constituent still contributes the built in Function.name, so the second .name resolves to a string, and calling a string fails as well.pick(person).nonexistent() is an error too, and for a simpler reason: a key that was never in keyof Person never appears in the mapped type in the first place..name.age.id.email() and .name.age.email.id() produce the same type. Picked is a union, union members are unordered, and Pick emits keys in keyof T order, so the visiting order does not leak into the result.Pick<Person, 'name' | 'id' | 'age' | 'email'> exactly, which is why the accumulator has to hold a union of literals rather than, say, a tuple of keys.This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges