• TypeScript 中的 Object.keys的用法
  • 发布于 2个月前
  • 236 热度
    0 评论
  • 双人剧
  • 0 粉丝 28 篇博客
  •   
如果大家比较熟悉 TypeScript 开发,那肯定遇到过下面这种情况:
interface Options {
  hostName: string;
  port: number;
}
// 堆代码 duidaima.com
function validateOptions (options: Options) {
  Object.keys(options).forEach(key => {
    if (options[key] == null) {
        // @error {w=12} Expression of type 'string' can't be used to index type 'Options'.
      throw new Error(`Missing option ${key}`);
    }
  });
}
乍看之下,这个错误完全是莫名其妙。我们完全可以使用 options 键来访问 options ,但 TypeScript 为什么还非要报错?只要通过将 Object.keys(options) 强制转换为 (keyof typeof options)[],就能有效规避这个问题。
const keys = Object.keys(options) as (keyof typeof options)[];
keys.forEach(key => {
  if (options[key] == null) {
    throw new Error(`Missing option ${key}`);
  }
});
既然方法如此简单,TypeScript 为什么不出手解决?
查看 Object.keys 的类型定义,我们会看到如下内容:
// typescript/lib/lib.es5.d.ts
interface Object {
  keys(o: object): string[];
}
这个类型定义非常简单,即接收 object 并返回 string[]。也就是说,我们可以轻松让这个方法接收通用参数 T 并返回(keyof T)[]。
class Object {
  keys<T extends object>(o: T): (keyof T)[];
}
只要这样定义 Object.keys,就不会触发任何类型错误。所以大家第一反应肯定是把 Object.keys 定义成这样,可 TypeScript 偏没有这么做。究其原因,与 TypeScript 的结构类型系统有关。

TypeScript 中的结构类型
只要发现有属性丢失或者类型错误,TypeScript 就会马上报错。
function saveUser(user: { name: string, age: number }) {}
const user1 = { name: "Alex", age: 25 };
saveUser(user1); // OK!
const user2 = { name: "Sarah" };
saveUser(user2);
         // @error {w=5} Property 'age' is missing in type { name: string }.
const user3 = { name: "John", age: '34' };
saveUser(user3);
         // @error {w=5} Types of property 'age' are incompatible.\n  Type 'string' is not assignable to type 'number'.
但如果我们提交的是无关的属性,那 TypeScript 不会做出任何反应。
function saveUser(user: { name: string, age: number }) {}


const user = { name: "Alex", age: 25, city: "Reykjavík" };
saveUser(user); // Not a type error
这就是结构类型系统的设计思路。如果 A 是 B 的超集(即 A 包含 B 中的所有属性),则可以将类型 A 分配给 B。

但如果 A 是 B 的真超集(即 A 中的属性比 B 更多),则:
A 可被分配给 B,但
B 不可被分配给 A。

注意:除了需要是属性的超集之外,具体属性类型也有影响。
以上讲解可能过于抽象,下面咱们从更具体的例子入手。
type A = { foo: number, bar: number };
type B = { foo: number };
const a1: A = { foo: 1, bar: 2 };
const b1: B = { foo: 3 };


const b2: B = a1;
const a2: A = b1;
      // @error {w=2} Property 'bar' is missing in type 'B' but required in type 'A'.
其中的关键点在于,当我们面对一个类型 T 的对象时,也就相当于确定该对象至少包含 T 中的属性。但我们并不知道 T 是否切实存在,所以 Object.keys 的类型机制才会是现在这个样子。下面我们再举一例。

Object.keys 的不安全用法
假设我们正为某项 Web 服务创建一个端点,此端点会创建一个新用户。我们的现有 User 接口如下所示:
interface User {
  name: string;
  password: string;
}
在将用户保存至数据库之前,我们先要确保这里的 User 对象有效。
name 必须为非空。
password 必须有至少 6 个字符。
因此,我们创建一个 validators 对象,其中包含 User 中每个属性的验证函数:
const validators = {
  name: (name: string) => name.length < 1
    ? "Name must not be empty"
    : "",
  password: (password: string) => password.length < 6
    ? "Password must be at least 6 characters"
    : "",
};
之后我们再创建一个 validateUser 函数,通过这些验证器运行 User 对象:
 
