1

In the code below I am retrieving each key from an object, removing the digits and duplicates and then I store them in an array. I'd like to use that array as a type.

Here's what the color object looks like.

const color = {
    blue100: '#...',
    blue500: '#...',
    red100: '#...',
    red500: '#...',
    yellow100: '#...',
    yellow500: '#...',
    ...
}

Here's what I came up with so far

const colorChars = Object.keys(color).map((x) => x.replace(/[0-9]/g, ''))
const colorArray = [...new Set(colorChars)]
const colorNames = colorArray as const

export type ColorNameType = typeof colorNames
// expected: type ColorNameType = 'blue' | 'red' | 'yellow'

Here's the error I get

A 'const' assertions can only be applied to references to enum members, or string, number, boolean, array, or object literals.

The error disappears if I wrap colorArray like so [...colorArray] but it doesn't solve the issue because the type stays as String[]. Any idea on how to solve this or is there another attractive work around?

Alireza Ahmadi
  • 8,579
  • 5
  • 15
  • 42
Horai Nuri
  • 5,358
  • 16
  • 75
  • 127

3 Answers3

1

If you need to convert object keys to union type you can simply use keyof typeof YourConstObject like this:

const color = {
    blue100: '#1',
    blue500: '#2',
    red100: '#3',
    red500: '#4',
    yellow100: '#5',
    yellow500: '#6',
} as const;

type Keys = keyof typeof color;//"blue100" | "blue500" | "red100" | "red500" | "yellow100" | "yellow500"

Or if you need to create union type with values of object you can do like this:

type ValueOf<T> = T[keyof T]

type values = ValueOf<typeof color>//"#1" | "#2" | "#3" | "#4" | "#5" | "#6"

PlaygroundLink

But note that whenever you try to manipulate the object keys like using map, push, replace so ts compiler have face with string[] type because it does not make sense for the compiler your new array, like: blue, red....

Alireza Ahmadi
  • 8,579
  • 5
  • 15
  • 42
1

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:

  1. Convert union to tuple - SO answer
  2. Hex validation in typescript article - SO answer
  3. My blog where you can find some interesting typescript examples
  4. Reduce literal type in typescript
  5. 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

0

as const can only be used at declaration.

So this example would work:

const color = {
  blue100: '#...',
  blue500: '#...',
  red100: '#...',
  red500: '#...',
  yellow100: '#...',
  yellow500: '#...',

  ...
} as const;

but not this:

const colorChars = Object.keys(color).map((x) => x.replace(/[0-9]/g, ''))
const colorArray = [...new Set(colorChars)]
const colorNames = colorArray as const

I don't think there is a way to achieve what you're trying to do in Typescript.

Joyescat
  • 507
  • 5
  • 11