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
propertiesto have typeProperties. But inpropertiesthe type oflevelis a list of strings, while inPropertiesit's just a string.