Search code examples
typescripttypescript-types

Typescript: How to declare a type of array whose values are equal to keys of object?


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")[]

Solution

  • 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")[]
    

    Playground link to code