1

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

3
  • Terminology issue: you are talking about string literal types, not template literal types. Commented Mar 5, 2021 at 20:02
  • @jcalz thanks. Title updated. Commented Mar 5, 2021 at 20:04
  • It looks like you want properties to have type Properties. But in properties the type of level is a list of strings, while in Properties it's just a string. Commented Mar 5, 2021 at 20:23

1 Answer 1

2

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

Sign up to request clarification or add additional context in comments.

1 Comment

What a beautiful answer, not only you share very advanced knowledge but also you took the time to explain how things work in a very well arranged way. Not only It now works but I also know why. Very well appreciated @jcalz Thanks

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.