how to define MyInterfaceKeys
in this code?
interface MyInterface extends Record<string, any> {
Appearance?: "default" | "primary" | "link";
Size?: "small" | "medium" | "large";
Color?: string;
Block?: boolean;
}
type MyInterfaceKeys = (keyof MyInterface)[]
// ok => MyInterfaceKeys === ["Block", "Color"]
// ok => MyInterfaceKeys === ["Appearance"]
// Error => MyInterfaceKeys === ["UnknownKey"]
in fact, I want to convert object props to a union literals:
type MyInterfaceKeys = ("Appearance" | "Size" | "Color" | "Block")[]
Object types in TypeScript have a set of known keys, which correspond to individual string or number literals (or symbols); and a set of index signature keys, which correspond to multiple possible keys at once (these used to just be string
or number
, but now you can use pattern template literals and symbols in index signatures also). For example, the following type:
type Foo = {
[k: string]: 0 | 1
x: 0,
y: 1
}
has two known keys "x"
and "y"
and one index signature key string
. Your MyInterface
type has four known keys, but it also has a string
index signature key (because it extends Record<string, any>
which has a string
index signature key).
The keyof
operator produces the union of all the keys. For Foo
, that is "x" | "y" | string
, and for MyInterface
that is "Appearance" | "Size" | "Color" | "Block" | string
.
Since every string literal (like "x"
and "y"
) is a subtype of string
, the union of string literal types with string
is just string
, and the compiler eagerly reduces it to this. So keyof Foo
and keyof MyInterface
is just string
. In some sense, string
"absorbs" all the string literal keys.
So you cannot use keyof
to get just the known keys if there are any index signature keys that absorb it.
So, what can you do? The cleanest thing you can do is to consider refactoring your code so that the information you want can be captured more easily:
interface MyKnownInterface {
Appearance?: "default" | "primary" | "link";
Size?: "small" | "medium" | "large";
Color?: string;
Block?: boolean;
}
interface MyInterface extends Record<string, any>, MyKnownInterface { }
type KK = (keyof MyKnownInterface) & string
// type KK = "Appearance" | "Size" | "Color" | "Block"
type MyInterfaceKeys = (keyof MyKnownInterface)[]
// type MyInterfaceKeys = (keyof MyKnownInterface)[]
// type MyInterfaceKeys = ("Appearance" | "Size" | "Color" | "Block")[]
Here MyInterface
is equivalent to what it was before, but keyof MyKnownInterface
is the union of known keys that you want.
You could also use type manipulation to try to tease out the known keys instead of using just keyof
, although I think all the possible ways I know of have some weird edge cases (I'm about to file a bug having to do with symbol
keys). But hopefully you won't run into such edge cases(the MyInterface
example doesn't).
One way is to use key remapping in mapped types to suppress the index signatures, which can be recognized as any key that doesn't require a value even if it's not optional (index signatures represent any number of keys of the right type, including zero such keys):
type KnownKeys<T> = keyof {
[K in keyof T as {} extends { [P in K]: any } ? never : K]: never
}
If we do that in your case, you get:
type MyInterfaceKeys = (KnownKeys<MyInterface>)[]
// type MyInterfaceKeys = ("Appearance" | "Size" | "Color" | "Block")[]