Tuple to Object looks almost too small to be interesting, but it introduces a pattern that shows up again and again in TypeScript: take a union of literal types and feed that union into a mapped type.
If you have already worked through how to implement Pick in TypeScript and how to implement Readonly in TypeScript, this challenge is a useful next step. Those problems iterate over object keys. This one starts from a tuple, extracts its element types, and then rebuilds an object from those values.
In this article, we will answer four practical questions:
- What is the challenge actually asking for?
- Why does
T[number]turn a tuple into a union of its element types? - Why does the constraint need to be
readonly PropertyKey[]? - Why does
as constmatter so much for the final result?
1. Start with what the challenge is really checking
The original example already shows the whole goal:
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type result = TupleToObject<typeof tuple>
// expected {
// tesla: 'tesla'
// 'model 3': 'model 3'
// 'model X': 'model X'
// 'model Y': 'model Y'
// }
Every tuple item becomes both the key and the value in the resulting object type.
So this is not "convert an array into some object shape." It is more specific:
- take every element from the tuple
- use that element as an object key
- use that same element again as the value type
The tests make the target clearer because they also cover numbers, symbol, and an invalid case:
const tupleNumber = [1, 2, 3, 4] as const
const sym1 = Symbol(1)
const sym2 = Symbol(2)
const tupleSymbol = [sym1, sym2] as const
const tupleMix = [1, '2', 3, '4', sym1] as const
type cases = [
TupleToObject<typeof tupleNumber>,
TupleToObject<typeof tupleSymbol>,
TupleToObject<typeof tupleMix>,
]
// @ts-expect-error
type error = TupleToObject<[[1, 2], {}]>
That tells us something important about the valid key space up front:
- strings are allowed
- numbers are allowed
symbolvalues are allowed- arrays and objects are not allowed
That is exactly the set of types represented by PropertyKey.
2. The full solution is only one line
Here is the whole implementation:
type TupleToObject<T extends readonly PropertyKey[]> = {
[K in T[number]]: K
}
The code is short because two TypeScript features do almost all of the work:
T[number]extracts every element type from the tuple[K in T[number]]: Kmaps each extracted element into a property
If those two pieces click, the whole challenge becomes straightforward.
3. T[number] is the step that unlocks the problem
This part is the real heart of the challenge:
T[number]
When T is a tuple type, indexing it with number means "give me the union of every element type in this tuple."
For example:
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type CarTuple = typeof tuple
CarTuple is not string[].
Because of as const, it is the readonly tuple:
readonly ['tesla', 'model 3', 'model X', 'model Y']
Now apply indexed access:
type Keys = CarTuple[number]
and you get:
type Keys = 'tesla' | 'model 3' | 'model X' | 'model Y'
That union is exactly what we need. Once the tuple elements have been turned into a union, the rest of the problem becomes a standard mapped type.
4. How the mapped type turns that union into an object
Now look at the second half of the solution:
[K in T[number]]: K
Read it like this:
- iterate over every member of the union produced by
T[number] - create a property whose key is that member
- make the value type the same member again
So for this tuple:
const tuple = ['tesla', 'model 3', 'model X', 'model Y'] as const
type Result = TupleToObject<typeof tuple>
the mapped type conceptually expands to:
type Result = {
tesla: 'tesla'
'model 3': 'model 3'
'model X': 'model X'
'model Y': 'model Y'
}
The same logic works for numeric literals too:
const tupleNumber = [1, 2, 3, 4] as const
type NumberObject = TupleToObject<typeof tupleNumber>
which becomes:
type NumberObject = {
1: 1
2: 2
3: 3
4: 4
}
And it also works for symbol literals:
const sym1 = Symbol(1)
const sym2 = Symbol(2)
const tupleSymbol = [sym1, sym2] as const
type SymbolObject = TupleToObject<typeof tupleSymbol>
which becomes:
type SymbolObject = {
[sym1]: typeof sym1
[sym2]: typeof sym2
}
So the challenge is really just one clean pipeline:
- tuple
- union of tuple element types via
T[number] - mapped type over that union
- final object where each element is both key and value
5. Why T must extend readonly PropertyKey[]
This generic constraint is doing two jobs at once:
type TupleToObject<T extends readonly PropertyKey[]> = {
[K in T[number]]: K
}
First, PropertyKey restricts the tuple elements to valid object-key types:
type PropertyKey = string | number | symbol
That is why this error case is rejected:
type error = TupleToObject<[[1, 2], {}]>
[1, 2] is not a valid object key type, and {} is not a valid object key type either, so the constraint correctly blocks them.
Second, the readonly part matters because as const produces a readonly tuple:
const tuple = ['tesla', 'model 3'] as const
The type of tuple is:
readonly ['tesla', 'model 3']
If the generic were written like this instead:
type TupleToObject<T extends PropertyKey[]> = {
[K in T[number]]: K
}
then typeof tuple would not satisfy the constraint, because a readonly tuple is not assignable to a mutable array type.
So readonly PropertyKey[] is not just a stylistic improvement.
It matches the actual input shape that the challenge uses.
6. Why as const matters more than many beginners expect
Without as const, the tuple in the example does not stay a literal tuple type:
const tuple = ['tesla', 'model 3', 'model X', 'model Y']
Now typeof tuple becomes string[], not:
readonly ['tesla', 'model 3', 'model X', 'model Y']
That changes everything.
If T is string[], then:
T[number]
is just string, not the literal union of each individual item.
So the result becomes a broad string-keyed object type rather than the precise object the challenge wants.
That is why as const matters here.
It preserves:
- literal element values
- tuple length
- readonly tuple behavior
All three are useful, but the literal element types are the most important part for this challenge.
7. Two common mistakes are worth avoiding
Using keyof T instead of T[number]
If you iterate over keyof T, you are no longer iterating over the tuple values.
You are iterating over tuple indices and array members, which is not what the challenge asks for.
This problem wants the element values from the tuple, not the positions of those elements.
Forgetting that duplicate tuple values collapse in the final object
Because T[number] produces a union, duplicate values do not stay duplicated:
type Keys = ['a', 'a'][number] // 'a'
And because object types cannot have two separate properties with the same key, duplicates naturally collapse into one property in the final mapped type.
That behavior is usually fine, but it is helpful to remember that the intermediate step is a union, not a list.
8. One line of code, one reusable mental model
The final answer is still:
type TupleToObject<T extends readonly PropertyKey[]> = {
[K in T[number]]: K
}
But behind that one line are three ideas that show up everywhere in TypeScript:
- tuples can preserve literal values when you use
as const T[number]turns those tuple element types into a union- mapped types can rebuild an object from that union
Once that mental model is clear, Tuple to Object stops feeling like a trick question and starts feeling like a natural combination of indexed access types and mapped types.