在TypeScript中,“keyof typeof”是什么意思?

328

请解释一下 TypeScript 中 keyof typeof 是什么意思。

例子:

enum ColorsEnum {
    white = '#ffffff',
    black = '#000000',
}

type Colors = keyof typeof ColorsEnum;

最后一行等价于:

type Colors = "white" | "black"

但它是如何工作的呢?

我原本以为typeof ColorsEnum会返回类似"Object"之类的东西,然后keyof "Object"不会有什么有趣的事情发生。但我显然是错了。


2
在“编译时”,typeof 的含义与 JS 在运行时的 typeof 不同... - SparK
5个回答

831
要理解在TypeScript中使用keyof typeof,首先需要了解什么是字面类型字面类型的联合。因此,我将首先解释这些概念,然后详细解释keyoftypeof。之后,我将回到enum来回答问题中所需的内容。虽然这是一个较长的答案,但示例易于理解。

字面类型

TypeScript中的字面类型是stringnumberboolean的更具体类型。例如,"Hello World"是一个string,但string并不是"Hello World"。因此,"Hello World"是一种更具体的类型,即字面类型。

可以声明字面类型如下:

type Greeting = "Hello"

这意味着类型为Greeting的对象只能有一个string"Hello",不能有其他任何string值或任何其他类型的值,如下面的代码所示:
let greeting: Greeting
greeting = "Hello" // OK
greeting = "Hi"    // Error: Type '"Hi"' is not assignable to type '"Hello"'

字面类型本身并不实用,但是当与联合类型、类型别名和类型保护结合使用时,它们变得强大。

以下是字面类型联合的示例:

type Greeting = "Hello" | "Hi" | "Welcome"

现在类型为Greeting的对象可以有值"Hello""Hi""Welcome"
let greeting: Greeting
greeting = "Hello"       // OK
greeting = "Hi"          // OK
greeting = "Welcome"     // OK
greeting = "GoodEvening" // Error: Type '"GoodEvening"' is not assignable to type 'Greeting'

只有keyof

keyof 用于某个类型 T,它会给你一个新类型,这个新类型是字面量类型的联合,这些字面量类型是 T 的属性名。结果类型是字符串的子类型。

例如,考虑下面的 interface

interface Person {
    name: string
    age: number
    location: string
}

在类型Person上使用keyof运算符将会得到一个新的类型,如下面的代码所示:

type SomeNewType = keyof Person

SomeNewType 是一个由 Person 类型的属性构成的字面类型联合("name" | "age" | "location")。

现在,您可以创建 SomeNewType 类型的对象:

let newTypeObject: SomeNewType
newTypeObject = "name"           // OK
newTypeObject = "age"            // OK
newTypeObject = "location"       // OK
newTypeObject = "anyOtherValue"  // Error...

在对象上同时使用keyof typeof

如您所知,typeof运算符可以给出一个对象的类型。 在上面的Person接口示例中,我们已经知道了类型,因此只需在类型Person上使用keyof运算符。

但是当我们不知道对象的类型或者我们只有一个值而不是该值的类型时,该怎么办呢?例如下面的情况:

const bmw = { name: "BMW", power: "1000hp" }

这是我们一起使用 keyof typeof 的地方。

typeof bmw 给出类型: { name: string, power: string }

然后 keyof 运算符给出如下代码中所示的字面类型联合:

type CarLiteralType = keyof typeof bmw

let carPropertyLiteral: CarLiteralType
carPropertyLiteral = "name"       // OK
carPropertyLiteral = "power"      // OK
carPropertyLiteral = "anyOther"   // Error...

keyof typeofenum 上的使用

在 TypeScript 中,枚举被用作编译时的类型,以实现常量的类型安全性,但它们在运行时被视为对象。这是因为,一旦 TypeScript 代码被编译为 JavaScript,它们就会被转换为普通对象。因此,上面的对象解释也适用于此处。问题中 OP 给出的示例是:

enum ColorsEnum {
    white = '#ffffff',
    black = '#000000',
}

这里的ColorsEnum是一个运行时存在的对象,而不是一个类型。因此,我们需要像以下代码一样同时调用keyof typeof操作符:

type Colors = keyof typeof ColorsEnum

let colorLiteral: Colors
colorLiteral = "white"  // OK
colorLiteral = "black"  // OK
colorLiteral = "red"    // Error...

