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

做者:Marius Schulz前端

译者:前端小智git

来源:Marius Schulzgithub

点赞再看,养成习惯
web


本文 GitHub:github.com/qq449245884… 上已经收录,更多往期高赞文章的分类,也整理了不少个人文档,和教程资料。欢迎Star和完善,你们面试能够参照考点复习,但愿咱们一块儿有点东西。面试

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

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元素中:app

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

原文: mariusschulz.com/blog/object… mariusschulz.com/blog/keyof-…


交流

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

github.com/qq449245884…

由于篇幅的限制,今天的分享只到这里。若是你们想了解更多的内容的话,能够去扫一扫每篇文章最下面的二维码,而后关注我们的微信公众号,了解更多的资讯和有价值的内容。

相关文章
相关标签/搜索