Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

【TypeScript 演化史 -- 6】对象扩展运算符和 rest 运算符及 keyof 和查找类型 #155

Open
husky-dot opened this issue Nov 29, 2019 · 0 comments

Comments

@husky-dot
Copy link
Owner

作者:Marius Schulz

译者:前端小智

来源:Marius Schulz


阿里云最近在做活动,低至2折,真心觉得很划算了,可以点击本条内容或者链接进行参与
https://promotion.aliyun.com/ntms/yunparter/invite.html?userCode=pxuujn3r

腾讯云最近在做活动,百款云产品低至 1 折,可以点击本条内容或者链接进行参与


为了保证的可读性,本文采用意译而非直译。

TypeScript 2.1 增加了对 对象扩展运算和 rest 属性提案的支持,该提案在 ES2018 中标准化。可以以类型安全的方式使用 restspread 属性。

对象 rest 属性

假设已经定义了一个具有三个属性的简单字面量对象

const marius = {
  name: "Marius Schulz",
  website: "https://mariusschulz.com/",
  twitterHandle: "@mariusschulz"
};

使用 ES6 解构语法,可以创建几个局部变量来保存相应属性的值。TypeScript 将正确地推断每个变量的类型:

const { name, website, twitterHandle } = marius;

name;          // Type string
website;       // Type string
twitterHandle; // Type string

这些都是正确的,但这到现在也啥新鲜的。除了提取感兴趣的一组属性之外,还可以使用...语法将所有剩余的属性收集到rest元素中:

const { twitterHandle, ...rest } = marius;

twitterHandle; // Type string
rest; // Type { name: string; website: string; }

TypeScript 会为得到结果的局部变量确定正确的类型。虽然 twitterHandle 变量是一个普通的字符串,但 rest 变量是一个对象,其中包含剩余两个未被解构的属性。

对象扩展属性

假设咱们希望使用 fetch() API 发出 HTTP 请求。它接受两个参数:一个 URL 和一个 options 对象,options 包含请求的任何自定义设置。

在应用程序中,可以封装对fetch()的调用,并提供默认选项和覆盖给定请求的特定设置。这些配置项类似如下:

const defaultOptions = {
  method: "GET",
  credentials: "same-origin"
};

const requestOptions = {
  method: "POST",
  redirect: "follow"
};

使用对象扩展,可以将两个对象合并成一个新对象,然后传递给 fetch() 方法

// Type { method: string; redirect: string; credentials: string; }
const options = {
  ...defaultOptions,
  ...requestOptions
};

对象扩展属性创建一个新对象,复制 defaultOptions 中的所有属性值,然后按照从左到右的顺序复制requestOptions中的所有属性值,最后得到的结果如下:

console.log(options);
// {
//   method: "POST",
//   credentials: "same-origin",
//   redirect: "follow"
// }

请注意,分配顺序很重要。如果一个属性同时出现在两个对象中,则后分配的会替换前面的。

当然,TypeScript 理解这种顺序。因此,如果多个扩展对象使用相同的键定义一个属性,那么结果对象中该属性的类型将是最后一次赋值的属性类型,因为它覆盖了先前赋值的属性:

const obj1 = { prop: 42 };
const obj2 = { prop: "Hello World" };

const result1 = { ...obj1, ...obj2 }; // Type { prop: string }
const result2 = { ...obj2, ...obj1 }; // Type { prop: number }

制作对象的浅拷贝

对象扩展可用于创建对象的浅拷贝。假设咱希望通过创建一个新对象并复制所有属性来从现有todo项创建一个新todo项,使用对象就可以轻松做到:

const todo = {
  text: "Water the flowers",
  completed: false,
  tags: ["garden"]
};

const shallowCopy = { ...todo };

实际上,你会得到一个新对象,所有的属性值都被复制:

console.log(todo === shallowCopy);
// false

console.log(shallowCopy);
// {
//   text: "Water the flowers",
//   completed: false,
//   tags: ["garden"]
// }

现在可以修改text属性,但不会修改原始的todo项:

hallowCopy.text = "Mow the lawn";

console.log(shallowCopy);
// {
//   text: "Mow the lawn",
//   completed: false,
//   tags: ["garden"]
// }

console.log(todo);
// {
//   text: "Water the flowers",
//   completed: false,
//   tags: ["garden"]
// }

但是,新的todo项引用与第一个相同的 tags 数组。由于是浅拷贝,改变数组将影响这两个todo

shallowCopy.tags.push("weekend");

console.log(shallowCopy);
// {
//   text: "Mow the lawn",
//   completed: false,
//   tags: ["garden", "weekend"]
// }

