前言

文中内容都是参考https://mariusschulz.com/blog/literal-type-widening-in-typescript 以及
https://github.com/danvk/effective-typescript 内容。

类型拓宽(Type Widening)

拓宽的字面量类型(Widening Literal Types)

const关键字申明的变量
当你用const关键字申明了一个变量,并且初始化了一个字面量值, TypeScript会将其推断为字面量类型:

const stringLiteral = "https"; // const stringLiteral: "https"const numericLiteral = 42; // const numericLiteral: 42const booleanLiteral = true;  // const booleanLiteral: truebooleanLiteral = false;// Cannot assign to 'booleanLiteral' because it is a constant.

因为有const关键字,下面的每一个变量值都不能进行批改, 所以推断为字面量类型是十分适合的。 它保留了赋值的精确类型信息。
对于null, undefined也是如此:

const a = null;// const a: nullconst b = undefined;// const b: undefined

let关键字申明的变量
如果你将下面的这些常量赋值给let申明的变量, 每一个字面量类型会被拓宽为相应的拓宽类型:

let widenedStringLiteral = stringLiteral; // let widenedStringLiteral: stringlet widenedNumericLiteral = numericLiteral; // let widenedNumericLiteral: numberlet widenedBooleanLiteral = booleanLiteral;// let widenedBooleanLiteral: booleanwidenedBooleanLiteral = 1;     // 没故障 

const关键字申明的变量不同, let关键字申明的变量初始化之后还是能够被批改的。 如果 Typescript 将每一个 let 变量都推断为字面量类型,那么之后如果想给它赋值初始值以外的值都会导致编译时报错。

基于这个起因, let关键字申明的变量会被推断为拓宽后的类型。

对于null, undefined

let a = null;// let a: anylet b = undefined;  // let b: anyconst c = null;// const c: nulllet x = c;// let x: nulllet y = b;// let y: undefinedlet z = a;// let z: null

通过 letvar 定义的变量如果满足未显式申明类型注解且被赋予了 nullundefined 值,则推断出这些变量的类型是 any

枚举类型
对于枚举类型同样也如此:

enum FlexDirection {  Row,  Column,}const enumLiteral = FlexDirection.Row;// const enumLiteral: FlexDirection.Rowlet widenedEnumLiteral = enumLiteral;// let widenedEnumLiteral: FlexDirection

总结一下字面量类型拓展的规定:

  • 字符串字面量类型会被拓宽为字符串类型
  • 数字字面量类型会被拓宽为数字类型
  • 布尔字面量类型会被拓宽为布尔类型
  • 枚举字面量类型会被拓宽为枚举类型

假如你正在编写一个向量库,你首先定义了一个 Vector3 接口,而后定义了 getComponent 函数用于获取指定坐标轴的值:

interface Vector3 {  x: number;  y: number;  z: number;}function getComponent(vector: Vector3, axis: "x" | "y" | "z") {  return vector[axis];}

然而,当你尝试应用 getComponent 函数时,TypeScript 会提醒以下错误信息:

let x = "x";// let x: stringlet vec = { x: 10, y: 20, z: 30 };// let vec: {//   x: number;//   y: number;//   z: number;// };getComponent(vec, x); // 类型“string”的参数不能赋给类型“"x" | "y" | "z"”的参数

很显著, 因为变量x的类型被推断为string类型, 而getComponent函数冀望它的第二个参数有一个更具体的类型。 这在理论场合被拓宽了, 所以导致了一个谬误。
TypeScript 提供了一些管制拓宽过程的办法。其中一种办法是应用 const。如果用 const 而不是 let 申明一个变量,那么它的类型会更窄。
应用const修复后面例子中的谬误:

const x = "x";// const x: "x"let vec = { x: 10, y: 20, z: 30 };// let vec: {//   x: number;//   y: number;//   z: number;// };getComponent(vec, x);  // 没故障

因为 x 不能从新赋值,所以 TypeScript 能够推断更窄的类型,就不会在后续赋值中呈现谬误。因为字符串字面量型 “x” 能够赋值给 “x”|”y”|”z”,所以代码会通过类型查看器的查看。
然而,const 并不是万灵药。对于对象和数组,依然会存在问题。

const mixed = ['x', 1];  

mixed变量类型应该是什么? 这里有一些可能性:

  • ('x' | 1)[]
  • ['x', 1]
  • [string, number]
  • readonly [string, number]
  • (string | number)[]
  • readonly (string|number)[]
  • [any, any]
  • any[]

没有更多的上下文,TypeScript 无奈晓得哪种类型是 "正确的" 。
对于以下代码,在JS中是没问题的:

const v = {  x: 1,};v.x = 3;  // OKv.x = '3'; v.y = 4; v.name = 'Pythagoras';

而在TypeScript中, 对于v的类型来说: 可能是

  • { readonly x: 1}
  • {x: number}
  • {[key: string]: number}

