vdegenne
vdegenne

Reputation: 13270

Make a String Literal Types from an Array in TypeScript

I have this object in my definition files

export const properties = {
  name: '',
  level: ['a', 'b', 'c'],
  uri: '',
  active: true
}

(note: it's not an interface it's actually a real object, the reason I need an object is because I need it as a reference during runtime)

Now I try to make a type from this object, so this is what I need

export type Properties = {
  name: string;
  level: 'a'|'b'|'c';
  uri: string;
  active: boolean
}

I tried using

export type Properties = typeof properties

but level is translated as string[] which is normal, but how can I use TypeScript to map ['a', 'b', 'c'] to 'a'|'b'|'c' ? And if it's possible how can I do that during the mapping of one type to another?

Thanks

Upvotes: 1

Views: 397

Answers (1)

jcalz
jcalz

Reputation: 327819

If you define properties exactly as you've done above, then it won't work. By the time you get around to writing type Properties =, the compiler has already widened the level property to string[], and forgotten all about the string literal types of its elements, as you've seen.

In order to even have a chance to get "a" | "b" | "c" out of properties, therefore, you will need to alter the definition of properties. The easiest way is to use a const assertion to give the compiler a hint that you want the narrowest type it can infer. For example:

const properties = {
    name: '',
    level: ['a', 'b', 'c'] as const,
    uri: '',
    active: true
}

We've asserted level as const, and now typeof properties looks like this:

/* const properties: {
    name: string;
    level: readonly ["a", "b", "c"];
    uri: string;
    active: boolean;
} */

So, how can we transform that to get Properties? Assuming the question "how can I do that during the mapping of one type to another?" means you'd like every array-like thing to be transformed to its element type, and assuming that you only need to do this one-level deep (and not recursively), then you can define this type function:

type Transform<T> = { [K in keyof T]: 
  T[K] extends readonly any[] ? T[K][number] : T[K] 
}

That's a mapped type where we take the input type T, and for each property key K, we index into it to get its property type (T[K]). If that property type is not an array (readonly any[] is actually more general than any[]), we leave it alone. If it is an array, then we grab its element type by indexing into it with number (if you have an array arr and a number n, then arr[n] will be an element).

For typeof properties, that results in:

type Properties = Transform<typeof properties>
/* type Properties = {
    name: string;
    level: "a" | "b" | "c";
    uri: string;
    active: boolean;
} */

as desired.

Playground link to code

Upvotes: 2

Related Questions