Skip to content

0x01a-在TypeScript中有效地使用keyof和typeof来表示类型(译)

翻译自:
https://mayashavin.com/articles/types-from-constants-typescript

在本文中,我们将学习如何通过结合 typeof、keyof 和枚举类型运算符来提取和声明常量类型,以使您的代码更具优势。

为了获得更好的编程体验,您应该在您的集成开发环境(如VSCode)中安装 TypeScript。它将为您提供许多必要的功能,如error highlighting,、IntelliSense、linting等等… 您还应该安装一些扩展,如 JavaScript and TypeScript NightlyESLint,等等。

了解 typeof

在 TypeScript 中,我们可以使用 typeof 来提取变量属性的类型,如下面的示例所示:

js
const Name = {
    firstName: 'Maya',
    lastName: 'Shavin'
};

let myName: typeof Name;
const Name = {
    firstName: 'Maya',
    lastName: 'Shavin'
};

let myName: typeof Name;

在上面的代码中,我们使用从变量 Name 中提取的类型来声明变量 myName,这个类型将是一个包含两个属性 - firstName 和 lastName 的对象。当在你的 IDE 中悬停在 myName 上时,TypeScript 将显示从 Name 推断出的 myName 的类型,如下面的截图所示:

在这里注意,当使用typeof提取对象的类型时,接收到的类型将是一个完整的类型结构,每个属性都有其自己的类型(例如myName的类型将有两个字段 - firstName是字符串类型,lastName是字符串类型)。

另一个类型操作符 typeof 可以与 ReturnType 结合使用,用来提取函数返回数据的类型。为了实现这一目的,我们执行以下操作:

  • 使用 typeof 获取已声明函数的函数类型
  • 使用 ReturnType<T>,其中 T 是第1步中提取的类型,来获取 T 返回值的类型

下面的示例演示了如何提取decoupleName()的返回类型

js
function decoupleName (name: string) {
    const [firstName, ...remaining] = name.split(" ");
    return {
        firstName,
        lastName: remaining.reduce((text, word) => text ? `${text} ${word}` : word, '')
    }
}

type NameObj = ReturnType<typeof decoupleName>
function decoupleName (name: string) {
    const [firstName, ...remaining] = name.split(" ");
    return {
        firstName,
        lastName: remaining.reduce((text, word) => text ? `${text} ${word}` : word, '')
    }
}

type NameObj = ReturnType<typeof decoupleName>

TypeScript 将自动引用NameObj的正确类型,如下面的屏幕截图所示:

在 TypeScript 中,我们还可以使用 typeof 关键字来担当类型守卫,在条件语句中进行判断,就像在 JavaScript 中一样。不过在 TypeScript 中,主要适用于字符串、对象、函数等基本类型。

现在我们理解了 typeof 及其用法。接下来,我们将探讨 keyof。

理解 keyof

虽然 typeof 用于得到变量表示的类型,但 keyof 则用于对象类型,并生成一个由该变量键的自由联合组成的类型,如下所示:

js
interface User {
    firstName: string;
    lastName: string;
};

type UserKeys = keyof User
interface User {
    firstName: string;
    lastName: string;
};

type UserKeys = keyof User

上面的等同于以下声明:

ts
type UserKeys = "firstName" | "lastName"
type UserKeys = "firstName" | "lastName"

然而,与 typeof 不同,你不能直接在变量上使用 keyof。对于以下代码,TypeScript会抛出错误。

js
const Name = {
    firstName: 'Maya',
    lastName: 'Shavin'
};

type NameKeys = keyof Name; //error
const Name = {
    firstName: 'Maya',
    lastName: 'Shavin'
};

type NameKeys = keyof Name; //error

要从对象变量中提取类型,例如常量值的对象映射,我们需要结合keyoftypeof ,我们接下来将学习这一点。

从对象的键(或属性)中提取类型

取以下的ImageVariables,它们作为修改图像时使用的变量的指标。

js
export const ImageVariables = {
  width: 'w',
  height: 'h',
  aspectRatio: 'ar',
  rotate: 'a',
  opacity: 'o',
} as const;
export const ImageVariables = {
  width: 'w',
  height: 'h',
  aspectRatio: 'ar',
  rotate: 'a',
  opacity: 'o',
} as const;

这里需要在对象末尾加上 const 关键字,以表示其为只读。否则,TypeScript 不会允许我们从中提取类型,因为可能会在过程中修改对象的内部属性

ImageVariables包含了从其键到在根据Cloudinary mechanism.转换图像时使用的匹配变量符号的映射。要生成基于ImageVariables属性(或键)的类型,我们执行以下操作:

  • 使用 typeof 获取代表 ImageVariables 的类型
js
type ImageVariablesType = typeof ImageVariables
type ImageVariablesType = typeof ImageVariables
  • 根据 ImageVariablesType 种类的键提取一个新类型,使用`keyof
js
type ImageFields = keyof ImageVariablesType
type ImageFields = keyof ImageVariablesType

或者,我们可以将上述两个步骤合并成一个步骤,如下所示:

js
type ImageFields = keyof typeof ImageVariables
type ImageFields = keyof typeof ImageVariables

我们现在有了ImageFields类型,其中包含了ImageVariables所接受的字段,就像下面的截图中显示的那样:

现在我们可以按以下方式使用这个生成的类型:

js
const transformImage = (field: ImageFields) => {
    const variable = ImageVariables[field]
    //do something with it
}
const transformImage = (field: ImageFields) => {
    const variable = ImageVariables[field]
    //do something with it
}

通过根据ImageVariables属性声明类型,确保了transformImage的任何使用都是安全的,我们可以保证传递的字段必须始终存在于ImageVariables中。否则,TypeScript将会检测并提醒用户任何错误。

另外,Intellisense会提示用户可接受的数值,降低传递错误数值的可能性。

顺便提一下,类型检查的行为类似于在运行时使用hasOwnProperty()检查,不过它只发生在编译时。

听起来很简单。如果我们想将ImageVariables中的键值提取到新类型中怎么办?我们接下来看一下。

从对象的键(或属性)的值中提取类型

如果我们想要根据ImageVariables的键的值生成一个类型,我们可以执行以下操作:

js
type VariableValues = typeof ImageVariables[ImageFields]
type VariableValues = typeof ImageVariables[ImageFields]

由于我们已经将ImageVariablesType声明为ImageVariables的类型,因此我们可以将上面的内容重写为:

js
type VariableValues = ImageVariablesType[ImageFields]
type VariableValues = ImageVariablesType[ImageFields]

通过上面的代码,我们现在有了一个新类型VariableValues ,它接受以下值:

从一个命名常量对象的值和键生成类型,在许多情况下都是有益的,比如当你需要处理各种数据映射时,以及标准键或值在它们之间进行映射的情况。通过在对象映射上使用 keyof 和 typeof 可以帮助创建相关映射和类型之间的连接,从而避免潜在的 bug。

另外,我们也可以结合枚举和 typeof 来实现相同的目标。

奖励:与枚举一起工作

枚举是声明命名常量类型的一种方便而有组织的方式。它允许我们创建一组不同的常量数值,每个枚举字段可以是数字或基于字符串的。我们可以将我们的ImageVariables重写如下:

js
enum EImageVariables {
  width = 'w',
  height = 'h',
  aspectRatio = 'ar',
  rotate = 'a',
  opacity = 'o',
}
enum EImageVariables {
  width = 'w',
  height = 'h',
  aspectRatio = 'ar',
  rotate = 'a',
  opacity = 'o',
}

使用枚举的优点之一是我们可以使用枚举的名称作为接受值声明的类型。因此,代替以下代码:

js
type VariableValues = typeof ImageVariables[ImageFields]

function transform(value: VariableValues) {
    //perform something
}
type VariableValues = typeof ImageVariables[ImageFields]

function transform(value: VariableValues) {
    //perform something
}

我们可以使用EImageVariables重写如下:

js
function transform(value: EImageVariables) {
    //perform something
}
function transform(value: EImageVariables) {
    //perform something
}

类型检查在更少的代码中执行相同的操作😉。然而,要从已声明的枚举EImageVariables的键(或属性)获取类型,我们仍然需要像常规常量对象一样使用keyof typeof的组合:

js
type ImageFields = keyof typeof EImageVariables
type ImageFields = keyof typeof EImageVariables

就是这样。上面的代码产生的结果与使用ImageVariables时相同。现在让我们回顾一下如何从常量对象的键和值中获取类型:

js
export const ImageVariables = {
 width: 'w',
 height: 'h',
 aspectRatio: 'ar',
 rotate: 'a',
 opacity: 'o',
} as const;

type ImageVariablesType = typeof ImageVariables;
type ImageFields = keyof ImageVariablesType;
type VariableValues = ImageVariablesType[ImageFields];
export const ImageVariables = {
 width: 'w',
 height: 'h',
 aspectRatio: 'ar',
 rotate: 'a',
 opacity: 'o',
} as const;

type ImageVariablesType = typeof ImageVariables;
type ImageFields = keyof ImageVariablesType;
type VariableValues = ImageVariablesType[ImageFields];

与使用枚举相比:

js
enum EImageVariables {
  width = 'w',
  height = 'h',
  aspectRatio = 'ar',
  rotate = 'a',
  opacity = 'o',
}

type EImageVariablesType = typeof EImageVariables;
type EImageFields = keyof EImageVariablesType;
//No need for getting the type of values
enum EImageVariables {
  width = 'w',
  height = 'h',
  aspectRatio = 'ar',
  rotate = 'a',
  opacity = 'o',
}

type EImageVariablesType = typeof EImageVariables;
type EImageFields = keyof EImageVariablesType;
//No need for getting the type of values

与常量对象一样,我们可以直接在代码中使用枚举的键的值,例如EImageVariables.width。在运行时,枚举以 JavaScript 对象的形式存在于编译后的代码中,并且表现得像一个对象。

总的来说,枚举是可以的。在 TypeScript 中,由于其实现方式不够高效(希望已经修复或将很快修复),许多人对其对性能的影响提出质疑。

那么我们应该使用它们吗?这取决于您。选择权在您手中。

总结

我们在使用 TypeScript 时经常忽视像typeof或keyof这样的类型运算符,因为它们看起来过于基础。然而,它们在构建您的类型系统时起着至关重要的作用。只有当正确地结合其他 TypeScript 语法时,它们才能帮助您开发应用程序的高级和复杂类型。让我们尝试一下,看看它们能为您带来什么。