阿里、蚂蚁、晟腾、中科加禾精彩分享 AI 基础设施洞见,现购票可享受 9 折优惠 |AICon 了解详情
写点什么

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:002883

评论

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

Go 入门很简单:如何在 Go 中使用日志包

宇宙之一粟

日志 log Go 语言 4月月更

Apache ShardingSphere 如何实现分布式事务

SphereEx

Apache 数据库 ShardingSphere SphereEx

Android C++系列:C++最佳实践6 constexpr与decltype

轻口味

c++ android ndk jni 4月月更

Windows注册表内容

Sher10ck

注册表

组件容器化Statefulset与Deployment的选型与实践

移动云大数据

Deployment statefulset

TASKCTL 资源树刷新、重载和重置的不同区别

TASKCTL

DevOps 分布式 敏捷开发 自动化运维 大数据运维

Build On 活动预告 | 构建你的第一个基于知识图谱的推荐模型

亚马逊云科技 (Amazon Web Services)

云计算

阿里云发布中国云原生数据湖应用洞察白皮书

阿里云大数据AI技术

大数据 数据湖

为什么需要可编程代理

Flomesh

负载均衡 代理 Pipy 可编程代理

Git技巧小能手

西门阿杰

git 版本控制

【高并发】如何解决可见性和有序性问题?这次彻底懂了!

冰河

并发编程 多线程 协程 异步编程 精通高并发系列

行业大咖齐聚 多元主题碰撞 OpenHarmony技术日等你来

Geek_283163

OpenAtom OpenHarmony

[Day20]-[动态规划]零钱兑换 II

方勇(gopher)

LeetCode 动态规划 数据结构算法

微信小程序开发系列(一) :开发环境搭建和微信小程序的视图设计与开发

Jerry Wang

微信小程序 微信 开发视图 微信小程序专栏 4月月更

架构实战营-外包学生管理系统架构文档

CityAnimal

架构实战营 #架构实战营 「架构实战营」

百度小程序包流式下载安装优化

百度Geek说

小程序

融云首席科学家任杰:互联网兵无常势,但总有人正年轻

融云 RongCloud

解决报错:SSL certificate problem: certificate has expired

liuzhen007

SSL证书 4月月更

深入解析require源码,知其根,洞其源

战场小包

前端 Node 4月月更

Linux驱动框架与杂项字符设备框架介绍

DS小龙哥

4月月更

湖仓一体,Hologres加速云数据湖DLF技术原理解析

阿里云大数据AI技术

sql 大数据 分布式计算 Cloud Native MaxCompute

学生管理系统架构文档

小虾米

架构实战营

Mybatis-plus在项目中的应用

Rubble

4月日更 4月月更

HDFS小文件分析实践

移动云大数据

hdfs 小文件

什么是域名(Domain Name ) ?

源字节1号

Cilium 原理解析:网络数据包在内核中的流转过程

火山引擎边缘云

Linux 云原生 网络

制约国内企业知识管理发展的因素

小炮

知识管理 企业知识管理 企业知识管理工具

GAIA-IR: GraphScope 上的并行化图查询引擎

6979阿强

大数据 并行计算 图计算 GraphScope 图分析

面试突击41:notify是随机唤醒吗?

王磊

Java java面试

spring-cloud-kubernetes与SpringCloud Gateway

程序员欣宸

Java spring-cloud 4月月更

预备,请关注!DingOS先导小视频发布!

鼎道智联

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