类型保护
类型保护是指缩小类型的范围,在一定的块级作用域内由编译器推导其类型,提示并规避不合法的操作,提高代码质量。
类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。
我们可以通过typeof
、instanceof
、in
、is
和字面量类型
将代码分割成范围更小的代码块,在这一块中,变量的类型是确定的。
typeof
先来看看JavaScript中typeof的用法:
具体可参考 MDN typeof
typeof 操作符返回一个字符串,表示未经计算的操作数的类型。
类型 | 结果 |
---|---|
Undefined | "undefined" |
Null | "object" |
Boolean | "boolean" |
Number | "number" |
BigInt(ECMAScript 2020 新增) | "bigint" |
String | "string" |
Symbol (ECMAScript 2015 新增) | "symbol" |
宿主对象(由 JS 环境提供) | 取决于具体实现 |
Function 对象 | "function" |
其他任何对象 | "object" |
// Undefined
typeof undefined === 'undefined';
typeof declaredButUndefinedVariable === 'undefined';
typeof undeclaredVariable === 'undefined';
typeof null === 'object';
typeof false === 'boolean';
typeof Boolean(1) === 'boolean'; // Boolean() 会基于参数是真值还是虚值进行转换
// Symbols
typeof Symbol() === 'symbol';
typeof Symbol('foo') === 'symbol';
typeof Symbol.iterator === 'symbol';
typeof 18n === 'bigint';
// 字符串
typeof 'hello' === 'string';
typeof String(1) === 'string';
// 数值
typeof 99 === 'number';
typeof NaN === 'number';
typeof Number(1) === 'number'; // Number 会尝试把参数解析成数值
// 对象
typeof {a: 1} === 'object';
// 使用 Array.isArray 或者 Object.prototype.toString.call
// 区分数组和普通对象
typeof [1, 2, 4] === 'object';
typeof new Date() === 'object';
typeof /regex/ === 'object';
// 函数
typeof function() {} === 'function';
typeof class C {} === 'function'
typeof Math.sin === 'function';
TypeScript中的typeof
主要用途是在类型上下文中获取变量或者属性的类型。
如:
获取变量类型
function fn (x: string | number) { if (typeof x === 'string') { x.toFixed(2); // Property 'toFixed' does not exist on type 'string'. return x.split(''); } // ... }
获取对象的类型
interface IPerson { name: string; age: number; } let person: IPerson = { name: 'xman', age: 18 }; type Person = typeof person; let p: Person = { name: 'zxx', age: 20 }
以上代码中通过typeof获取到person对象的类型,之后我们就可以使用Person类型。
对于嵌套对象也是一样:const userInfo = { name: 'xman', age: 18, address: { provice: '湖北', city: '武汉' } } type UserInfo = typeof userInfo;
此时UserInfo类型如下:
type UserInfo = { name: string; age: number; address: { provice: string; city: string; }; }
获取函数的类型
function add (x: number, y: number): number { return x + y; } type Add = typeof add;
此时Add类型为
type Add = (x: number, y: number) => number
instanceof
先来看看JavaScript中instanceof的用法:
具体可参考 MDN instanceof
instanceof 运算符用于检测构造函数的 prototype 属性是否出现在某个实例对象的原型链上。
简单例子如下:
// 定义构造函数
function C(){}
function D(){}
var o = new C();
o instanceof C; // true,因为 Object.getPrototypeOf(o) === C.prototype
o instanceof D; // false,因为 D.prototype 不在 o 的原型链上
o instanceof Object; // true,因为 Object.prototype.isPrototypeOf(o) 返回 true
C.prototype instanceof Object // true,同上
C.prototype = {};
var o2 = new C();
o2 instanceof C; // true
o instanceof C; // false,C.prototype 指向了一个空对象,这个空对象不在 o 的原型链上。
D.prototype = new C(); // 继承
var o3 = new D();
o3 instanceof D; // true
o3 instanceof C; // true 因为 C.prototype 现在在 o3 的原型链上
TypeScript中instanceof
与typeof
类似,区别在于typeof判断基础类型,instanceof判断是否为某个对象的实例。
其右侧要求是一个构造函数.
class Person {
public name: string;
public age: number;
public constructor(theName: string, age: number) {
this.name = theName;
this.age = age;
}
}
class Animal {
public height: string;
public weight: string;
public constructor(height: string, weight: string) {
this.height = height;
this.weight = weight;
}
}
function typeGuard (arg: Person | Animal) {
if (arg instanceof Person) {
arg.height = '60kg'; // Property 'height' does not exist on type 'Person'.
} else if (arg instanceof Animal) {
arg.name = '猴子'; // Property 'name' does not exist on type 'Animal'.
}
}
in
先来看看JavaScript中in的用法:
具体可参考 MDN in
如果指定的属性在指定的对象或其原型链中,则in 运算符返回true
注意:
in右操作数必须是一个对象值。
简单例子如下:
const car = { make: 'Honda', model: 'Accord', year: 1998 };
console.log('make' in car); // true;
delete car.make;
if ('make' in car === false) {
car.make = 'Suzuki';
}
console.log(car.make); // Suzuki
// 数组
var trees = new Array("redwood", "bay", "cedar", "oak", "maple");
0 in trees; // 返回 true
3 in trees; // 返回 true
6 in trees; // 返回 false
"bay" in trees; // 返回 false (必须使用索引号,而不是数组元素的值)
// 只是将一个属性的值赋值为undefined,而没有删除它,则 in 运算仍然会返回true
trees[3] = undefined;
3 in trees; // 返回 true
"length" in trees; // 返回 true (length 是一个数组属性)
Symbol.iterator in trees; // 返回 true (数组可迭代,只在 ES2015+ 上有效)
// 内置对象
"PI" in Math; // 返回 true
var color1 = new String("green");
"length" in color1; // 返回 true
var color2 = "coral";
"length" in color2; // 报错 (color2 不是对象)
// 如果一个属性是从原型链上继承来的,in 运算符也会返回 true
"toString" in {}; // 返回 true
TypeScript中in
操作符用于确定属性是否存在于某个对象上, 这也是一种缩小范围的类型保护
class Person {
public name: string;
public age: number;
public constructor(theName: string, age: number) {
this.name = theName;
this.age = age;
}
}
class Animal {
public height: string;
public weight: string;
public constructor(height: string, weight: string) {
this.height = height;
this.weight = weight;
}
}
function typeGuard (arg: Person | Animal) {
if ('name' in arg) {
arg.name = 'xman';
} else if ('height' in Animal) {
arg.height = '100kg';
}
}
类型谓词(is 关键字 )
类型谓词(type predicates): 为 parameterName is Type 这种形式。 parameterName必须来自于当前函数签名里的一个参数名。
is
关键字一般用于函数返回值类型中,判断参数是否属于某一类型,并根据结果返回对应的布尔类型。
定义一个类型保护,只要简单地定义一个函数,其返回值是一个 类型谓词
class Fish {
swim () {
console.log('游泳~');
}
eat () {
console.log('进食!');
}
}
class Bird {
fly () {
console.log('飞翔~');
}
eat () {
console.log('进食!');
}
}
function getSmallPet(): Fish | Bird {
return Math.random() > 0.5 ? new Fish() : new Bird()
}
let pet = getSmallPet();
pet.eat();
pet.swim();
// Property 'swim' does not exist on type 'Fish | Bird'.
// Property 'swim' does not exist on type 'Bird'.
pet.fly();
// Property 'fly' does not exist on type 'Fish | Bird'.
// Property 'fly' does not exist on type 'Fish'.
以上代码中, getSmallPet函数中,即可以返回Fish类型对象,又可以返回Bird类型对象,由于返回对象类型不确定,所以使用联合类型对象共有的方法时,一切正常,但是使用联合类型对象各自独有的方法时,ts 会报错。
此时我们可以使用自定义类型保护来解决这个问题。
如下:
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
拓展函数
const isNumber = (val: unknown): val is number => typeof val === 'number';
const isString = (val: unknown): val is string => typeof val === 'string';
const isSymbol = (val: unknown): val is symbol => typeof val === 'symbol';
const isFunction = (val: unknown): val is Function => typeof val === 'function';
const isObject = (val: unknown): val is Record<any, any> => val !== null && typeof val === 'object';
function isPromise<T = any>(val: unknown): val is Promise<T> {
return isObject(val) && isFunction(val.then) && isFunction(val.catch);
}
const objectToString = Object.prototype.toString;
const toTypeString = (value: unknown): string => objectToString.call(value);
const isPlainObject = (val: unknown): val is object => toTypeString(val) === '[object Object]';
字面量类型保护
以下代码定义了两个接口和一个类别类型
interface Circle {
kind: "circle"; // 字符串字面量类型
radius: number;
}
interface Square {
kind: "square"; // 字符串字面量类型
sideLength: number;
}
type Shape = Circle | Square;
现在我们实现一个获取面积的方法:
function getArea(shape: Shape) {
return Math.PI * shape.radius ** 2;
// Property 'radius' does not exist on type 'Shape'.
// Property 'radius' does not exist on type 'Square'.
}
此时提示Square中不存在属性radius, 通过判断字面量类型来进行区分:
function getArea (shape: Shape) {
switch (shape.kind) {
case "circle": // Circle类型
return Math.PI * shape.radius ** 2;
case "square": // Square类型
return shape.sideLength ** 2;
}
}
最后考虑default,可以利用never类型的特性实现全面性检查。
function getArea (shape: Shape) {
switch (shape.kind) {
case "circle": // Circle类型
return Math.PI * shape.radius ** 2;
case "square": // Circle类型
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape;
return _exhaustiveCheck;
}
}
注意:
never类型表示的是那些永不存在的值的类型。
但是如果又新增了联合类型, 但是忘记同时修改switch case分支控制流程, 最后shape就会被收窄为 Triangle 类型, 导致无法赋值给never类型,这时就会产生一个编译错误。
所以在使用 never
类型时一定要避免出现新增了联合类型而没有对应的实现的情况。
interface Triangle {
kind: "triangle";
sideLength: number;
}
type Shape = Circle | Square | Triangle;
function getArea (shape: Shape) {
switch (shape.kind) {
case "circle": // Circle类型
return Math.PI * shape.radius ** 2;
case "square": // Circle类型
return shape.sideLength ** 2;
default:
const _exhaustiveCheck: never = shape; // Type 'Triangle' is not assignable to type 'never'.
return _exhaustiveCheck;
}
}
以上ts代码均在 https://www.typescriptlang.or... 上运行过,版本为4.7.2。
最后, 如有错误,欢迎各位大佬指点!感谢!
参考资料
https://rangle.io/blog/how-to-use-typescript-type-guards/
https://www.typescriptlang.org/docs/handbook/2/narrowing.html#using-type-predicates
https://juejin.cn/post/6974746024464089124#heading-4
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。