TS是JS的超集,在JS的基础上添加了一套类型系统,这样的TS可以被静态分析带来的好处显而易见。
let val: string = 'val';
声明一个string
类型的变量val
。
let val: string = 'val';
val = 1; // Type 'number' is not assignable to type 'string'.
因为number类型和string类型并不兼容,在string类型值出现的地方并不能使用number类型指完成替换,所以在TS世界中给string类型的val变量赋值number类型的值会报错。
但是在JS中并没有赋值的限制:
// javascript
var val = 'val';
val = 1; // 这里不会报错
在JS中变量val首先被赋值了字符串,然后被赋值了数字,这两个数据类型并不一致,但是因为JS没有静态类型检查,所以这并不会报错。
TS的联合类型可以适应这种情况,表示这个变量可能是类型a也可能是类型b也可能是类型c等类型。
let val: string | number = 'val'
val = 1 // 这里并不会报错
在TS中,这里声明的变量val则表示可能是类型string也可能是类型number,所以对变量赋值string类型值和number类型值,并不会报错。
但问题也随之而来。
function test(val: string | number) {
val.toLowerCase() // Property 'toLowerCase' does not exist on type 'string | number'.
}
TS会提示类型 string | number 上没有属性toLowserCase。
这个报错也很容易理解,因为类型string的数据上可以调用方法toLowerCase,但是number不可以。因为变量val的类型,string和number都有可能,TS并不能确定val在运行时是string类型,所以会出现这个错误。
我们需要一个方法来告诉TS这个变量现在是string类型。
对于上面例子中的变量val.toLowerCase的报错来说是因为val的类型范围有点大(string | number),如果我们通过某种方式缩小该范围为string,那么在val上访问属性toLowserCase应当没有问题。
function test(val: string | number) {
if (typeof val === 'string') {
val.toLowerCase() // 这里并不会报错
console.log('val是字符串')
} else {
console.log('val是数字')
}
}
在TS中,if
中的typeof val === 'string'
这种形式的代码会被识别为类型保护(type guard)。TS会分析代码的执行流程,缩小变量可能的类型。在分支if (typeof val === ‘string’) 中变量val的类型被TS识别为’string’,所以在这个分支下val调用string类型数据的方法并不会报错。
因为val是string和number这两个类型的联合,TS不仅知道if子句中的val是string类型,还知道else中的val是number类型。
可以被typeof进行类型保护的类型有:
上面介绍了对于基础类型的识别,在TS中使用频率更多的还有对象,使用typeof来区别不同对象显然是有问题的,因为typeof出来的结果都是’object’无法分辨两个不同的对象。
type A = {a: string}
type B = {b: string}
function test(val: A | B) {
val.a // Property 'a' does not exist on type 'A | B'
}
在test函数中无论是访问val.a还是val.b都会报错,而原因已经明白,TS无法确定变量val的具体类型,TS并不知道当前是类型A还是类型B,所以我们需要帮他一把。
type A = {a: string}
type B = {b: string}
function test(val: A | B) {
if ('a' in val) {
console.log(val.a)
return
}
console.log(val.b)
}
其中的in操作同样会被TS识别为类型保护,如果属性a存在于变量val中那么就能识别出val变量是类型A,则可以正常访问类型A中存在的属性a。
对于对象类型的区分除了使用操作符in还可以使用instanceof来完成。
function test(x: Date | string) {
if (x instanceof Date) {
console.log(x.toUTCString());
} else {
console.log(x.toUpperCase());
}
}
使用严格等于(===)也可以在某些特别情况下正确缩小类型范围
function example(x: string | number, y: string | boolean) {
if (x === y) {
// We can now call any 'string' method on 'x' or 'y'.
x.toUpperCase();
y.toLowerCase();
} else {
console.log(x); // x 是 string | number
console.log(y); // y 是 string | boolean
}
}
在这个例子中x
是string
和number
的联合,而y是string
和boolean
的联合,当x === y
的时候只可能x
和y
都是string类型。所以在分支if (x === y) 中x和y的类型被正确识别为string类型。
我们知道在JS中宽松等于(== null)可以匹配null和undefined两种类型,当然TS也知道,所以 ==null,可以被用来识别类型。
interface Container {
value: number | null | undefined;
}
function multiplyValue(container: Container, factor: number) {
// Remove both 'null' and 'undefined' from the type.
if (container.value != null) {
console.log(container.value);
// Now we can safely multiply 'container.value'.
container.value *= factor;
}
}
即使container.value
可能是null
或者undefined
类型,但是在分支container.value != null
中,该变量类型就只可能是number
类型,所以其参与算术运算并不会报错。
interface Circle {
kind: "circle";
radius: number;
}
interface Square {
kind: "square";
sideLength: number;
}
type Shape = Circle | Square;
function getArea(shape: Shape) {
if (shape.kind === "circle") {
return Math.PI * shape.radius ** 2;
}
}
TS会通过参与联合的类型都有的属性kind来识别当前shape是Circle或者Square达到类型保护的目的。
上面介绍了TS对于联合类型中基础类型和对象类型的类型保护。但是这并不能覆盖全部的场景,例如上面介绍到的内容,并不能区分两个函数:
type fn1 = (arg: number) => boolean
type fn2 = (arg: string) => boolean
function test(fn: fn1 | fn2) {
return fn(1)
}
在这个例子里,我们并没有方法来识别fn是类型fn1还是类型fn2。
在前面的例子里,例如 if (typeof a === ‘string’) 这里面的变量a会被TS类型系统识别为string,如果TS将识别一个变量为某个类型的能力开放给开发者,上面的问题就会迎刃而解。这个能力就是类型谓词。
通过观察上面类型保护的规律就会发现TS总会询问:变量a是类型A吗?被识别为类型保护的操作的回答总是true或者false都是boolean值。typoef a === ‘string’或者’a’ in A或者a instanceof A,这些操作的返回值都是boolean,并且都是和特定类型作比较。
// 我就是类型谓词形成的类型保护
function isTypefn1(fn: fn1 | fn2): fn is fn1 {
if (fn.name === 'fn1') return true
return false
}
function test(fn: fn1 | fn2) {
if (isTypefn1(fn)) return fn(1)
else return fn('1')
}
其中 isTypefn1
调用的返回值就会告诉TS入参是不是类型fn1,这样TS就可以识别变量fn的类型完成类型保护。
Narrowing