132
这是一个典型的Stack Overflow答案,应该用来向新用户解释如何回答问题。非常好,这应该成为被采纳的答案。 很棒。 - dudewad
29
我需要每月阅读这个答案。 - X. Wang
15
这绝对是一个范本式的好回答。 - tcf01
1
非常好的答案,帮助了我很多。 - Wakerboy135

74

keyof接受一个对象类型并返回一个接受该对象任何键的类型。

type Point = { x: number; y: number };
type P = keyof Point; // type '"x" || "y"'

const coordinate: P = 'z' // Type '"z"' is not assignable to type '"x" | "y"'.

使用 TypeScript 类型的 typeof

typeof 在调用 JavaScript 对象时和调用 TypeScript 类型时的行为不同。

  • 在运行时,TypeScript 在调用 JavaScript 值时使用 JavaScript 的 typeof,并返回以下其中之一:"undefined"、"object"、"boolean"、"number"、"bigint"、"string"、"symbol"、"function"
  • TypeScript 的 typeof 在调用类型值时被调用,但也可以在类型表达式中调用 JavaScript 值。它还可以推断 JavaScript 对象的类型,返回更详细的对象类型。
type Language = 'EN' | 'ES'; 
const userLanguage: Language = 'EN';
const preferences = { language: userLanguage, theme: 'light' };

console.log(typeof preferences); // "object"
type Preferences = typeof preferences; // type '{language: 'EN''; theme: string; }'

因为第二个typeof preferences是在类型表达式中,实际上调用的是TypeScript自己的typeof,而不是JavaScript的。

keyof typeof

因为keyof是一个TypeScript概念,我们将调用TypeScript的typeof版本。 keyof typeof会推断出JavaScript对象的类型,并返回其键的联合类型。因为它可以推断出键的确切值,所以它可以返回它们的literal types的联合类型,而不仅仅返回"string"。
type PreferenceKeys = keyof typeof preferences; // type '"language" | "theme"'

35

enum 创建了一个实例化的对象。使用 typeof 可以获得此 enum 的自动生成类型。

现在我们可以使用 keyof 获取所有索引,以确保 Colors 只能包含其中之一。


17

TypeScript中的常见误解

通常将TypeScript描述为JavaScript运行时之上的一种类型层。好像类型和值存在于不同的平面上。但是,在TypeScript中,有些东西既是类型也是值。

这对于以下内容是正确的:

  • 类,
  • 枚举,
  • 命名空间。

何时可以使用 keyof

keyof 关键字仅适用于类型级别。您不能将其应用于JavaScript值。

何时需要使用 keyof typeof

当您处理既是类型又是值的内容(例如类或枚举),但您特别关注该值的类型是什么时,就需要使用它。

最简单的例子:

const foo = { bar: 42 }; // foo is a value
type Foo = typeof foo; // Foo is the type of foo

type KeyOfFoo = keyof Foo; // "keyof Foo" is the same as "keyof typeof foo", which is "bar"

通常情况下,当您看到这个:

type A = keyof typeof B;

typeof B部分告诉TypeScript查看B的类型,可以将其视为将B转换为其类型。类似于将二维对象投射到一维空间。

由于typeof B是一个类型而不是一个值,因此现在我们可以在其上使用keyof

例子

类是类型和值。你可以调用它们,但也可以在它们上使用keyof

declare class Foo {
    static staticProperty: string;

    dynamicProperty: string;
}

type Constructor = typeof Foo;
type Instance = Foo;

type A = keyof Constructor; // "prototype" | "staticProperty"
type B = keyof Instance; // "dynamicProperty"

通过使用 typeofkeyof,我们可以在实例类型构造函数类型之间切换使用 keyof


2

为了找到任何值的类型,我们使用typeof操作。例如:

const user = {
   getPersonalInfo(){},
   getLocation(){}
}

在这里,"user"是一个值,所以typeof 运算符非常实用。
type userType = typeof user

这里的userType提供了类型信息,表示user是一个对象,有两个属性getPersonalInfo和getLocation,都是返回void的函数。

现在如果你想找到user的键,可以使用keyof关键字。

type userKeys = keyof userType

这句话表示用户键=userKeys= 'getPersonalInfo'| 'getLocation'

如果您尝试像type userKeys = keyof user这样获取用户的密钥,您将会收到一个错误提示'user' refers to a value, but is being used as a type here. Did you mean 'typeof user'?


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接