对于对象,TypeScript 的拓宽算法会将其外部属性视为将其赋值给 let 关键字申明的变量,进而来推断其属性的类型。因而 v 的类型为 {x:number} 。这使得你能够将 obj.x 赋值给其余 number 类型的变量,而不是 string 类型的变量,并且它还会阻止你增加其余属性。

const v = {  x: 1,};// const v: {//   x: number;// };v.x = 3; // 没故障v.x = "3";// 不能将类型“"3"”调配给类型“number”v.y = 4;// 类型“{ x: number; }”上不存在属性“y”v.name = "Pythagoras";// 类型“{ x: number; }”上不存在属性“name”

其余
函数的形参

let strFun = (str = "this is string") => str;// let strFun: (str?: string) => stringconst specifiedStr = "this is string";// const specifiedStr: "this is string"let str2 = specifiedStr;// let str2: stringlet strFun2 = (str = specifiedStr) => str;// let strFun2: (str?: string) => string

总结一下:
所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性 ,如果满足指定了初始值且未显式增加类型注解的条件,那么它们推断进去的类型就是指定的初始值字面量类型拓宽后的类型,这就是字面量类型拓宽。

如何笼罩 TypeScript 的默认行为?
TypeScript 试图在具体性和灵活性之间获得均衡。它须要推断一个足够具体的类型来捕捉谬误,但又不能推断出谬误的类型。它通过属性的初始化值来推断属性的类型,当然有几种办法能够笼罩 TypeScript 的默认行为。

  1. 提供显式类型正文:

    const v: { x: 1 | 3 | 5 } = {  x: 1,};// const v: {//   x: 1 | 3 | 5;// };
  2. 应用const断言
    当你在一个值之后应用 const 断言时,TypeScript 将为它推断出最窄的类型,没有拓宽。

    const v1 = {  x: 1,  y: 2,};// const v1: {//   x: number;//   y: number;// };const v2 = {  x: 1 as const,  y: 2,};// const v2: {//   x: 1;//   y: number;// };const v3 = {  x: 1,  y: 2,} as const;// const v3: {//   readonly x: 1;//   readonly y: 2;// };

    对数组应用const断言:

    const a1 = [1, 2, 3];  // const a1: number[]const a2 = [1, 2, 3] as const;// const a2: readonly [1, 2, 3]

    如果你认为类型拓宽导致了谬误,那么能够思考增加一些显式类型正文或应用 const 断言。

非拓宽的字面量类型(Non-Widening Literal Types)

你能够显式地给一个变量标注字面量类型来新建一个非拓宽字面量类型的变量:

const stringLiteral: "https" = "https"; // const stringLiteral: "https" (non-widening)const numericLiteral: 42 = 42; // const stringLiteral: "https"  (non-widening)

当把一个非拓宽字面量类型的变量赋值给另一个变量的时候,字面量类型不会被拓宽:

let widenedStringLiteral = stringLiteral; // let widenedStringLiteral: "https" (non-widening)let widenedNumericLiteral = numericLiteral; // let widenedNumericLiteral: 42 (non-widening)

留神,类型仍然是 https42。和之前不一样,之前会被别离拓宽为 stringnumber 类型。

非拓宽字面量类型的用途

以下例子中, 应用了两个拓宽后的字符串字面量类型的变量构建了一个数组:

const http = "http"; // const http: "http" (widening)const https = "https"; // const https: "https" (widening)const protocols = [http, https]; // const protocols: string[]const first = protocols[0]; // const first: stringconst second = protocols[1];// const second: string

Typescript 会将protocols推断为 string[]。因而,数组的元素 firstsecond 都会被推断为 string 类型。httphttps 的字面量类型信息在拓宽的过程中失落了。

让咱们再显式地将这两个常量标注为 http 和 https 类型:

const http: "http" = "http"; // const http: "http" (non-widening)const https: "https" = "https"; // const https: "https" (non-widening)const protocols = [http, https]; // const protocols: ("http" | "https")[]const first = protocols[0]; // const first: "http" | "https"const second = protocols[1];// const second: "http" | "https"

此时 protocols 数组会被推断为 ("http" | "https")[],这示意这个数组只能蕴含字符串 "http" 或者 "https", firstsecond 都被推断为 "http" | "https" 类型, 这是因为数组类型并没有辨别索引0和索引1地位的 "http""https" 具体类型,数组只晓得元素不论在哪个索引地位,只能蕴含这两个字面量类型。

如果你想保留数组中字符串字面量类型的地位信息,你能够显式地将这个数组标注为领有两个元素的元组类型:

const http = "http"; // const http: "http" (widening)const https = "https"; // const https: "https" (widening)const protocols: ["http", "https"] = [http, https]; // const protocols: ["http", "https"]const first = protocols[0]; // const first: "http" (non-widening)const second = protocols[1];// const second: "https" (non-widening)

当初,firstsecond 被推断为他们各自非拓宽的字符串字面量类型。

最初, 如有谬误,欢送各位大佬指导!感激!

参考资料

https://mariusschulz.com/blog/literal-type-widening-in-typescript
https://github.com/danvk/effective-typescript
https://cloud.tencent.com/developer/article/1618836