【TypeScript 演化史 -- 7】映射类型和更好的字面量类型推断

做者:Marius Schulz
译者:前端小智
来源:Marius Schulz

干货系列文章汇总以下,以为不错点个Star:前端

Github: https://github.com/qq44924588...](https://github.com/qq44924588...git


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

TypeScript 2.1 引入了映射类型,这是对类型系统的一个强大的补充。本质上,映射类型容许w我们经过映射属性类型从现有类型建立新类型。根据我们指定的规则转换现有类型的每一个属性。转换后的属性组成新的类型。算法

使用映射类型,能够捕获类型系统中相似 Object.freeze() 等方法的效果。冻结对象后,就不能再添加、更改或删除其中的属性。来看看如何在不使用映射类型的状况下在类型系统中对其进行编码:typescript

interface Point {
  x: number;
  y: number;
}

interface FrozenPoint {
  readonly x: number;
  readonly y: number;
}

function freezePoint(p: Point): FrozenPoint {
  return Object.freeze(p);
}

const origin = freezePoint({ x: 0, y: 0 });

// Error! Cannot assign to 'x' because it
// is a constant or a read-only property.
origin.x = 42;

我们定义了一个包含 xy 两个属性的 Point 接口,我们还定义了另外一个接口FrozenPoint,它与 Point 相同,只是它的全部属性都被使用 readonly 定义为只读属性。segmentfault

freezePoint 函数接受一个 Point 做为参数并冻结该参数,接着,向调用者返回相同的对象。然而,该对象的类型已更改成FrozenPoint,所以其属性被静态类型化为只读。这就是为何当试图将 42 赋值给 x 属性时,TypeScript 会出错。在运行时,分配要么抛出一个类型错误(严格模式),要么静默失败(非严格模式)。 api

虽然上面的示例能够正确地编译和工做,但它有两大缺点微信

  1. 须要两个接口。除了 Point 类型以外,还必须定义 FrozenPoint 类型,这样才能将 readonly 修饰符添加到两个属性中。当我们更改 Point 时,还必须更改FrozenPoint,这很容易出错,也很烦人。
  2. 须要 freezePoint 函数。对于但愿在应用程序中冻结的每种类型的对象,我们就必须定义一个包装器函数,该函数接受该类型的对象并返回冻结类型的对象。没有映射类型,我们就不能以通用的方式静态地使用 Object.freeze()

使用映射类型构建 Object.freeze()

来看看 Object.freeze()是如何在 lib.d.ts 文件中定义的:app

/**
  * Prevents the modification of existing property attributes and values, and prevents the addition of new properties.
  * @param o Object on which to lock the attributes.
  */
freeze<T>(o: T): Readonly<T>;

该方法的返回类型为Readonly<T>,这是一个映射类型,它的定义以下:函数

type Readonly<T> = {
  readonly [P in keyof T]: T[P]
};

这个语法一开始可能会让人望而生畏,我们来一步一步分析它:

  • 用一个名为 T 的类型参数定义了一个泛型 Readonly。
  • 在方括号中,使用了 keyof 操做符。keyof TT 类型的全部属性名表示为字符串字面量类型的联合。
  • 方括号中的 in 关键字表示咱们正在处理映射类型。[P in keyof T]: T[P]表示将 T类型的每一个属性 P 的类型转换为 T[P]。若是没有readonly修饰符,这将是一个身份转换。
  • 类型 T[P] 是一个查找类型,它表示类型 T 的属性 P 的类型。
  • 最后,readonly 修饰符指定每一个属性都应该转换为只读属性。

由于 Readonly<T> 类型是泛型的,因此我们为T提供的每种类型都正确地入了Object.freeze() 中。

const origin = Object.freeze({ x: 0, y: 0 });

// Error! Cannot assign to 'x' because it
// is a constant or a read-only property.
origin.x = 42;

映射类型的语法更直观解释

此次我们使用 Point 类型为例来粗略解释类型映射如何工做。请注意,如下只是出于解释目的,并不能准确反映TypeScript使用的解析算法。

从类型别名开始:

type ReadonlyPoint = Readonly<Point>;

如今,我们能够在 Readonly<T> 中为泛型类型 T 的替换 Point 类型:

type ReadonyPoint = {
  readonly [P in keyof Point]: Point[P]
};

如今我们知道 TPoint,能够肯定keyof Point表示的字符串字面量类型的并集:

type ReadonlyPoint = {
  readonly [P in "x" | "y"]: Point[p]
};

类型 P 表示每一个属性 xy,我们把它们做为单独的属性来写,去掉映射的类型语法

type ReadonlyPoint = {
  readonly x: Point["x"];
  readonly y: Point["y"];
};

最后,我们能够解析这两种查找类型,并将它们替换为具体的 xy 类型,这两种类型都是 number

type ReadonlyPoint = {
  readonly x: number;
  readonly y: number;
};

最后,获得的 ReadonlyPoint 类型与我们手动建立的 FrozenPoint 类型相同。



clipboard.png


更多映射类型的示例

上面已经看到 lib.d.ts 文件中内置的 Readonly <T> 类型。此外,TypeScript 定义了其余映射类型,这些映射类型在各类状况下都很是有用。以下:

/**
 * Make all properties in T optional
 */
type Partial<T> = {
  [P in keyof T]?: T[P]
};

