TypeScript 2.1 增加了对 对象扩展运算和 rest 属性提案的支持,该提案在 ES2018 中标准化。可以以类型安全的方式使用 rest
和 spread
属性。
对象 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
方法返回一个元组数组,每个元组包含一个属性键和相应的值。不可否认,在返回类型中有大量的方括号,但是我们一直在寻找类型安全性。
原文:
https://mariusschulz.com/blog/object-rest-and-spread-in-typescript