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 typeCreate 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 typeView on GitHub: https://tsch.js.org/17908
Change the following code to make the test cases pass (no type check errors).
/* _____________ 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 AllMeGet access to all 200+ challenges, including every medium, hard, and extreme one.
Monthly subscription.
Cancel anytime + 30-day money-back guarantee.
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.
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.
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.
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.
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.
{ key: ['one', 'two', 'three'] } → 'one' | 'two' | 'three'. A single key, so the final indexed access has only one property type to return.{ key: [1, 2, 3] } → 1 | 2 | 3. Nothing in the type is string-specific; infer V picks up numeric literals just as happily.AllMeasurementUnits → all six units. Two keys, and the union is flat rather than nested, because indexed access with a union of keys already unions the results.'four' or 'ounce' is rejected, which is the half of the behaviour that an any stub would quietly let through.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