类型守卫(类型保护)
TypeScript 类型守卫(或者叫类型保护)(Type Guards)是一种在运行时检查变量类型的机制,它可以帮助 TypeScript 在特定的代码块中缩小变量的类型范围
类型守卫包括如下几种
typeof类型守卫
// 1. typeof 类型守卫
function printValue(value: string | number | boolean): void {
if (typeof value === "string") {
console.log(`字符串: ${value.toUpperCase()}`);
} else if (typeof value === "number") {
console.log(`数字: ${value.toFixed(2)}`);
} else {
console.log(`布尔值: ${value ? "真" : "假"}`);
}
}
instanceof类型守卫
// 2. instanceof 类型守卫
class Animal { constructor(public name: string) {} }
class Dog extends Animal { bark() { console.log("汪汪!"); } }
class Cat extends Animal { meow() { console.log("喵!"); } }
function makeSound(animal: Animal): void {
if (animal instanceof Dog) animal.bark();
else if (animal instanceof Cat) animal.meow();
}
- 使用自定义类型守卫 (使用类型谓词)
// 3. 自定义类型守卫
interface Bird { fly(): void; name: string; }
interface Fish { swim(): void; name: string; }
function isFish(pet: Bird | Fish): pet is Fish {
return (pet as Fish).swim !== undefined;
}
function move(pet: Bird | Fish): void {
if (isFish(pet)) pet.swim();
else pet.fly();
}
in操作符类型守卫
// 4. in 操作符类型守卫
interface Square { kind: 'square'; size: number; }
interface Rectangle { kind: 'rectangle'; width: number; height: number; }
function calculateArea(shape: Square | Rectangle): number {
return 'size' in shape ? shape.size * shape.size : shape.width * shape.height;
}
- 字面量类型守卫
// 5. 字面量类型守卫
type Direction = 'north' | 'south' | 'east' | 'west';
function move2(direction: Direction): void {
switch (direction) {
case 'north': console.log('向北移动'); break;
case 'south': console.log('向南移动'); break;
case 'east': console.log('向东移动'); break;
case 'west': console.log('向西移动'); break;
default:
const exhaustiveCheck: never = direction;
throw new Error(`未处理的方向: ${exhaustiveCheck}`);
}
}
好处
- 增加代码的类型安全
- 减少类型断言的使用
类型谓词
类型谓词(Type Predicates)是 TypeScript 中的一种特殊返回类型注解,它用于自定义类型守卫函数
类型谓词的形式为 parameterName is Type,其中 parameterName 必须是当前函数的参数名
比如下面代码
// 自定义类型守卫处理可能为null/undefined的值
function isNonNullish<T>(value: T | null | undefined): value is T {
return value !== null && value !== undefined;
}
// 应用示例
const values = ['a', 'b', null, 'c', undefined];
const nonNullValues = values.filter(isNonNullish); // 类型为 string[]
类型断言
注意:ts 中的类型转换概念一般可用指类型断言,只有在 js 中才有类型转换概念
TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。
TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。(断言不是万能的,它会先检查断言类型是否能够兼容,不能兼容的话会报错)
比如
interface Duck {
name: string;
age: number;
city: string;
}
interface Bowl {
price: number;
}
const bowl: Bowl = {
price: 100,
};
// Error 类型错误,Bowl 和 Duck 是完全不能兼容的,这种情况不允许变型,同时也不能断言
const duck: Duck = bowl as Duck;
类型兼容
类型兼容性用于确定一个类型是否能赋值给其他类型,ts 中判断类型是否能够兼容,会通过四种方式判断,分别是
- 协变
- 逆变
- 双向协变
- 不变(即不能类型兼容,此时会报错)
协变、逆变、双向协变、不变
子类型和可赋值性
子类型
- 父类型是属性更少的一方,子类型继承父类型所有属性的同时,有新的属性
参考集合论中,父集和子集的关系即可,父类型属性更少,属于子集;子类型属性更多,属于父集。
注意:父类型相当于 子集 而不是父集,因为他的属性更少
可赋值性
范围更大的类型可以赋值给范围更小的类型,也就是子类型可以赋值给父类型,而父类型不能赋值给子类型
实际应用
// 比如使用泛型的场景,extends约束了,T必须包含a
function fn<T extends { a: string }>(args: T) {
return args.a;
}
fn({ a: "123", b: 123 });
fn({ b: 123 }); // 报错
协变
注意:这里的
T是类型构造器,比如这里的普通类型定义
如果 A 是 B 的子类型,那么 T<A> 也是 T<B> 的子类型,这种情况就叫做协变
TypeScript 中数组和对象的属性是协变的
// 假设已经定义了 Dog 和 Animal,这个在很多教程中都是一样的
// 数组的协变
let animal: Animal[] = []
let dog: Dog[] = []
animal = dog // 协变,animal 可以兼容 dog
// 对象的协变
let obj1 = {
prop: Dog // 仅用来表示类型
}
let obj2 = {
prop: Animal
}
obj2 = obj1
逆变
注意:这里的
T是类型构造器,比如这里的函数类型定义
如果 A 是 B 的子类型,那么 T<B> 反而是 T<A> 的子类型,这种情况就叫做逆变
TypeScript 中函数参数是逆变的
// 逆变
// T<Animal>
var getAnimal = function (animal: Animal): void {
console.log(animal.name);
};
// T<Dog>
var getDog = function (dog: Dog): void {
console.log(dog.name);
};
getAnimal = getDog; // X
getDog = getAnimal; // 这里类型逆变了
双向协变
在 TypeScript 中,由于灵活性等权衡,对于函数参数默认的处理是 双向协变 的。也就是既可以 visitAnimal = visitDog,也可以 visitDog = visitAnimal。在开启了 tsconfig 中的 strictFunctionType 后才会严格按照 逆变 来约束赋值关系。
不变
不变就是不允许变型。如果两个类型完全不相同,它们是不能兼容的。
interface Duck {
name: string;
age: number;
city: string;
}
interface Bowl {
price: number;
}
const bowl: Bowl = {
price: 100,
};
// Error 类型错误,Bowl 和 Duck 是完全不能兼容的,这种情况不允许变型
const duck: Duck = bowl;
参考资料