console.log(todo);
// {
//   text: "Water the flowers",
//   completed: false,
//   tags: ["garden", "weekend"]
// }

如果想创建一个序列化对象的深拷贝,可以考虑使用 JSON.parse(JSON.stringify(obj)) 或其他方法,如 object.assign()。对象扩展仅拷贝属性值,如果一个值是对另一个对象的引用,则可能导致意外的行为。

keyof 和查找类型

JS 是一种高度动态的语言。在静态类型系统中捕获某些操作的语义有时会很棘手。以一个简单的 prop 函数为例:

function prop(obj, key) {
  return obj[key];
}

它接受一个对象和一个键,并返回相应属性的值。一个对象的不同属性可以有完全不同的类型,咱们甚至不知道 obj 是什么样子的。

那么如何在 TypeScript 中编写这个函数呢?先尝试一下:

有了这两个类型注释,obj 必须是对象,key 必须是字符串。咱们现在已经限制了两个参数的可能值集。然而,TS 仍然推断返回类型为 any

const todo = {
  id: 1,
  text: "Buy milk",
  due: new Date(2016, 11, 31)
};

const id = prop(todo, "id");     // any
const text = prop(todo, "text"); // any
const due = prop(todo, "due");   // any

如果没有更进一步的信息,TypeScript 就不知道将为 key 参数传递哪个值,所以它不能推断出prop函数的更具体的返回类型。咱们需要提供更多的类型信息来实现这一点。

keyof 操作符号

在 JS 中属性名称作为参数的 API 是相当普遍的,但是到目前为止还没有表达在那些 API 中出现的类型关系。

TypeScript 2.1 新增加 keyof 操作符。输入索引类型查询或 keyof,索引类型查询keyof T产生的类型是 T 的属性名称。假设咱们已经定义了以下 Todo 接口:

interface Todo {
  id: number;
  text: string;
  due: Date;
}

各位可以将 keyof 操作符应用于 Todo 类型,以获得其所有属性键的类型,该类型是字符串字面量类型的联合

type TodoKeys = keyof Todo; // "id" | "text" | "due"

当然,各位也可以手动写出联合类型 "id" | "text" | "due",而不是使用 keyof,但是这样做很麻烦,容易出错,而且维护起来很麻烦。而且,它应该是特定于Todo类型的解决方案,而不是通用的解决方案。

索引类型查询

有了 keyof,咱们现在可以改进 prop 函数的类型注解。我们不再希望接受任意字符串作为 key 参数。相反,咱们要求参数 key 实际存在于传入的对象的类型上

function prop <T, K extends keyof T>(obj: T, key: K) {
  return obj[key]
}

TypeScript 现在以推断 prop 函数的返回类型为 T[K],这个就是所谓的 索引类型查询查找类型。它表示类型 T 的属性 K 的类型。如果现在通过 prop 方法访问下面 todo 的三个属性,那么每个属性都有正确的类型:

const todo = {
  id: 1,
  text: "Buy milk",
  due: new Date(2016, 11, 31)
};

const id = prop(todo, "id");     // number
const text = prop(todo, "text"); // string
const due = prop(todo, "due");   // Date

现在,如果传递一个 todo 对象上不存在的键会发生什么

编译器会报错,这很好,它阻止咱们试图读取一个不存在的属性。

另一个真实的示例,请查看与TypeScript编译器一起发布的 lib.es2017.object.d.ts 类型声明文件中Object.entries()方法:

interface ObjectConstructor {
  // ...
  entries<T extends { [key: string]: any }, K extends keyof T>(o: T): [keyof T, T[K]][];
  // ...
}

entries 方法返回一个元组数组,每个元组包含一个属性键和相应的值。不可否认,在返回类型中有大量的方括号,但是我们一直在寻找类型安全性。


编辑中可能存在的bug没法实时知道,事后为了解决这些bug,花了大量的时间进行log 调试,这边顺便给大家推荐一个好用的BUG监控工具 Fundebug

原文:
https://mariusschulz.com/blog/object-rest-and-spread-in-typescript
https://mariusschulz.com/blog/keyof-and-lookup-types-in-typescript


交流

干货系列文章汇总如下,觉得不错点个Star,欢迎 加群 互相学习。

https://github.com/qq449245884/xiaozhi

因为篇幅的限制,今天的分享只到这里。如果大家想了解更多的内容的话,可以去扫一扫每篇文章最下面的二维码,然后关注咱们的微信公众号,了解更多的资讯和有价值的内容。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant