2025上半年,最新 AI实践都在这!20+ 应用案例,任听一场议题就值回票价 了解详情
写点什么

TypeScript 中的 Object.keys 类型为什么非要这么设计?

作者 | alexharri

  • 2023-08-28
    北京
  • 本文字数:3597 字

    阅读完需:约 12 分钟

TypeScript中的Object.keys类型为什么非要这么设计?

如果大家比较熟悉 TypeScript 开发,那肯定遇到过下面这种情况:

 

interface Options {  hostName: string;  port: number;}

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);  }});

复制代码

 

原文链接:


https://alexharri.com/blog/typescript-structural-typing

 相关阅读:


TypeScript 与 JavaScript:你应该知道的区别

“TypeScript 不值得!”前端框架 Svelte 作者宣布重构代码,反向迁移到 JavaScript 引争议

Typescript- 类型检测和变量的定义

理论 + 实践:从原型链到继承模式,掌握 Object 的精髓

2023-08-28 08:003285

评论

发布
暂无评论
发现更多内容

java培训 @Autowired 的实现原理

@零度

JAVA开发

代码注释的艺术,优秀代码真的不需要注释吗?

阿里巴巴中间件

阿里云 代码 技术分享

趣步运动挖矿系统开发模式详情

开发微hkkf5566

Seata 企业版正式开放公测

阿里巴巴中间件

阿里云 云原生 seata

最全!即学即会 Serverless Devs 基础入门(下)

Serverless Devs

技术分享| 浅谈调度平台设计

anyRTC开发者

Web 音视频 指挥调度 调度 快对讲

web前端培训 SCSS 使用技巧总结分享

@零度

前端开发 scss

淘金家园NFT链游系统开发模式详解

开发微hkkf5566

IET技能调研首次落地亚洲国家 为阿联酋工程技术发展提供洞察

E科讯

Hoo研究院|5月下半月区块链行业投资机构动向

区块链前沿News

Hoo

知名巧克力全球召回,区块链帮你摆脱“甜蜜陷阱”

旺链科技

区块链 产业区块链 食品安全 食品溯源

5G发牌三周年 云网融合加速 如何解决企业网络之忧?

郑州埃文科技

IP地址 网络空间安全 5G 企业上云 云网融合

如何正确的评测视频画质

百度Geek说

视屏质量

Websocket 10分钟快速入门

领创集团Advance Intelligence Group

websocket

函数计算异步任务能力介绍 - 任务触发去重

阿里巴巴中间件

阿里云 云原生 函数计算 FC

最全!即学即会 Serverless Devs 基础入门(上)

Serverless Devs

大数据培训Linux常用命令总结分享

@零度

大数据 Linux

Apache Kyuubi 高可用的云原生实现

移动云大数据

高可用 云原生 Kyuubi

阿里超大规模 Flink 集群运维实践

阿里云大数据AI技术

大数据 flink 运维

GraphX 图计算实践之模式匹配抽取特定子图

NebulaGraph

图数据库 知识图谱 Nebula Graph

分布式数据库下子查询和 Join 等复杂 SQL 如何实现?

TDengine

数据库 tdengine 时序数据库

WEB3:什么是去中心化数据库

devpoint

分布式 去中心化 Web3.0 6月月更 InfoQ极客传媒15周年庆

spring-cloud-k8s 跨 NS 的坑

Damon

微服务架构 云原生 6月月更

淘金家园NFT链游系统开发逻辑分析

开发微hkkf5566

软件定义汽车时代下的云原生转型实战方法论

York

容器 云原生 车联网 软件架构 6月月更

中兴新支点加入龙蜥社区,共建操作系统开源新生态

OpenAnolis小助手

开源 龙蜥社区 合作 CLA 中兴新支点

企业竞争利器——知识管理

小炮

直播回顾:隐私计算的关键技术以及行业应用技巧 | 龙蜥技术

OpenAnolis小助手

开源 直播 隐私计算 龙蜥社区 密码技术

5G、AI、云计算,什么是云渲染?

Finovy Cloud

人工智能 云服务器 GPU服务器

TypeScript中的Object.keys类型为什么非要这么设计?_工程化_InfoQ精选文章