The Problem
When using Object.entries()
in TypeScript, we lose the relationship between keys and their value types.
For example:
type User = {
age: number;
name: string;
active: boolean;
}
// TypeScript gives us [string, any][]
// We want [('age' | 'name' | 'active'), (number | string | boolean)][]
const entries = Object.entries(user)
But even worse, we lose the specific relationships. We can't be sure that when we have the 'age' key, we get a number.
The Solution
We can create a type that preserves these relationships:
type Entries<T> = Array
{
[K in keyof T]: [K, T[K]]
}[keyof T]
>
Let's break down how this works:
[K in keyof T]
- Maps over each key in T[K, T[K]]
- Creates a tuple of the key and its value type[keyof T]
- Indexes into the mapped type to create a unionArray<...>
- Makes it an array of these tuples
Using It
type User = {
age: number;
name: string;
active: boolean;
}
// Now TypeScript knows:
// If key is 'age', value is number
// If key is 'name', value is string
// If key is 'active', value is boolean
const entries = Object.entries(user) as Entries<User>
for (const [key, value] of entries) {
if (key === 'age') {
// TypeScript knows value is number
console.log(value + 1)
}
if (key === 'name') {
// TypeScript knows value is string
console.log(value.toUpperCase())
}
}
Why This Matters
This pattern is particularly useful when:
Building type-safe APIs
Working with configuration objects
Processing data with specific key-value relationships
Creating generic utility functions
The Power of Mapped Types
This solution showcases several advanced TypeScript features:
Mapped types for transforming each property
Tuple types for key-value pairs
Indexed access types to get value types
Array types with unions
By combining these features, we maintain type safety while working with object entries.