First of all we need to iterate over color
keys an get rid of number
sufix.
const color = {
blue100: '#hex',
blue500: '#hex',
red100: '#hex',
red500: '#hex',
yellow100: '#hex',
yellow500: '#hex',
} as const;
type MapColor<T extends Record<`${string}${number}`, string>> = {
[Prop in keyof T as RemoveNumber<Prop>]: T[Prop]
}
// type Result = {
// readonly blue: "#hex";
// readonly red: "#hex";
// readonly yellow: "#hex";
// }
type Result = MapColor<typeof color>
We need just to get rid of number
suffix.
This is our main util
type RemoveNumber<T> = T extends string ? ReduceToString<Filter<T>> : never
Let's start from the end. Our Filter
utility type converts the string into tuple and removes all numbers:
const color = {
blue100: '#hex',
blue500: '#hex',
red100: '#hex',
red500: '#hex',
yellow100: '#hex',
yellow500: '#hex',
} as const;
type Filter<T extends string, Cache extends string[] = []> =
T extends `${infer Color}${infer Tail}`
? Color extends `${number}`
? Cache
: Filter<Tail, [...Cache, Color]>
: never
{
// ["b", "l", "u", "e"] | ["r", "e", "d"] | ["y", "e", "l", "l", "o", "w"]
type Test = Filter<keyof typeof color>
}
Because we need to operate on string, we need to convert it back - to the string.
type Elem = string;
type ReduceToString<
Arr extends ReadonlyArray<Elem>,
Result extends string = ''
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends Elem
? `${Result}${H}`
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<Elem>
? H extends Elem
? ReduceToString<Tail, `${Result}${H}`>
: never
: never
: never;
{
type Test = ReduceToString<Filter<keyof typeof color>>
}
Let's make a quick summarize. We ended up with this type:
// type Result = {
// readonly blue: "#hex";
// readonly red: "#hex";
// readonly yellow: "#hex";
// }
type Result = MapColor<typeof color>
Now we need to convert keyof Result
to a tuple with permutation of all possible states:
// credits goes to https://twitter.com/WrocTypeScript/status/1306296710407352321
type TupleUnion<U extends string, R extends any[] = []> = {
[S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];
{
// | TupleUnion<"red" | "yellow", ["blue"]>
// | TupleUnion<"blue" | "yellow", ["red"]>
// | TupleUnion<"blue" | "red", ["yellow"]>
type Test = TupleUnion<keyof MapColor<typeof color>>
}
This is because Object.keys
does not give you a guarantee of key order.
Now we can implement the function:
function colorNames<
Key extends `${string}${number}`,
Value extends string,
Color extends Record<Key, Value>
>(color: Color): TupleUnion<keyof MapColor<Color>>
function colorNames<
Key extends `${string}${number}`,
Value extends string,
Color extends Record<Key, Value>
>(color: Color) {
return [...new Set(Object.keys(color)
.map(
(x) => x.replace(/[0-9]/g, '')
))];
}
const result = colorNames(color) // ["blue", "red", "yellow"]
type ColorType = (typeof result)[number] // "blue" | "red" | "yellow"
WHole code:
const color = {
blue100: '#hex',
blue500: '#hex',
red100: '#hex',
red500: '#hex',
yellow100: '#hex',
yellow500: '#hex',
} as const;
// type Result = {
// readonly blue: "#hex";
// readonly red: "#hex";
// readonly yellow: "#hex";
// }
type Result = MapColor<typeof color>
type Filter<T extends string, Cache extends string[] = []> =
T extends `${infer Color}${infer Tail}`
? Color extends `${number}`
? Cache
: Filter<Tail, [...Cache, Color]>
: never
{
// ["b", "l", "u", "e"] | ["r", "e", "d"] | ["y", "e", "l", "l", "o", "w"]
type Test = Filter<keyof typeof color>
}
type Elem = string;
type ReduceToString<
Arr extends ReadonlyArray<Elem>,
Result extends string = ''
> = Arr extends []
? Result
: Arr extends [infer H]
? H extends Elem
? `${Result}${H}`
: never
: Arr extends readonly [infer H, ...infer Tail]
? Tail extends ReadonlyArray<Elem>
? H extends Elem
? ReduceToString<Tail, `${Result}${H}`>
: never
: never
: never;
{
// "blue" | "red" | "yellow"
type Test = ReduceToString<Filter<keyof typeof color>>
}
type RemoveNumber<T> = T extends string ? ReduceToString<Filter<T>> : never
type MapColor<T extends Record<`${string}${number}`, string>> = {
[Prop in keyof T as RemoveNumber<Prop>]: T[Prop]
}
{
type Test = MapColor<typeof color>
}
// credits goes to https://twitter.com/WrocTypeScript/status/1306296710407352321
type TupleUnion<U extends string, R extends any[] = []> = {
[S in U]: Exclude<U, S> extends never ? [...R, S] : TupleUnion<Exclude<U, S>, [...R, S]>;
}[U];
{
// | TupleUnion<"red" | "yellow", ["blue"]>
// | TupleUnion<"blue" | "yellow", ["red"]>
// | TupleUnion<"blue" | "red", ["yellow"]>
type Test = TupleUnion<keyof MapColor<typeof color>>
}
function colorNames<
Key extends `${string}${number}`,
Value extends string,
Color extends Record<Key, Value>
>(color: Color): TupleUnion<keyof MapColor<Color>>
function colorNames<
Key extends `${string}${number}`,
Value extends string,
Color extends Record<Key, Value>
>(color: Color) {
return [...new Set(Object.keys(color)
.map(
(x) => x.replace(/[0-9]/g, '')
))];
}
const result = colorNames(color) // ["blue", "red", "yellow"]
type ColorType = (typeof result)[number] // "blue" | "red" | "yellow"
Playground
List of useful links:
- Convert union to tuple - SO answer
- Hex validation in typescript article - SO answer
- My blog where you can find some interesting typescript examples
- Reduce literal type in typescript
- Here you can find more explanation of how
Reduce
works with pure js examples
Please keep in mind that it is not required to use as const
with color
object. I have used it only for type utilities test purpose. colorNames
function is able to infer each color
key.
If you are interested in function arguments type inference. please take a look on my article