一、类型推断
在之前我们的学习过程中,在定义基础类型的变量都需要写类型注解,虽然这样可以清晰地标明变量的数据类型,但书写起来实在是麻烦。而TS也贴心的 帮我们考虑到了这个问题,在大多情况下,TS会根据上下文环境及赋值表达式自动推演出变量的类型,无需我们写明类型注解,这种能力我们称为类型推断:
let str = 'this is string';
let num = 1;
let bool = true;
// 等价于
let str: string = 'this is string';
let num: number = 1;
let bool: boolean = true;
除了有初始赋值的变量之外,有默认值的函数参数以及函数的返回类型,都可以类型推断:
/** 根据参数的类型,推断出返回值的类型也是 number */
function add1(a: number, b: number) {
return a + b;
}
const x1= add1(1, 1); // 推断出 x1 的类型也是 number
/** 推断参数 b 的类型是number,返回值的类型也是数字 */
function add2(a: number, b = 1) {
return a + b;
}
如果变量在声明的时候,没有同时赋值,那么无论之后赋什么类型的值,都会被推断为 any 类型,可接收任何类型的值:
// 声明时未赋值 类型为 any
let myFavoriteNumber;
myFavoriteNumber = 'seven';
myFavoriteNumber = 7;
二、类型断言
1、概念
类型断言是指在某些情况下,我们非常明确某个值的变量类型,但从TS的类型推断来说,可能会有多种数据类型。此时我们就可以通过类型断言来,强行指定值的变量类型,这样就可以将这个值赋给对应类型的变量了,例如:
// 初始化了一个数组
const arrayNumber: number[] = [1, 2, 3, 4];
// 我们明确知道 有大于3的数字 所以返回值一定是number
// 但是在TS看来,返回值可能是number也可能是 undefined ,所以直接赋值给number变量 会报错
const greaterThan2: number = arrayNumber.find(num => num > 2); // 提示 ts(2322)
// 增加类型断言 来强行指定返回值的数据类型
const greaterThan2: number = arrayNumber.find(num => num > 2) as number;
类型断言的语法有两种:<数据类型>数据
或 数据 as 数据类型
,两者作用相同,但尖括号个在React的JSX中会产生语法冲突,推荐使用 as 语法:
let someValue: any = "this is a string";
// 尖括号 语法
let strLength: number = (<string>someValue).length;
// as 语法
let strLength: number = (someValue as string).length;
2、非空断言
我们可以通过后缀表达式操作符 !
来断言某个值是 非 null 也 非 undefined 的类型,简单来说就是将某个值的值域排除null和undefined:
// 定义变量 使其类型为联合类型
let may: null | undefined | string;
// 使用非空断言,排除 null 和undefined 所以只能是 string 所以可使用toString()
may!.toString(); // ok
// 未使用非空断言 值可能为多种类型 不可直接使用 toString()
may.toString(); // 报错 ts(2531)
3、确定赋值断言
在TS中,如果一个变量在声明但未赋值的情况下被使用,则会编译报错。但如果我们十分明确这个变量会被赋值,那我们就可以使用确定赋值断言,即在声明变量时,在变量后面加一个!
,TS编译器就不会再报错:
// 声明变量 但不赋值 使用 确定赋值断言
let x!: number;
// 在这个方法里赋值
initialize();
// 但编译器不知道会赋值 如果不加 确定赋值断言 ,则会报错
console.log(2 * x); // Ok
function initialize() {
x = 10;
}
三、字面量类型
1、概念
在TS中,字面量不仅可以表示值,还可以表示类型,即字面量类型。也就是在声明变量的时候给变量赋值,将值作为一个类型来设置变量的类型。目前支持的字面量类型支持三种:字符串类型、数字类型、布尔类型:
// 字符串字面量类型
let specifiedStr: 'this is string' = 'this is string';
// 数字字面量类型
let specifiedNum: 1 = 1;
// 布尔字面量类型
let specifiedBoolean: true = true;
在上面的案例中, 'this is string'
表示一个字符串字面量类型,相当于 string
类型的子类型,所以除了'this is string'
,其他string
类型的值也不能赋值给那个变量:
// 字符串字面量类型
let specifiedStr: 'this is string' = 'this is string';
// string 类型
let str: string = 'any string';
// 类型 'string' 不能赋值给类型 'this is string'
specifiedStr = str; // ts(2322)
// 类型 'this is string' 能赋值给类型 'string'
str = specifiedStr; // ok
数字字面量类型和布尔字面量类型类似。
2、字符串字面量类型
在实际应用中,定义单个字符串字面量类型并没有太大用处,我们通常都是把多个字面量类型组成一个联合类型,来明确的限制变量的值域。与函数结合使用,限制参数为指定的字面量类型集合,在编译时,会去检查参数是否为指定联合类型中的成员:
// 联合字面量类型
type Direction = 'up' | 'down';
// 限制函数的参数类型
function move(dir: Direction) {
// ...
}
// 只能是 up 或 ok
move('up'); // ok
// 如果为其他string 则会编译报错
move('right'); // ts(2345)
3、数字字面量类型和布尔字面量类型
用法与字符串字面量类型相似,都是为了更明确的限制变量的值域,让使用者必须用特定值的数据:
interface Config {
size: 'small' | 'big';
isEnable: true | false;
margin: 0 | 2 | 4;
}
四、类型拓宽(Type Widening)
1、概念
所有let和var定义的变量、函数的形参、对象的非只读属性,若满足类型推断的条件,那么他们推断出来的类型,就是初始值字面量类型拓宽后的父类型,而const因为是常量,不可变更,所以类型不会拓宽:
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'
2、限制类型拓宽
我们可以给变量增加对应的字面量类型的类型注解,来限制类型拓宽:
// 增加类型注解 限制类型拓宽 类型是 'this is string'
const specifiedStr: 'this is string' = 'this is string';
// 即便使用 let 定义,类型是 'this is string'
let str2 = specifiedStr;
3、特殊类型拓宽
在TS中,通过 let、var 定义的变量如果未标明类型注解,并且赋值为null 或 undefined ,那此变量的类型将拓宽为 any:
let x = null; // 类型拓宽成 any
let y = undefined; // 类型拓宽成 any
const z = null; // 常量不会类型拓宽 类型是 null
let z2 = z; // 类型是 null
let x2 = x; // 类型是 null
let y2 = y; // 类型是 undefined
注意:在严格模式下,一些比较老的版本中(2.0)null 和 undefined 并不会被拓宽成“any”。
4、对象属性的类型拓宽
TS的拓宽算法会将对象内部属性视为将其赋值给 let 关键字声明的变量,进而来推断其属性的类型。而且还会阻止我们向对象添加声明时不存在的属性:
// 声明对象
const obj = {
x: 1, // 类型拓宽为 number 类型
};
// 赋number类型的值
obj.x = 6;
// 赋 string 类型 会报错
obj.x = '6'; // Type '"6"' is not assignable to type 'number'.obj.x = '6';
// 添加新属性
obj.y = 8; // Property 'y' does not exist on type '{ x: number; }'.obj.y = 8
五、类型缩小(Type Narrowing)
1、概念
在TS,通过某些手段将变量的类型由一个较为宽泛的集合缩小到一个明确的集合的过程,就是类型缩小。
2、类型守卫
我们可以使用结合 typeof
的类型守卫,将函数参数的进行类型缩小:
let func = (anything: any) => {
// 如果传过来的参数是 string 类型 返回时就会缩小为string类型
if (typeof anything === 'string') {
return anything; // 类型是 string
// 如果传过来的参数是 number 类型 返回时就会缩小为number类型
} else if (typeof anything === 'number') {
return anything; // 类型是 number
}
return null;
};
使用类型守卫时,需要注意某些特殊值,比如:typeof null 的结果是 object。
3、流程控制+等值判断
我们还可以通过流程控制语句(包括但不限于 if、三目运算符、switch 分支等)+ 等值判断(===)来将联合类型进行类型缩小:
// 定义一个联合类型
type Goods = 'pen' | 'pencil' |'ruler';
// 参数为联合类型
const getCost = (item: Goods) => {
// 利用等值判断进行类型缩小
if (item === 'pen') {
return item; // item 类型缩小为 'pen'
} else if (item === 'pencil') {
return item; // item 类型缩小为 'pencil'
} else {
return item; // item 类型缩小为 'ruler'
}
}
4、标签联合
我们还可以给类型定义一个type属性,通过type属性来缩小类型:
// 接口类型(下一篇博客会讲)
interface UploadEvent {
// 定义 type
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;
}
}
六、交叉类型
交叉类型是指将使用 &
将多个类型合并为一个类型,该类型包含所合并类型的所有特性。很明显,如果使用原始类型、字面量类型等合并成交叉类型,是没有任何意义的,因为不存在数据能够同时属于多个原始类型。交叉类型真正的用处在于将多个接口类型(下一篇博客会讲)合并成一个类型,从而实现类似于接口继承的效果:
// 合并两个对象类型 使得 IntersectionType 同时拥有了 id、name、age 属性
type IntersectionType = { id: number; name: string; } & { age: number };
// 类型注释后 必须符合有 id、name、age 属性
const mixed: IntersectionType = {
id: 1,
name: 'name',
age: 18
}
交叉类型相当于求类型的并集,但如果两个接口具有同名属性会怎么办?这里分为多种情况:两个同名属性类型相同、两个同名属性类型不同且不兼容、两个同名属性类型不同但一个是另一个子类型。
若是第一种情况,两个同名属性类型相同,那没啥太大影响,会将两个同名属性合成一个:
// 定义一个交叉类型
type IntersectionTypeConfict = { id: number; name: string; } & { age: number; name: string; };
// 交叉之后 相当于 { id: number; name: string; age: number}
在两个同名属性类型不同且不兼容的情况下,例如一个是string一个是number,那么定义出来的交叉类型就是一个无用类型,因为不可能有数据既属于string又属于number,这个交叉类型的同名属性也就变成了 never 类型:
// 定义一个交叉类型 因为有同名属性且类型不同不兼容 所以同名属性会变成never类型
type IntersectionTypeConfict = { id: number; name: string; } & { age: number; name: number; };
const mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ts(2322) 错误,'number' 类型不能赋给 'never' 类型
age: 2
};
如果同名属性的类型兼容,比如一个是 number,另一个是 数字字面量类型或number的子类型,那么合并出来的交叉类型的该属性会变成范围小的子类型:
// 定义交叉类型 因为有同名属性且类型不同但是兼容 所以属性会变成 子类型
type IntersectionTypeConfict = { id: number; name: 2; } & { age: number; name: number; };
let mixedConflict: IntersectionTypeConfict = {
id: 1,
name: 2, // ok
age: 2
};
mixedConflict = {
id: 1,
name: 22, // '22' 类型不能赋给 '2' 类型
age: 2
};
还有一种特殊情况就是:同名的那个属性是复杂数据类型,那么交叉的结果,就是将对象成员进行合并,合并规则与类型合并相同:
interface A {
x:{d:true},
}
interface B {
x:{e:string},
}
interface C {
x:{f:number},
}
// 合并之后 类型变为 {x: {d: true,e: string,f: number}}
type ABC = A & B & C
let abc:ABC = {
x:{
d:true,
e:'',
f:666
}
}