/**
 * From T pick a set of properties K
 */
type Pick<T, K extends keyof T> = {
  [P in K]: T[P]
};

/**
 * Construct a type with a set of properties K of type T
 */
type Record<K extends string, T> = {
  [P in K]: T
};

这里还有两个关于映射类型的例子,若是须要的话,能够本身编写:

/**
 * Make all properties in T nullable
 */
type Nullable<T> = {
  [P in keyof T]: T[P] | null
};

/**
 * Turn all properties of T into strings
 */
type Stringify<T> = {
  [P in keyof T]: string
};

映射类型和联合的组合也是颇有趣:

type X = Readonly<Nullable<Stringify<Point>>>;
// type X = {
//     readonly x: string | null;
//     readonly y: string | null;
// };

映射类型的实际用例

实战中常常能够看到映射类型,来看看 React 和 Lodash :

  • React:组件的 setState 方法容许我们更新整个状态或其中的一个子集。我们能够更新任意多个属性,这使得setState方法成为 Partial<T> 的一个很好的用例。
  • Lodashpick 函数从一个对象中选择一组属性。该方法返回一个新对象,该对象只包含我们选择的属性。可使Pick<T>该行为进行构建,正如其名称所示。

更好的字面量类型推断

字符串、数字和布尔字面量类型(如:"abc"1true)以前仅在存在显式类型注释时才被推断。从 TypeScript 2.1 开始,字面量类型老是推断为默认值。在 TypeScript 2.0 中,类型系统扩展了几个新的字面量类型:

  • boolean 字面量类型
  • 数字字面量
  • 枚举字面量

不带类型注解的 const 变量或 readonly 属性的类型推断为字面量初始化的类型。已经初始化且不带类型注解的 let 变量、var 变量、形参或非 readonly 属性的类型推断为初始值的扩展字面量类型。字符串字面量扩展类型是 string,数字字面量扩展类型是number,truefalse 的字面量类型是 boolean,还有枚举字面量扩展类型是枚举。

更好的 const 变量推断

我们从局部变量和 var 关键字开始。当 TypeScript 看到下面的变量声明时,它会推断baseUrl变量的类型是 string

var baseUrl = "https://example.com/";
// 推断类型: string

let 关键字声明的变量也是如此

let baseUrl = "https://example.com/";
// 推断类型: string

这两个变量都推断为string类型,由于它们能够随时更改。它们是用一个字面量字符串值初始化的,可是之后能够修改它们。

可是,若是使用const关键字声明变量并使用字符串字面量进行初始化,则推断的类型再也不是 string,而是字面量类型

const baseUrl = "https://example.com/";
// 推断类型: "https://example.com/"

因为常量字符串变量的值永远不会改变,所以推断出的类型会更加的具体。 baseUrl 变量没法保存 "https://example.com/" 之外的任何其余值。

字面量类型推断也适用于其余原始类型。若是用直接的数值或布尔值初始化常量,推断出的仍是字面量类型

const HTTPS_PORT = 443;
// 推断类型: 443

const rememberMe = true;
// 推断类型: true

相似地,当初始化器是枚举值时,推断出的也是字面量类型:

enum FlexDirection {
  Row,
  Column
}

const direction = FlexDirection.Column;
// 推断类型: FlexDirection.Column

注意,direction 类型为 FlexDirection.Column,它是枚举字面量类型。若是使用letvar 关键字来声明 direction 变量,那么它的推断类型应该是 FlexDirection

更好的只读属性推断

与局部 const 变量相似,带有字面量初始化的只读属性也被推断为字面量类型

class ApiClient {
  private readonly baseUrl = "https://api.example.com/";
  // 推断类型: "https://api.example.com/"

  get(endpoint: string) {
    // ...
  }
}

只读类属性只能当即初始化,也能够在构造函数中初始化。试图更改其余位置的值会致使编译时错误。所以,推断只读类属性的字面量类型是合理的,由于它的值不会改变。

固然,TypeScript 不知道在运行时发生了什么:用 readonly 标记的属性能够在任什么时候候被一些JS 代码改变。readonly 修饰符只限制从 TypeScript 代码中对属性的访问,在运行时就无能为力。也就是说,它会被编译时删除掉,不会出如今生成的 JS 代码中。

推断字面量类型的有用性

你可能会问本身,为何推断 const 变量和 readonly 属性为字面量类型是有用的。考虑下面的代码:

const HTTP_GET = "GET"; // 推断类型: "GET"
const HTTP_POST = "POST"; // 推断类型: "POST"

function get(url: string, method: "GET" | "POST") {
  // ...
}

get("https://example.com/", HTTP_GET);

若是推断 HTTP_GET 常量的类型是 string 而不是 “GET”,则会出现编译时错误,由于没法将HTTP_GET 做为第二个参数传递给get函数:

Argument of type 'string' is not assignable to parameter of type '"GET" | "POST"'

固然,若是相应的参数只容许两个特定的字符串值,则不容许将任意字符串做为函数参数传递。可是,当为两个常量推断字面量类型“GET”“POST”时,一切就都解决了。


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

原文:
https://mariusschulz.com/blog...
https://mariusschulz.com/blog...


交流

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

https://github.com/qq449245884/xiaozhi

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

clipboard.png

相关文章
相关标签/搜索