I think your main stumbling block is that the operations you're performing are not distributing over union types. You want to get all the property value types of an object, which normally one could write as
type ValueOf<T> = T[keyof T];
But this often fails when T
is a union of object types. The problem is that if you have an object type like {a: string}
and index into it with its key type "a"
, you'll get its value type string
. But if you have a union of object types like {a: string} | {b: number}
, there's no key you can do to index into it safely. You can't index into it with "a" | "b"
. Since {a: string}
has no known b
property and {b: number}
has no known a
property, you can't index into the union with either key. And no, you don't get undefined
if you index into {a: string}
with a key that isn't a
. Object types in TypeScript are extendible and may have extra properties. If you index into {a: string}
with "b"
you would get the any
type out, not undefined
. And the compiler complains anyway. When you ask the compiler what keyof ({a: string} | {b: number})
is, it says it's the never
type; there are no keys. And so ValueOf<{a: string} | {b: number}>
is also never
:
type Sad = ValueOf<{ a: string } | { b: number }>
// type Sad = never
If you want to get string | number
out of {a: string} | {b: number}
, you conceptually want to break the union into pieces {a: string}
and {b: number}
, index into each with its known keys, and put them back together in a new union. That is, you want to distribute the ValueOf<T>
operation over unions in T
.
Luckily, TypeScript provides distributive conditional types with such behavior. If you have a generic type parameter T
and check it against anything with a conditional type, like T extends unknown ? F<T> : X
, the compiler will automatically distribute the F<T>
operation over unions in T
.
So we can turn ValueOf
into AllValuesOf
like this:
type AllValuesOf<T> = T extends any ? T[keyof T] : never;
Yes, this looks like a no-op. But T extends any ? T[keyof T] : never
should be read "for each union member U
in T
, calculate U[keyof U]
, and unite them back into a new union".
Let's try it on our toy union type from above:
type Happy = AllValuesOf<{ a: string } | { b: number }>
// type Happy = string | number
Works like a charm.
Armed with AllValuesOf<T>
, you can do indexed accesses and mapped types more easily on your object type now:
type ProductSections = typeof PRODUCT_SECTIONS;
type SECTION_NAME = AllValuesOf<ProductSections>["name"]
// type SECTION_NAME = "Delicious fruit" | "Fresh vegetables" | "Quality soda"
type PRODUCT = AllValuesOf<AllValuesOf<ProductSections>["products"]>
//"Banana" | "Apple" | "Mixed greens" | "Lettuce" | "Cucumbers" | "Coke" | "Sprite";
type SELECTED_PRODUCTS_STATE = { [K in keyof ProductSections]:
AllValuesOf<ProductSections[K]["products"]> }
/* type SELECTED_PRODUCTS_STATE = {
readonly fruit: "Banana" | "Apple";
readonly vegetables: "Mixed greens" | "Lettuce" | "Cucumbers";
readonly soda: "Coke" | "Sprite";
} */
Looks good!
Playground link to code