'Keyof not working in template literals due to Symbols

I would expect either of these types to work, but these both throw errors.

export type Object1<T extends { [k: string]: any }> = `${keyof T}`;
export type Object2<T extends Record<string, any>> = `${keyof T}`;

They both give this error

TS2322: Type 'key T' is not assignable to type 'string | number | bigint | boolean | null | undefined'. 
 Type 'string | number | symbol' is not assignable to type 'string | number | bigint | boolean | null | undefined'.

I'm not understanding why symbol | number are appearing despite specifying that the keys are strings.



Solution 1:[1]

The issue is that the constraints don't restrict the keys to strings. They require that all string keys in T have a value of type any, but it is perfectly fine for T to have other keys as well.

For example, if you leave out the template literal type to get rid of the error, you can apply Object1 to an object type with a string, a number, and a symbol key:

type Object1<T extends {[k: string]: any}> = keyof T

type Test1 = Object1<{s: 'str', 1: 'num', [Symbol.iterator]: 'sym'}>
// type Test1 = typeof Symbol.iterator | "s" | 1

And the same thing holds for Object2:

type Object2<T extends Record<string, any>> = keyof T

type Test2 = Object2<{s: 'str', 1: 'num', [Symbol.iterator]: 'sym'}>
// type Test1 = typeof Symbol.iterator | "s" | 1

TypeScript playground

Solution 2:[2]

The answer of @Oblosys offers a good explanation why keyof still produces all possible key types even though you specified a constraint. If I understand your question correctly, your goal is to get all the keys of a type inside a string literal (maybe for further string manipulation?)

Maybe this can help you:

export type KeyLiterals<T extends {[k: string] : any}> = 
  `${keyof T extends string | number ? keyof T : never}`;

type Test = KeyLiterals<{a: string, b: string}> // 'a' | 'b'

We can check if keyof is of type string or number to make sure that no symbols remain in the union.


Now you can do any further string manipulation like adding a prefix.

export type KeyLiterals2<T extends {[k: string] : any}> = 
  `prefix_${keyof T extends string | number ? keyof T : never}`;

type Test2 = KeyLiterals2<{a: string, b: string}> // 'prefix_a' | 'prefix_b'

Sources

This article follows the attribution requirements of Stack Overflow and is licensed under CC BY-SA 3.0.

Source: Stack Overflow

Solution Source
Solution 1 Oblosys
Solution 2 Tobias S.