Filter a union of object types down to the members that have a given key. No recursion, no infer: the whole difficulty is understanding distribution.
UnionToObjectFromKey<Union, Key> takes a union of object types and a key name, and returns the member(s) that actually have that key. The solution is compact, but it hinges on two mechanics you'll use constantly in advanced TypeScript: distributive conditional types and per-member keyof checks. The same distribute-and-filter idea underlies how TypeScript narrows discriminated unions.
For example
type Foo = { foo: string; common: boolean }
type Bar = { bar: number; common: boolean }
type Result = UnionToObjectFromKey<Foo | Bar, 'foo'> // expected to be FooFind the object containing the key in the union type by the key. It takes two parameters: a union of object types and a key name.
View on GitHub: https://tsch.js.org/33763
Change the following code to make the test cases pass (no type check errors).
/* _____________ Your Code Here _____________ */
type UnionToObjectFromKey<Union, Key> = any
/* _____________ Test Cases _____________ */
import type { Equal, Expect } from '../helpers'
type Foo = {
foo: string
common: boolean
}
type Bar = {
bar: number
common: boolean
}
type Other = {
other: string
}
type cases = [
Expect<Equal<UnionToObjectFromKey<Foo | Bar, 'foo'>, Foo>>,
Expect<
Equal<
UnionToObjectFromKey<Foo | Bar, 'common'>,
| {
foo: string
common: boolean
}
| {
bar: number
common: boolean
Unlock 170+ medium, hard, and extreme challenges to master advanced TypeScript.
Monthly subscription. Cancel anytime.
Everything fits in two nested conditionals:
type UnionToObjectFromKey<Union, Key> = Union extends unknown
? Key extends keyof Union
? Union
: never
: neverEach conditional has a distinct job.
The outer check Union extends unknown is always true, since every type extends unknown, so it never filters anything. Its only purpose is to trigger distribution: when the type before extends is a bare generic parameter and it's instantiated with a union, TypeScript evaluates the conditional once per member and unions the results.
So for Union = Foo | Bar, the type expands to:
// (Key extends keyof Foo ? Foo : never)
// | (Key extends keyof Bar ? Bar : never)Without this wrapper, keyof Union inside would mean keys shared by all members (keyof (Foo | Bar) is only 'common'), and you could never match a key that exists on just one member. Distribution gives you each member's own keyof instead.
Inside the distributed branch, Union refers to a single member at a time. Key extends keyof Union asks: is Key one of this member's keys?
UnionToObjectFromKey<Foo | Bar, 'foo'>: 'foo' extends keyof Foo is true, so the Foo branch yields Foo; 'foo' extends keyof Bar is false, so the Bar branch yields never.Foo | never. Here the second mechanic kicks in: never is the identity element of unions, so it vanishes. The final answer is Foo.This "return the member or never, let never disappear" idiom is how the built-in Extract and Exclude utilities work under the hood. You've effectively written a custom Extract that filters by key presence instead of assignability.
UnionToObjectFromKey<Foo | Bar, 'foo'> → only Foo has a foo key → Foo.UnionToObjectFromKey<Foo | Bar, 'common'> → both members have common, so both survive and the result stays a union: Foo | Bar. Nothing in the solution forces a single winner; "all members that match" is the correct semantic.UnionToObjectFromKey<Foo | Bar | Other, 'common'> → the tests define Other as { other: string }, so it has no common key, collapses to never and drops out, leaving Foo | Bar. This is the test that proves filtering really happens member by member.There's no recursion and no infer here; the difficulty is conceptual. You need to know that a no-op conditional like Union extends unknown exists purely to switch on distribution, and that keyof behaves very differently on a whole union than on its members. Add the fact that never drops out of unions, and the solution almost writes itself. You'll spot the pattern in plenty of real-world types that pick a variant out of a discriminated union.
This challenge is originally from here.
Track your progress through 100+ hands-on challenges. Free, sign in with GitHub.
Or start solving right away: explore all TypeScript challenges