前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >理解 TypeScript 类型拓宽

理解 TypeScript 类型拓宽

作者头像
阿宝哥
发布2020-04-22 17:09:17
1.6K0
发布2020-04-22 17:09:17
举报
文章被收录于专栏:全栈修仙之路全栈修仙之路

创建了一个 “重学TypeScript” 的微信群,想加群的小伙伴,加我微信 “semlinker”,备注重学TS。 本文是 ”重学TS系列“ 第 30 篇文章,感谢您的阅读!

一、类型拓宽

之前的文章,我们已经介绍了 TypeScript 的类型收窄,本文我们将介绍 TypeScript 的类型拓宽。在一些情况下,TypeScript 从上下文推断类型,减少了程序员显式指定明显类型的需要。例如:

代码语言:javascript
复制
let name = "semlinker";

此时变量 name 的类型会被推断为 string 基本类型,因为这是用于初始化它的值的类型。从表达式推断变量、属性或函数结果的类型时,源类型的拓宽形式用作目标的推断类型。类型的拓宽是所有出现的空类型和未定义类型都被类型 any 替换。

以下示例显示了拓宽类型以产生推断的变量类型的结果。

代码语言:javascript
复制
let a = null;                 // let a: any  
let b = undefined;            // let b: any  
let c = { x: 0, y: null };    // let c: { x: number, y: null }  
let d = [ null, undefined ];  // let d: (null | undefined)[]

在运行时,每个变量都有一个值。但是在静态分析时,当 TypeScript 检查你的代码时,变量含有一组可能的值和类型。当你使用常量初始化变量但不提供类型时,类型检查器需要确定一个。换句话说,它需要根据你指定的单个值来确定一组可能的值。在 TypeScript 中,此过程称为拓宽。理解它可以帮助你理解错误并更有效地使用类型注释。

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

代码语言:javascript
复制
interface Vector3 {
  x: number;
  y: number;
  z: number;
}

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

但是,当你尝试使用 getComponent 函数时,TypeScript 会提示以下错误信息:

代码语言:javascript
复制
let x = "x";
let vec = { x: 10, y: 20, z: 30 };

// Argument of type 'string' is not assignable to parameter of type 
// '"x" | "y" | "z"'.(2345)
getComponent(vec, x); // Error

为什么会出现上述错误呢?通过 TypeScript 的错误提示消息,我们知道是因为变量 x 的类型被推断为 string 类型,而 getComponent 函数期望它的第二个参数有一个更具体的类型。这在实际场合中被拓宽了,所以导致了一个错误。

这个过程是复杂的,因为对于任何给定的值都有许多可能的类型。例如:

代码语言:javascript
复制
const mixed = ['x', 1];

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

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

没有更多的上下文,TypeScript 无法知道哪种类型是 “正确的”,它必须猜测你的意图。尽管 TypeScript 很聪明,但它无法读懂你的心思。它不能保证 100% 正确,正如我们刚才看到的那样的疏忽性错误。

在最初的例子中,变量 x 的类型被推断为字符串,因为 TypeScript 允许这样的代码:

代码语言:javascript
复制
let x = 'semlinker';
x = 'kakuqo';
x = 'lolo';

对于 JavaScript 来说,以下代码也是合法的:

代码语言:javascript
复制
let x = 'x';
x = /x|y|z/;
x = ['x', 'y', 'z'];

在推断 x 的类型为字符串时,TypeScript 试图在特殊性和灵活性之间取得平衡。一般规则是,变量的类型在声明之后不应该改变,因此 string 比 string|RegExp 或 string|string[] 或任何字符串更有意义。

TypeScript 提供了一些控制拓宽过程的方法。其中一种方法是使用 const。如果用 const 而不是 let 声明一个变量,那么它的类型会更窄。事实上,使用 const 可以帮助我们修复前面例子中的错误:

代码语言:javascript
复制
const x = "x"; // type is "x" 
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // OK

因为 x 不能重新赋值,所以 TypeScript 可以推断更窄的类型,就不会在后续赋值中出现错误。因为字符串字面量型 “x” 可以赋值给 “x”|”y”|”z”,所以代码会通过类型检查器的检查。

然而,const 并不是万灵药。对于对象和数组,仍然会存在问题。前面的 mixed 示例说明了数组的问题:TypeScript 应该推断 mixed 类型为元组类型吗?它应该为 mixed 推断出什么类型?对象也会出现类似的问题。

以下这段代码在 JavaScript 中是没有问题的:

代码语言:javascript
复制
const obj = { 
  x: 1,
}; 

obj.x = 6; 
obj.x = '6';

obj.y = 8;
obj.name = 'semlinker';

而在 TypeScript 中,对于 obj 的类型来说,它可以是 {readonly x:1} 类型,或者是更通用的 {x:number} 类型。当然也可能是 {[key: string]: number} 或 object 类型。对于对象,TypeScript 的拓宽算法会将其内部属性视为将其赋值给 let 关键字声明的变量,进而来推断其属性的类型。因此 obj 的类型为 {x:number} 。这使得你可以将 obj.x 赋值给其他 number 类型的变量,而不是 string 类型的变量,并且它还会阻止你添加其他属性。

因此最后三行的语句会出现错误:

代码语言:javascript
复制
const obj = { 
  x: 1,
};

obj.x = 6; // OK 

// Type '"6"' is not assignable to type 'number'.
obj.x = '6'; // Error

// Property 'y' does not exist on type '{ x: number; }'.
obj.y = 8; // Error

// Property 'name' does not exist on type '{ x: number; }'.
obj.name = 'semlinker'; // Error

TypeScript 试图在具体性和灵活性之间取得平衡。它需要推断一个足够具体的类型来捕获错误,但又不能推断出错误的类型。它通过属性的初始化值来推断属性的类型,当然有几种方法可以覆盖 TypeScript 的默认行为。一种是提供显式类型注释:

代码语言:javascript
复制
// Type is { x: 1 | 3 | 5; }
const obj: { x: 1 | 3 | 5 } = {
  x: 1 
};

另一种方法是使用 const 断言。不要将其与 let 和 const 混淆,后者在值空间中引入符号。这是一个纯粹的类型级构造。让我们来看看以下变量的不同推断类型:

代码语言:javascript
复制
// Type is { x: number; y: number; }
const obj1 = { 
  x: 1, 
  y: 2 
}; 

// Type is { x: 1; y: number; }
const obj2 = {
  x: 1 as const,
  y: 2,
}; 

// Type is { readonly x: 1; readonly y: 2; }
const obj3 = {
  x: 1, 
  y: 2 
} as const;

当你在一个值之后使用 const 断言时,TypeScript 将为它推断出最窄的类型,没有拓宽。对于真正的常量,这通常是你想要的。当然你也可以对数组使用 const 断言:

代码语言:javascript
复制
// Type is number[]
const arr1 = [1, 2, 3]; 

// Type is readonly [1, 2, 3]
const arr2 = [1, 2, 3] as const;

如果你认为类型拓宽导致了错误,那么可以考虑添加一些显式类型注释或使用 const 断言。接下来我们来简单介绍一下字面量类型的拓宽。

二、非拓宽字面量类型

你可以通过显式地将变量标注为字面量类型来创建非拓宽字面量类型的变量:

代码语言:javascript
复制
// Type "https" (non-widening)
const stringLiteral: "https" = "https"; 
// Type 10 (non-widening)
const numericLiteral: 10 = 10;

将含有非拓宽字面量类型的变量赋给另一个变量时,比如以下示例中的 widenedStringLiteral 变量,该变量的类型不会被拓宽:

代码语言:javascript
复制
// Type "https" (non-widening)
const stringLiteral: "https" = "https"; 
// Type 10 (non-widening)
const numericLiteral: 10 = 10; 

// Type "https" (non-widening)
let widenedStringLiteral = stringLiteral; 
// Type 10 (non-widening)
let widenedNumericLiteral = numericLiteral;

注意,此时 widenedStringLiteral 和 widenedNumericLiteral 变量的类型仍然是 “https” 和 10。

三、非拓宽字面量类型的用处

为了理解为什么非拓宽的字面量是有用的,让我们再来看一下拓宽的字面量类型。在下面的例子中,我们通过两个拓宽的字符串字面量类型来创建数组:

代码语言:javascript
复制
// Type "http" (widening)
const http = "http";
// Type "https" (widening)
const https = "https"; 

// Type string[]
const protocols = [http, https]; 

const first = protocols[0]; // Type string
const second = protocols[1]; // Type string

TypeScript 推断出 protocols 的类型是 string[]。因此数组元素 firstsecond 的类型被认为是 string 类型。字面量类型 “http” 和 “https” 的概念在拓宽过程中丢失了。

如果你显式地把两个常量的类型分别设置为 http 和 https 的类型,那么protocols 常量的类型将被推断为 ("http" | "https")[]

代码语言:javascript
复制
// Type "http" (non-widening)
const http: "http" = "http";
// Type "https" (non-widening)
const https: "https" = "https";

// Type ("http" | "https")[]
const protocols = [http, https]; 

// Type "http" | "https"
const first = protocols[0];
// Type "http" | "https"
const second = protocols[1];

现在 first 和 second 的类型将是 "http" | "https"。这是因为我们并没有显式声明数组索引 0 和索引 1 处值的类型分别为 httphttps。它只是声明该数组只包含两个字面量类型的值,不管在哪个位置,也没有说明数组的长度。

假设出于某种原因,我们希望保留数组中字符串字面量类型的位置信息,这时我们可以显式地将 protocols 的类型设置为元组类型:

代码语言:javascript
复制
// Type "http" (widening)
const http = "http"; 
// Type "https" (widening)
const https = "https"; 

// Type ["http", "https"]
const protocols: ["http", "https"] = [http, https]; 

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

现在,first 和 second 变量的类型被推断为各自的非拓宽字符串字面量类型。

四、参考资源

  • literal-type-widening-in-typescript
  • 62 Specific Ways to Improve Your TypeScript
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020/04/21,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、类型拓宽
  • 二、非拓宽字面量类型
  • 三、非拓宽字面量类型的用处
  • 四、参考资源
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档