Make every property that allows undefined optional, restricted to a chosen key set. Split the object with key remapping, add the ? modifier, merge back.
value: string | undefined and value?: string look interchangeable, but only one of them lets callers omit the property.
That difference is a real API design problem: a property typed value: string | undefined must still be explicitly written at every call site, while value?: string can simply be left out. OptionalUndefined<T, Props> fixes such types by making every property whose type includes undefined optional. A second, optional generic Props restricts which properties may be altered. Solving it teaches you key remapping with as, adding modifiers in mapped types, generic parameter defaults, and the intersection-flattening trick.
OptionalUndefined<{ value: string | undefined, description: string }>
// { value?: string | undefined; description: string }
OptionalUndefined<{ value: string | undefined, description: string | undefined, author: string | undefined }, 'description' | 'author'>
// { value: string | undefined; description?: string | undefined; author?: string | undefined }Implement the util type OptionalUndefined<T, Props> that turns all the properties of T that can be undefined, into optional properties. In addition, a second -optional- generic Props can be passed to restrict the properties that can be altered.
OptionalUndefined<{ value: string | undefined, description: string }>
// { value?: string | undefined; description: string }
OptionalUndefined<{ value: string | undefined, description: string | undefined, author: string | undefined }, 'description' | 'author'>
// { value: string | undefined; description?: string | undefined, author?: string | undefined }View on GitHub: https://tsch.js.org/28143
Change the following code to make the test cases pass (no type check errors).
π Lifetime-License is leaving on August 10, 2026
Get it now for $29
One-time payment. Lifetime access to all pro challenges.
The full solution:
type Flatten<T> = { [K in keyof T]: T[K] }
type OptionalUndefined<T, Props extends keyof T = keyof T> = Flatten<
{
[K in keyof T as K extends Props
? undefined extends T[K]
? never
: K
: K]: T[K]
} & {
[K in keyof T as K extends Props
? undefined extends T[K]
? K
: never
: never]?: T[K]
}
>The plan: split T into two halves, the properties that should stay as they are and the properties that should become optional, then glue the halves back together.
PropsThe tests call the type both with and without a second argument: OptionalUndefined<T, 'value'> but also plain OptionalUndefined<T>. Generic defaults handle this: Props extends keyof T = keyof T means "if the caller doesn't say which properties to touch, touch all of them". The extends keyof T constraint also gives callers autocomplete and rejects keys that don't exist on T.
Both mapped types ask the same two questions about every key K of T, using key remapping (as) to keep or drop the key:
K in the allowed set? K extends Props. If not, the key is out of scope and must remain untouched.undefined? undefined extends T[K]. Note the direction: we ask whether undefined is assignable to T[K], which is true exactly when undefined is part of the union. For string | undefined it holds; for plain string it doesn't.Remapping a key to never removes it from the resulting object entirely. So for T = { value: string | undefined; desc: string } with Props = 'value', the two halves evaluate to:
// first mapped type: { desc: string } (value β never: in Props and undefined-able)
// second mapped type: { value?: string | undefined } (desc β never: not in Props)? modifierThe second mapped type ends with ?]: T[K]. Writing ? after the key clause adds the optional modifier to every surviving property, the counterpart of the -? syntax that Required uses to strip it. The value type stays T[K], so string | undefined remains string | undefined; only the modifier changes. (TypeScript treats value?: string and value?: string | undefined as the same type, since ? already implies undefined. That's why the tests can expect either spelling.)
Both mapped types iterate in keyof T rather than over some precomputed union. That makes them homomorphic: TypeScript preserves each property's original modifiers (?, readonly) as it copies it over. Where that pays off is the first mapped type: an already-optional key outside Props, say meta in OptionalUndefined<{ value: string | undefined; meta?: number }, 'value'>, keeps meta? optional purely thanks to this preservation. (In the tests' OptionalUndefined<{ value?: string }, 'value'>, by contrast, the optional value is inside Props and lands in the second mapped type, where it stays optional simply because that half adds ? explicitly.)
Flatten is neededThe intersection { desc: string } & { value?: string } is assignable to and from { desc: string; value?: string }, but the tests use the strict Equal type, which distinguishes an intersection from the equivalent flat object. Flatten fixes that: mapping { [K in keyof T]: T[K] } over an intersection produces a single flat object type, with all optional modifiers preserved. It's the same prettify idiom you'll see in many production type utilities.
OptionalUndefined<{ value: string; desc: string }, 'value'> β unchanged: value is in Props but can't be undefined, so nothing happens.OptionalUndefined<{ value: string | undefined; desc: string | undefined }, 'value'> β only value becomes optional. desc also allows undefined but is outside Props, so it stays required.OptionalUndefined<{ value?: string }> β already-optional properties pass through unchanged: undefined extends T['value'] is true for optional props, so they land in the optional half and simply stay optional.The pattern of splitting with key remapping, modifying one half, and re-merging with Flatten is one of the most reusable recipes in advanced TypeScript. Reach for it any time one subset of properties needs different treatment than the rest.
This challenge is originally from here.