Append to Object
Append to Object
Challenge
Implement a type that adds a new field to the interface. The type takes three arguments. The output should be an object with the new field.
type Test = { id: '1' }
type Result = AppendToObject<Test, 'value', 4> // expected to be { id: '1', value: 4 }Solution
type AppendToObject<T, U extends string, V> = {
[K in keyof T | U]: K extends keyof T ? T[K] : V
}The key idea: iterate over the union of existing keys and the new key, then decide each property's value with a conditional type.
Breaking it down
Step 1 — Union the key sets: keyof T | U
keyof T gives us all the existing keys of T. U is the new key we want to add. Their union means the mapped type will cover every property — old ones and the new one.
Step 2 — Resolve the value for each key
K extends keyof T ? T[K] : V- If
Kis already a key inT→ preserve the original value typeT[K] - Otherwise (i.e.,
K === U) → it's the new field, so give it typeV
Result: A flat object type containing all original properties plus the new U: V entry.
Why not just T & { [P in U]: V }?
The intersection approach works but produces an intersection type rather than a flat object:
type AppendToObject<T, U extends string, V> = T & { [P in U]: V }
// Result type: { id: '1' } & { value: 4 }TypeScript will structurally treat this the same, but it displays as an intersection in IDE tooltips rather than a clean merged object. The mapped-type solution produces a single, flat object type that's easier to read and reason about.
Deep Dive
The constraint U extends string
Object keys in TypeScript can be string, number, or symbol. Here we constrain U extends string to express that our new key must be a string literal type. Without this constraint, TypeScript would complain because keyof T | U requires a valid key type.
In practice this also lets TypeScript narrow U to a specific string literal (e.g., 'value'), which is what makes AppendToObject<Test, 'value', 4> resolve to { id: '1'; value: 4 } rather than { id: '1'; [x: string]: 4 }.
Overwriting existing keys
What if U is already a key in T? The conditional K extends keyof T ? T[K] : V would always take the first branch, keeping the original type — U would not be overwritten. If you want the new value to win (like Merge), flip the condition:
type AppendOrOverwrite<T, U extends string, V> = {
[K in keyof T | U]: K extends U ? V : K extends keyof T ? T[K] : never
}Relationship to Merge
AppendToObject is essentially a specialised Merge where the second "type" is a single-entry object { [U]: V }:
type AppendToObject<T, U extends string, V> = Merge<T, { [P in U]: V }>Understanding Merge first makes AppendToObject trivially easy — they're the same pattern.
Homomorphic vs non-homomorphic mapped types
The mapped type here is non-homomorphic: it doesn't directly map over keyof T alone, so modifiers like readonly and ? from T are not preserved automatically.
type Source = { readonly id: string; name?: string }
type Result = AppendToObject<Source, 'age', number>
// { id: string; name: string | undefined; age: number }
// ^ readonly and ? are lostTo preserve them you'd need to split the mapped type into two homomorphic parts and intersect (or use the flat trick):
type AppendToObject<T, U extends string, V> =
{ [K in keyof T]: T[K] } & { [P in U]: V }For most use cases (and for this challenge) the simple version is sufficient.
Key Takeaways
keyof T | Uin a mapped type is the cleanest way to "add one key" to an existing object type- A conditional type on the key (
K extends keyof T ? T[K] : V) elegantly distinguishes existing vs new properties - The intersection shorthand (
T & { [P in U]: V }) is equivalent but produces a non-flat type U extends stringis necessary to keepUas a valid mapped-type keyAppendToObjectis a degenerate case ofMerge— the second operand is just a single-property object
