Turn an object type into a tuple of its value types. The hard part is converting a union of keys into an ordered tuple, which the type system does not hand you.
Object.values is a one-liner at runtime. At the type level it needs three separate tricks.
ObjectValues<T> returns a tuple of the value types of T, in key order. An optional second parameter narrows the selection to a subset of keys. The obstacle is that keyof T gives you a union, unions have no index positions, and a tuple needs them. Getting from one to the other is the whole exercise, and the technique shows up any time you need an ordered list of keys: typed Object.entries, column orders for a table, argument lists derived from a config object.
interface TestCase1 {
a: number
b: string
c: 'this is c'
d: () => void
}
ObjectValues<TestCase1> // [number, string, 'this is c', () => void]
ObjectValues<TestCase1, 'a' | 'b' | 'd'> // [number, string, () => void]Implement a type, ObjectValues, just like Object.values.
It looks like this:
interface TestCase1 {
a: number
b: string
c: 'this is c'
d: () => void
}
ObjectValues<TestCase1> // [number, string, 'this is c', () => void]
ObjectValues<TestCase1, 'a' | 'b' | 'd'> // [number, string, () => void]View on GitHub: https://tsch.js.org/29572
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type ObjectValues<T, K extends keyof T = keyof T> = any[]
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
interface TestCase1 {
a: number
b: string
c: 'this is c'
d: () => void
}
type cases = [
Expect<
Equal<ObjectValues<TestCase1>, [number, string, 'this is c', () => void]>
>,
Expect<
Equal<
ObjectValues<TestCase1, 'a' | 'b' | 'd'>,
[number, string, () => void]
>
>,
// The compiler normalises union order, so 'd' | 'a' is the same type as
// 'a' | 'd' anGet access to all 200+ challenges, including every medium, hard, and extreme one.
One-time payment, lifetime access. 30-day money-back guarantee.
The solution in full:
type UnionToIntersection<U> = (U extends any ? (x: U) => void : never) extends (
x: infer I,
) => void
? I
: never
type LastInUnion<U> =
UnionToIntersection<U extends unknown ? () => U : never> extends () => infer L
? L
: never
type UnionToTuple<U, Acc extends unknown[] = []> = [U] extends [never]
? Acc
: UnionToTuple<Exclude<U, LastInUnion<U>>, [LastInUnion<U>, ...Acc]>
type ValuesAt<T, Keys> = { [I in keyof Keys]: T[Keys[I] & keyof T] }
type ObjectValues<T, K extends keyof T = keyof T> = ValuesAt<T, UnionToTuple<K>>Four types, each doing one job. Read them bottom-up: ObjectValues turns the key union into a tuple, then maps that tuple to values. Everything above is the machinery for the first step.
When a conditional type tests a bare type parameter, TypeScript applies it to each union member separately and unions the results. U extends any ? (x: U) => void : never with U = 'a' | 'b' does not produce one function type. It produces ((x: 'a') => void) | ((x: 'b') => void).
Wrapping the parameter in a tuple suppresses that. [U] extends [never] asks whether the whole union is never, instead of asking each member. That is why UnionToTuple can use it as a termination check: a bare U extends never would distribute over zero members and quietly return never.
UnionToIntersection takes that union of functions and infers a single parameter type out of it. Function parameters are checked contravariantly, so when TypeScript has to satisfy several signatures at once, the inferred parameter has to accept all of them: 'a' & 'b'. A union of inputs comes back as an intersection.
LastInUnion feeds it a union of nullary functions instead, where U sits in return position:
// U = 'a' | 'b' | 'c'
// step 1: (() => 'a') | (() => 'b') | (() => 'c')
// step 2: (() => 'a') & (() => 'b') & (() => 'c')
// step 3: infer L -> 'c'Step 3 is the part worth remembering. An intersection of function types behaves like an overload set, and inference from an overloaded signature picks the last overload. So L is the final member of the union as the compiler stores it.
UnionToTuple now has everything it needs. It takes the last member off the union, prepends it to an accumulator, and recurses on what is left:
// UnionToTuple<'a' | 'b' | 'c'>
// U = 'a' | 'b' | 'c' L = 'c' Acc = ['c']
// U = 'a' | 'b' L = 'b' Acc = ['b', 'c']
// U = 'a' L = 'a' Acc = ['a', 'b', 'c']
// U = never -> ['a', 'b', 'c']The accumulator pattern is what makes this terminate. Each step removes one member with Exclude, so the union shrinks on every call and [U] extends [never] eventually matches. Prepending rather than appending is what keeps the result in the original order, since we are consuming the union from the back.
The last step is ordinary mapped-type work with one wrinkle:
[object Object]Mapping over an array or tuple type is special-cased: you get a tuple back, not an object, and the length is preserved. The & keyof T is there to convince the checker that Keys[I] is a usable index. keyof Keys also ranges over length, map and the rest of the array surface, and TypeScript cannot see on its own that the generic Keys only holds keys of T.
With K defaulting to keyof T, calling ObjectValues<TestCase1> walks every key, and passing a subset walks only those.
K defaults to keyof T, and an interface reports its keys in declaration order, so the result tracks the order of the properties as written.'a' | 'b' | 'd' drops c and keeps the other three positions, which checks that the recursion works on a union that is not keyof T.'d' | 'a' gives [number, () => void], not [() => void, number]. The compiler normalises union members when it builds the union, so the order you type them in is not recoverable later. This is the one real limitation of the approach, and the upstream proposal flags it as an open idea rather than a requirement.This challenge is originally from here.
Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges