系列文章目录
引入一:Typescript基础引入(基础类型、元组、枚举)
引入二:Typescript面向对象引入(接口、类、多态、重写、抽象类、访问修饰符)
第一章:Typescript基础知识(Typescript介绍、搭建TypeScript环境、基本数据类型)
第二章:Typescript常用类型(任意值any、数组Array、函数Function、元组Tuple、类型推论、联合类型)
第三章:Typescript基础知识(类型断言、类型别名、字符串字面量类型、枚举、交叉类型)
第四章:
文章目录
- 系列文章目录
- 一、类型拓宽
- 1.1 什么是类型拓宽
- 1.2 如何控制类型拓宽
- 1.2.1 const 控制类型拓宽
- 1.2.2 提供显式类型注释
- 1.2.3 使用 const 断言
- 二、类型缩小
一、类型拓宽
1.1 什么是类型拓宽
所有通过 let 或 var 定义的变量、函数的形参、对象的非只读属性
,如果满足指定了初始值且未添加类型注解
的条件,那么它们推断出来的类型就是指定的初始值字面量类型拓宽后的类型
,这就是字面量类型拓宽。(第三章文章有提及过一嘴)
下面我们通过字符串字面量的示例来理解一下字面量类型拓宽:
let str = 'this is string'; // 类型是 string
let strFun = (str = 'this is string') => str; // 类型是 (str?: string) => string;
const specifiedStr = 'this is string'; // 类型是 'this is string'
let str2 = specifiedStr; // 类型是 'string'
let strFun2 = (str = specifiedStr) => str; // 类型是 (str?: string) => string;
实际上,除了字面量类型拓宽之外,TypeScript 对某些特定类型值也有类似类型拓宽的设计,比如对 null 和 undefined 的类型进行拓宽,通过 let、var 定义的变量如果满足未显式声明类型注解且被赋予了 null 或 undefined 值,则推断出这些变量的类型是 any:
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
注意:在严格模式下,一些比较老的版本中(2.0)null 和 undefined 并不会被拓宽成“any”。
1.2 如何控制类型拓宽
- 案例一
我们先来看一个示例,如下代码所示:
interface Vector {
x: number;
y: number;
z: number;
}
function getComponent(vector: Vector, axis: "x" | "y" | "z") {
return vector[axis];
}
let x = "x";
let vec = { x: 1, y: 2, z: 3 };
getComponent(vec, x); // Error:类型“string”的参数不能赋给类型“"x" | "y" | "z"”的参数。
上述代码中,为什么会出现错误呢?通过 TypeScript 的错误提示消息,我们知道是因为变量 x 的类型被推断为 string 类型
,而 getComponent 函数它的第二个参数有一个更具体的字面量类型
。这在实际场合中被拓宽了,所以导致了一个错误。
1.2.1 const 控制类型拓宽
如果用 const 而不是 let 声明一个变量,那么它的类型会更窄。事实上,使用 const 可以帮助我们修复案例一例子中的错误:
const x = "x"; // type is "x"
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // OK
因为 x 不能重新赋值,所以 TypeScript 可以推断更窄的类型,就不会在后续赋值中出现错误。
然而,const 对于对象和数组,仍然会存在问题
。
- 案例二
以下这段代码在 JavaScript 中是没有问题的:
const obj = {
x: 1,
};
obj.x = 6;
obj.x = '6';
obj.y = 8;
obj.name = 'semlinker';
但是在TypeScrip的环境中最后三局会出现错误:
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
上述代码中,对于 obj 的类型来说,它可以是 {readonly x:1} 类型,或者是更通用的 {x:number} 类型。当然也可能是 {[key: string]: number} 或 object 类型。TypeScript 的拓宽算法会将对象其内部属性赋值给 let 关键字声明的变量
,进而来推断其属性的类型。因此 obj 的类型为 {x:number} 。你可以将 obj.x 赋值给其他 number 类型的变量,但是它还会阻止你添加其他属性。
1.2.2 提供显式类型注释
如果用给let声明的变量设置显示的类型注释,也可以修复案例一例子中的错误:
let x: "x" = "x"; // type is "x"
let vec = { x: 10, y: 20, z: 30 };
getComponent(vec, x); // OK
基于字面量类型拓宽的条件,我们可以通过添加显示类型注解
控制类型拓宽行为。
const specifiedStr: 'this is string' = 'this is string'; // 类型是 '"this is string"'
let str2 = specifiedStr; // 即便使用 let 定义,类型是 'this is string'
1.2.3 使用 const 断言
不要将const 断言与 let 和 const 混淆,后者在值空间中引入符号。这是一个纯粹的类型级构造。让我们来看看以下变量的不同推断类型:
// 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 断言:
/ Type is number[]
const arr1 = [1, 2, 3];
// Type is readonly [1, 2, 3]
const arr2 = [1, 2, 3] as const;
二、类型缩小
在 TypeScript 中,我们可以通过某些操作将变量的类型由一个较为宽泛的集合缩小到相对较小、较明确的集合,这就是类型缩小。
比如,我们可以使用类型守卫将函数参数的类型从 any 缩小到明确的类型:
let func = (anything: any) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else if (typeof anything === 'number') {
return anything; // 类型是 number
}
return null;
};
同样,我们可以使用类型守卫将联合类型缩小到明确的子类型,示例如下:
{
let func = (anything: string | number) => {
if (typeof anything === 'string') {
return anything; // 类型是 string
} else {
return anything; // 类型是 number
}
};
}
我们也可以通过字面量类型等值判断(===)或其他控制流语句(包括但不限于 if、三目运算符、switch 分支)将联合类型收敛为更具体的类型,如下代码所示:
{
type Goods = 'pen' | 'pencil' |'ruler';
const getPenCost = (item: 'pen') => 2;
const getPencilCost = (item: 'pencil') => 4;
const getRulerCost = (item: 'ruler') => 6;
const getCost = (item: Goods) => {
if (item === 'pen') {
return getPenCost(item); // item => 'pen'
} else if (item === 'pencil') {
return getPencilCost(item); // item => 'pencil'
} else {
return getRulerCost(item); // item => 'ruler'
}
}
}
在上述 getCost 函数中,接受的参数类型是字面量类型的联合类型,函数内包含了 if 语句的 3 个流程分支,其中每个流程分支调用的函数的参数都是具体独立的字面量类型。
那为什么类型由多个字面量组成的变量 item 可以传值给仅接收单一特定字面量类型的函数 getPenCost、getPencilCost、getRulerCost 呢?这是因为在每个流程分支中,编译器知道流程分支中的 item 类型是什么。比如 item === ‘pencil’ 的分支,item 的类型就被收缩为“pencil”。
事实上,如果我们将上面的示例去掉中间的流程分支,编译器也可以推断出收敛后的类型,如下代码所示:
const getCost = (item: Goods) => {
if (item === 'pen') {
item; // item => 'pen'
} else {
item; // => 'pencil' | 'ruler'
}
}
一般来说 TypeScript 非常擅长通过条件来判别类型,但在处理一些特殊值时要特别注意 —— 它可能包含你不想要的东西!例如,以下从联合类型中排除 null 的方法是错误的:
const el = document.getElementById("foo"); // Type is HTMLElement | null
if (typeof el === "object") {
el; // Type is HTMLElement | null
}
因为在 JavaScript 中 typeof null 的结果是 “object” ,所以你实际上并没有通过这种检查排除 null 值。除此之外,false的原始值也会产生类似的问题:
function foo(x?: number | string | null) {
if (!x) {
x; // Type is string | number | null | undefined
}
}
因为空字符串和 0 都属于 false值,所以在分支中 x 的类型可能是 string 或 number 类型。帮助类型检查器缩小类型的另一种常见方法是在它们上放置一个明确的 “标签”:
interface UploadEvent {
type: "upload";
filename: string;
contents: string;
}
interface DownloadEvent {
type: "download";
filename: string;
}
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent) {
switch (e.type) {
case "download":
e; // Type is DownloadEvent
break;
case "upload":
e; // Type is UploadEvent
break;
}
}
这种模式也被称为 ”标签联合“ 或 ”可辨识联合“,它在 TypeScript 中的应用范围非常广。