#17908•Medium

Union From Values in an Object

Collect every element of every array held in an object into one union type, using a homomorphic mapped type and an indexed access rather than a utility type.

Config objects full of as const arrays are everywhere, and sooner or later you want the values back as a type.

UnionOfArrayValuesInObject<T> takes an object whose properties are arrays and produces the union of every element in every one of those arrays. One object with one array of three strings gives a three-member union. Two objects of three units each give a union of six. The point of the exercise is that you can get there with indexed access alone, without reaching for a built-in utility type.

const object1WithObjectArrays= {
  key: ['one', 'two', 'three']
} as const
 
const object2WithObjectArrays= {
  key: [1, 2, 3]
} as const
 
const AllMeasurementUnits = {
  weight: ['g', 'kg', 'pound'],
  length: ['cm', 'm', 'inch']
} as const
 
type UnionOfArrayValuesInObject<T> = any
 
const oneString: UnionOfArrayValuesInObject<typeof object1WithObjectArrays> = 'one' //should be a valid type
const fourString: UnionOfArrayValuesInObject<typeof object2WithObjectArrays> = 'four' //shouldn't be a valid type
 
const oneNumber: UnionOfArrayValuesInObject<typeof object2WithObjectArrays> = 1 //should be a valid type
 
const grams: UnionOfArrayValuesInObject<typeof AllMeasurementUnits> = 'g' //should be a valid type
const ounce: UnionOfArrayValuesInObject<typeof AllMeasurementUnits> = 'ounce' //shouldn't be a valid type

Challenge Instructions: Union From Values in an Object

Medium

Create a generalizable type UnionOfArrayValuesInObject that can be used to create a union type of all the values from all the array values inside an object that contains object keys and arrays as the objects.

The task is solvable without using utility types.

const object1WithObjectArrays= {
key: ['one', 'two', 'three']
} as const
 
const object2WithObjectArrays= {
key: [1, 2, 3]
} as const
 
const AllMeasurementUnits = {
weight: ['g', 'kg', 'pound'],
length: ['cm', 'm', 'inch']
} as const
 
type UnionOfArrayValuesInObject<T> = any
 
const oneString: UnionOfArrayValuesInObject<typeof object1WithObjectArrays> = 'one' //should be a valid type
const fourString: UnionOfArrayValuesInObject<typeof object2WithObjectArrays> = 'four' //shouldn't be a valid type
 
const oneNumber: UnionOfArrayValuesInObject<typeof object2WithObjectArrays> = 1 //should be a valid type
 
const grams: UnionOfArrayValuesInObject<typeof AllMeasurementUnits> = 'g' //should be a valid type
const ounce: UnionOfArrayValuesInObject<typeof AllMeasurementUnits> = 'ounce' //shouldn't be a valid type

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

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

ChallengeSolution
/* _____________ Your Code Here _____________ */

const object1WithObjectArrays= {
  key: ['one', 'two', 'three']
} as const

const object2WithObjectArrays= {
  key: [1, 2, 3]
} as const

const AllMeasurementUnits = {
  weight: ['g', 'kg', 'pound'],
  length: ['cm', 'm', 'inch']
} as const

type UnionOfArrayValuesInObject<T> = any

const oneString: UnionOfArrayValuesInObject<typeof object1WithObjectArrays> = 'one' //should be a valid type
const oneNumber: UnionOfArrayValuesInObject<typeof object2WithObjectArrays> = 1 //should be a valid type
const grams: UnionOfArrayValuesInObject<typeof AllMe

Pro Challenge

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

Monthly subscription.
Cancel anytime + 30-day money-back guarantee.

Detailed Explanation

type UnionOfArrayValuesInObject<T> = {
  [K in keyof T]: T[K] extends readonly (infer V)[] ? V : never
}[keyof T]

Two moves stacked on top of each other: rebuild the object with every array flattened into a union, then index that rebuilt object with all of its keys at once.

What the input type actually looks like

as const is doing quiet but essential work here. Without it, weight: ['g', 'kg', 'pound'] widens to string[] and the element types are gone before your type ever sees them. With it, the compiler keeps the literals and marks everything readonly:

// typeof AllMeasurementUnits
// {
//   readonly weight: readonly ['g', 'kg', 'pound']
//   readonly length: readonly ['cm', 'm', 'inch']
// }

That readonly matters for the pattern you match against. A readonly ['g', 'kg', 'pound'] is not assignable to (infer V)[], because a mutable array type promises push and friends. Write the constraint as readonly (infer V)[] and both readonly tuples and ordinary arrays match it.

Flattening one property

T[K] extends readonly (infer V)[] ? V : never asks the compiler to match the property type against "array of something" and name that something V. For a tuple, V is not the first element, it is the union of all of them, because that is the only single type every position satisfies:

// T['weight'] = readonly ['g', 'kg', 'pound']
// V           = 'g' | 'kg' | 'pound'

Run that over every key with a mapped type and you get the same object shape with flattened properties:

// {
//   readonly weight: 'g' | 'kg' | 'pound'
//   readonly length: 'cm' | 'm' | 'inch'
// }

The never branch is the fallback for a property that is not an array at all. It costs nothing in a union, so such properties drop out silently instead of breaking the result.

Collapsing the object into a union

The last step is the one that does the real work:

// Shape = { readonly weight: 'g' | 'kg' | 'pound'; readonly length: 'cm' | 'm' | 'inch' }
// keyof Shape = 'weight' | 'length'
// Shape['weight' | 'length'] = 'g' | 'kg' | 'pound' | 'cm' | 'm' | 'inch'

Indexing an object type with a union of keys returns the union of the matching property types. T[keyof T] is therefore shorthand for "all the values of T", and applying it to the flattened shape merges every per-key union into one.

The shorter version, and why I did not use it

You can skip the mapped type, at the price of a constraint:

type UnionOfArrayValuesInObject<T extends Record<PropertyKey, readonly unknown[]>> =
  T[keyof T][number]

T[keyof T] is already the union of the arrays, and indexing an array type with number gives its element type, so this passes the same three tests. The constraint is not optional: without it the compiler rejects T[keyof T][number] outright with "Type 'number' cannot be used to index type 'T[keyof T]'", because an unconstrained T has no guarantee that its values are indexable by a number. The mapped-type version needs no constraint, keeps non-array properties from being an error, and shows the flattening step, which is the part worth learning.

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