function validateUser(user: User) {
  // Pass user object through the validators
}
因为我们需要验证 user 中的各个属性,所以可以用 Object.keys 迭代 user 中的属性:
function validateUser(user: User) {
  let error = "";
  for (const key of Object.keys(user)) {
    const validate = validators[key];
    error ||= validate(user[key]);
  }
  return error;
}
注意:这部分代码片段中存在类型错误,但我们暂不细究,稍后再进一步讨论。
这种方法的问题是,user 用户可能包含 validators 中不存在的属性。
interface User {
  name: string;
  password: string;
}

function validateUser(user: User) {}
const user = {
  name: 'Alex',
  password: '1234',
  email: "alex@example.com",
};
validateUser(user); // OK!
即使 User 并没有指定 email 属性,由于结构类型允许提交无关属性,所以这里也不会触发类型错误。在运行时中,email 属性会导致 validator 处于 undefined 状态,并在调用时抛出错误。
for (const key of Object.keys(user)) {
  const validate = validators[key];
  error ||= validate(user[key]);
            // @error {w=8} TypeError: 'validate' is not a function.
}
好在 TypeScript 会在这段代码实际运行之前,就提醒我们其中存在类型错误。
for (const key of Object.keys(user)) {
  const validate = validators[key];
                   // @error {w=15} Expression of type 'string' can't be used to index type '{ name: ..., password: ... }'.
  error ||= validate(user[key]);
                     // @error {w=9} Expression of type 'string' can't be used to index type 'User'.
}
现在相信大家能够理解 Object.keys 的类型为什么要这样设计了。其实质,就是强制提醒我们对象中可能包含类型系统无法识别的属性。有了以上结构类型和潜在问题的知识储备,下面我们一起来看如何发挥结构类型的设计优势。

实际运用结构类型
结构类型带来了很大的灵活性,允许接口准确声明自己需要的属性。下面还是通过实例加以演示。设想我们编写了一个函数以解析 KeyboardEvent,并返回触发器的快捷方式。
function getKeyboardShortcut(e: KeyboardEvent) {
  if (e.key === "s" && e.metaKey) {
    return "save";
  }
  if (e.key === "o" && e.metaKey) {
    return "open";
  }
  return null;
}
为了确保代码按预期工作,下面我们编写一些单元测试:
expect(getKeyboardShortcut({ key: "s", metaKey: true }))
  .toEqual("save");
expect(getKeyboardShortcut({ key: "o", metaKey: true }))
  .toEqual("open");
expect(getKeyboardShortcut({ key: "s", metaKey: false }))
  .toEqual(null);
看起来不错,但 TypeScript 会报错:
getKeyboardShortcut({ key: "s", metaKey: true });
                    // @error {w=27,shiftLeft=48} Type '{ key: string; metaKey: true; }' is missing the following properties from type 'KeyboardEvent': altKey, charCode, code, ctrlKey, and 37 more.
一个个指定 37 个额外属性根本就不现实,我们当然可以将参数转换为 KeyboardEvent 来解决这个问题:
getKeyboardShortcut({ key: "s", metaKey: true } as KeyboardEvent);
但这可能遮盖掉其他可能发生的类型错误。
所以正确的思路,应该是更新 getKeyboardShortcut 以确保仅从事件中声明它需要的属性。
interface KeyboardShortcutEvent {
  key: string;
  metaKey: boolean;
}
function getKeyboardShortcut(e: KeyboardShortcutEvent) {}
现在测试代码需要满足的条件大大收窄,处理起来自然更加轻松。函数与全局 KeyboardEvent 类型的耦合也更少,且能够在更多上下文中使用。换言之,灵活性得到显著提升。

而这一切之所以可行,显然要归功于结构类型。作为后者的超集,KeyboardEvent 可被分配给 KeyboardShortcutEvent,这就回避了 KeyboardEvent 中的 37 个不相关属性。
window.addEventListener("keydown", (e: KeyboardEvent) => {
  const shortcut = getKeyboardShortcut(e); // This is OK!
  if (shortcut) {
    execShortcut(shortcut);
  }
});

用户评论