#29572•Hard

ObjectValues

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]

Challenge Instructions: ObjectValues

Hard

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).

ChallengeSolution
/* _____________ 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' an

Pro Challenge

Get access to all 200+ challenges, including every medium, hard, and extreme one.

One-time payment, lifetime access. 30-day money-back guarantee.

Detailed Explanation

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.

Distribution, and how to switch it off

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.

From a union to its last member

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.

Peeling the union, one member at a time

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.

Mapping the tuple to values

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.

Edge cases the tests cover

This challenge is originally from here.

Share this challenge

Related Challenges

Learn the Concepts

Become a TypeScript Pro

Track your progress through 200+ hands-on challenges. Free, sign in with GitHub.

Or start solving right away: explore all TypeScript challenges