打字稿:如何声明其值等于对象键的数组类型?

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

如何在这段代码中定义MyInterfaceKeys

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

事实上,我想将对象属性转换为联合文字:

type MyInterfaceKeys = ("Appearance" | "Size" | "Color" | "Block")[]

TypeScript 中的对象类型有一组 已知键 ,它们也对应于单个字符串或数字 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。例如下面的类型:

type Foo = {
    [k: string]: 0 | 1
    x: 0,
    y: 1
}

有两个已知密钥 "x""y" 以及一个索引签名密钥 string。您的 MyInterface 类型有四个已知密钥,但它也有一个 string 索引签名密钥(因为它扩展了 Record<string, any>,后者有一个 string 索引签名密钥)。


The keyof operator produces the union 的所有键。对于 Foo,即 "x" | "y" | string,对于 MyInterface,即 "Appearance" | "Size" | "Color" | "Block" | string

由于每个字符串文字(如 "x""y")都是 string 的子类型,因此字符串文字类型与 string 的联合只是 string,编译器急切地将它缩减为这个。所以 keyof Fookeyof MyInterface 就是 string。在某种意义上,string“吸收”了所有字符串文字键。

因此,如果有任何吸收它的索引签名密钥,您不能使用 keyof 来获取已知密钥。


那么,你能做什么?您可以做的最干净的事情是考虑重构您的代码,以便可以更轻松地捕获您想要的信息:

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

这里的MyInterface和之前的是等价的,但是keyof MyKnownInterface是你想要的已知键的并集


你也可以使用类型操作来尝试梳理出已知的键,而不是仅仅使用 keyof,尽管我认为我所知道的所有可能的方法都有一些奇怪的边缘情况(我即将提交一个与 symbol 键有关的错误)。但希望你不会 运行 陷入这种极端情况(MyInterface 示例不会)。

一种方法是使用key remapping in mapped types来抑制索引签名,它可以被识别为不需要值的任何键,即使它不是可选的(索引签名代表任意数量的键)正确的类型,包括零个这样的键):

type KnownKeys<T> = keyof {
    [K in keyof T as {} extends { [P in K]: any } ? never : K]: never
}

如果我们在您的情况下这样做,您将得到:

type MyInterfaceKeys = (KnownKeys<MyInterface>)[]
// type MyInterfaceKeys = ("Appearance" | "Size" | "Color" | "Block")[]

Playground link to code