使用TypeScript类型守卫处理联合类型
理解联合类型
在 TypeScript 中,联合类型是一种非常有用的类型定义方式,它允许一个变量具有多种类型。例如,我们可能有一个函数,它既可以接受字符串,也可以接受数字:
function printValue(value: string | number) {
console.log(value);
}
printValue('Hello');
printValue(42);
在上述代码中,printValue
函数的参数 value
是一个联合类型 string | number
,这意味着它可以接受字符串类型的值,也可以接受数字类型的值。
然而,当我们需要对联合类型的值进行特定类型的操作时,就会遇到一些挑战。比如,我们想要对 value
进行字符串拼接或者数字加法操作:
function printValue(value: string | number) {
if (typeof value ==='string') {
console.log(value + ' World');
} else {
console.log(value + 1);
}
}
printValue('Hello');
printValue(42);
这里我们使用了 typeof
操作符来判断 value
的实际类型,然后根据不同类型进行相应的操作。这其实就是类型守卫的一种简单应用。
类型守卫的概念
类型守卫是一种运行时检查机制,它可以在代码执行过程中缩小类型的范围。类型守卫的返回值是一个类型谓词,形式为 parameterName is Type
,其中 parameterName
是正在被检查的参数名,Type
是要判断的类型。
TypeScript 内置了一些类型守卫,比如 typeof
和 instanceof
。我们来看 instanceof
的例子,假设我们有一个类继承体系:
class Animal {
name: string;
constructor(name: string) {
this.name = name;
}
}
class Dog extends Animal {
bark() {
console.log('Woof!');
}
}
class Cat extends Animal {
meow() {
console.log('Meow!');
}
}
function handleAnimal(animal: Animal) {
if (animal instanceof Dog) {
animal.bark();
} else if (animal instanceof Cat) {
animal.meow();
}
}
const myDog = new Dog('Buddy');
const myCat = new Cat('Whiskers');
handleAnimal(myDog);
handleAnimal(myCat);
在 handleAnimal
函数中,我们使用 instanceof
类型守卫来判断 animal
实际是 Dog
还是 Cat
类型,然后调用相应的方法。
用户自定义类型守卫
除了内置的类型守卫,我们还可以定义自己的类型守卫。自定义类型守卫函数通常会接受一个联合类型的参数,并返回一个类型谓词。
假设我们有一个联合类型 string | number
,我们想要定义一个类型守卫来判断一个值是否为字符串:
function isString(value: string | number): value is string {
return typeof value ==='string';
}
function printValue(value: string | number) {
if (isString(value)) {
console.log(value + ' World');
} else {
console.log(value + 1);
}
}
printValue('Hello');
printValue(42);
在上述代码中,isString
函数就是一个自定义类型守卫。它接受 string | number
类型的 value
,返回 value is string
,表示如果返回 true
,则 value
是 string
类型。
在函数重载中使用类型守卫
函数重载在 TypeScript 中允许我们定义多个同名但参数列表不同的函数。结合类型守卫,我们可以更灵活地处理不同类型的输入。
考虑一个 add
函数,它既可以接受两个数字相加,也可以接受两个字符串拼接:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: string | number, b: string | number): string | number {
if (typeof a === 'number' && typeof b === 'number') {
return a + b;
} else if (typeof a ==='string' && typeof b ==='string') {
return a + b;
}
throw new Error('Invalid types for add');
}
const result1 = add(1, 2);
const result2 = add('Hello ', 'World');
在这个例子中,我们首先定义了两个函数重载签名,分别处理数字相加和字符串拼接。然后在实现函数中,使用 typeof
类型守卫来判断输入的类型,并执行相应的操作。
类型守卫与类型别名
类型别名是给类型定义一个新名字的方式。当联合类型使用类型别名定义时,类型守卫同样适用。
type StringOrNumber = string | number;
function isString(value: StringOrNumber): value is string {
return typeof value ==='string';
}
function printValue(value: StringOrNumber) {
if (isString(value)) {
console.log(value + ' World');
} else {
console.log(value + 1);
}
}
printValue('Hello');
printValue(42);
这里我们使用 type
关键字定义了 StringOrNumber
类型别名,它等价于 string | number
。然后我们可以像处理普通联合类型一样,使用自定义类型守卫 isString
来处理这个类型别名。
类型守卫与交叉类型
交叉类型是将多个类型合并为一个类型,它要求一个值必须同时满足多个类型的要求。虽然交叉类型和联合类型不同,但类型守卫在处理涉及交叉类型的联合类型时也很有用。
interface HasLength {
length: number;
}
interface IsNumber {
value: number;
}
type LengthOrNumber = HasLength | IsNumber;
function handleValue(value: LengthOrNumber) {
if ('length' in value) {
console.log('Length is', value.length);
} else if ('value' in value) {
console.log('Value is', value.value);
}
}
const obj1: HasLength = { length: 5 };
const obj2: IsNumber = { value: 10 };
handleValue(obj1);
handleValue(obj2);
在上述代码中,我们定义了 HasLength
和 IsNumber
两个接口,并通过 type
创建了联合类型 LengthOrNumber
。在 handleValue
函数中,我们使用 in
操作符作为类型守卫,判断 value
实际属于哪个类型,从而执行相应的操作。
类型守卫在数组联合类型中的应用
当数组元素是联合类型时,我们同样可以使用类型守卫来处理。
function printArrayValues(arr: (string | number)[]) {
arr.forEach((value) => {
if (typeof value ==='string') {
console.log(value + ' World');
} else {
console.log(value + 1);
}
});
}
const myArray: (string | number)[] = ['Hello', 42];
printArrayValues(myArray);
在这个例子中,myArray
是一个元素为 string | number
联合类型的数组。在 printArrayValues
函数中,我们通过 forEach
遍历数组元素,并使用 typeof
类型守卫对每个元素进行类型判断和相应操作。
高级类型守卫技巧
- 联合类型的反向类型守卫:有时候我们需要判断一个值不是某个类型,例如判断一个值不是字符串:
function isNotString(value: string | number): value is number {
return typeof value!=='string';
}
function printValue(value: string | number) {
if (isNotString(value)) {
console.log(value + 1);
} else {
console.log(value + ' World');
}
}
printValue('Hello');
printValue(42);
这里 isNotString
就是一个反向类型守卫,它判断 value
不是字符串类型,即 value
是数字类型。
- 结合多个类型守卫:在复杂的场景中,我们可能需要结合多个类型守卫来更精确地判断类型。
class Shape {}
class Circle extends Shape {
radius: number;
constructor(radius: number) {
super();
this.radius = radius;
}
}
class Square extends Shape {
side: number;
constructor(side: number) {
super();
this.side = side;
}
}
function draw(shape: Shape | Circle | Square) {
if (shape instanceof Circle) {
console.log('Drawing a circle with radius', shape.radius);
} else if (shape instanceof Square) {
console.log('Drawing a square with side', shape.side);
} else if ('radius' in shape) {
console.log('This should not happen, but it is like a circle with radius', (shape as Circle).radius);
} else if ('side' in shape) {
console.log('This should not happen, but it is like a square with side', (shape as Square).side);
}
}
const circle = new Circle(5);
const square = new Square(4);
const shape: Shape = new Shape();
draw(circle);
draw(square);
draw(shape);
在 draw
函数中,我们首先使用 instanceof
类型守卫判断 shape
是 Circle
还是 Square
。然后又结合 in
操作符作为额外的类型守卫,处理一些特殊情况(虽然这里的特殊情况逻辑可能在实际中并不常见,但展示了结合多个类型守卫的方法)。
类型守卫的性能考虑
虽然类型守卫在处理联合类型时非常有用,但在性能敏感的应用中,我们需要注意它们的使用。例如,过多的 typeof
或者 instanceof
判断可能会带来一定的性能开销,尤其是在循环中频繁使用时。
function processValues(arr: (string | number)[]) {
for (let i = 0; i < arr.length; i++) {
if (typeof arr[i] ==='string') {
// 执行字符串相关操作
} else {
// 执行数字相关操作
}
}
}
const largeArray: (string | number)[] = [];
for (let i = 0; i < 1000000; i++) {
if (i % 2 === 0) {
largeArray.push(i);
} else {
largeArray.push('' + i);
}
}
processValues(largeArray);
在上述代码中,processValues
函数对一个包含大量元素的联合类型数组进行遍历,并使用 typeof
类型守卫。如果这种操作非常频繁,可能会影响性能。在这种情况下,我们可以考虑优化算法,减少类型判断的次数,或者在设计阶段尽量避免频繁处理这种复杂的联合类型。
类型守卫与类型推断
类型守卫不仅可以在运行时缩小类型范围,还能帮助 TypeScript 进行更好的类型推断。
function getValue(): string | number {
return Math.random() > 0.5? 'Hello' : 42;
}
const value = getValue();
if (typeof value ==='string') {
// 这里 TypeScript 知道 value 是 string 类型
console.log(value.length);
} else {
// 这里 TypeScript 知道 value 是 number 类型
console.log(value.toFixed(2));
}
在上述代码中,getValue
函数返回一个 string | number
联合类型的值。通过 typeof
类型守卫,TypeScript 能够在不同分支中准确推断出 value
的类型,从而允许我们使用相应类型的属性和方法。
类型守卫在接口和类继承体系中的应用
在接口和类继承体系中,类型守卫可以帮助我们处理不同类型的对象。
interface Shape {
draw(): void;
}
class Circle implements Shape {
radius: number;
constructor(radius: number) {
this.radius = radius;
}
draw() {
console.log('Drawing a circle with radius', this.radius);
}
}
class Square implements Shape {
side: number;
constructor(side: number) {
this.side = side;
}
draw() {
console.log('Drawing a square with side', this.side);
}
}
function drawShapes(shapes: Shape[]) {
shapes.forEach((shape) => {
if (shape instanceof Circle) {
// 这里 shape 被推断为 Circle 类型
shape.draw();
} else if (shape instanceof Square) {
// 这里 shape 被推断为 Square 类型
shape.draw();
}
});
}
const circle = new Circle(5);
const square = new Square(4);
const shapeArray: Shape[] = [circle, square];
drawShapes(shapeArray);
在这个例子中,我们有一个 Shape
接口以及实现它的 Circle
和 Square
类。drawShapes
函数接受一个 Shape
数组,通过 instanceof
类型守卫,在不同分支中 shape
被准确推断为 Circle
或 Square
类型,从而可以调用相应的 draw
方法。
类型守卫与泛型
泛型在 TypeScript 中提供了一种创建可复用组件的方式,类型守卫在泛型代码中同样有重要作用。
function identity<T>(arg: T): T {
return arg;
}
function printIdentity<T>(arg: T) {
if (Array.isArray(arg)) {
console.log('It is an array with length', arg.length);
} else {
console.log('It is not an array');
}
return identity(arg);
}
const result1 = printIdentity([1, 2, 3]);
const result2 = printIdentity('Hello');
在上述代码中,printIdentity
函数使用了泛型 T
。通过 Array.isArray
类型守卫,我们可以判断传入的泛型参数是否为数组类型,并进行相应的操作。这展示了类型守卫在泛型函数中的应用,使得泛型代码可以根据实际类型进行不同的处理。
类型守卫在函数参数默认值中的应用
当函数参数有默认值且为联合类型时,类型守卫可以帮助我们正确处理默认值的类型。
function greet(name: string | undefined = 'Guest') {
if (typeof name ==='string') {
console.log('Hello,', name);
} else {
console.log('Hello, Guest');
}
}
greet();
greet('John');
在 greet
函数中,参数 name
是 string | undefined
联合类型且有默认值 'Guest'
。通过 typeof
类型守卫,我们可以确保在使用 name
时,它是 string
类型,从而避免潜在的运行时错误。
类型守卫与可选链操作符
可选链操作符 ?.
是 TypeScript 中的一个强大特性,它可以在对象属性可能为 null
或 undefined
时安全地访问属性。结合类型守卫,我们可以更灵活地处理复杂的对象结构。
interface User {
profile: {
address: {
city: string;
};
};
}
function printCity(user: User | null | undefined) {
if (user && 'profile' in user) {
const city = user.profile?.address?.city;
if (city) {
console.log('City is', city);
}
}
}
const user1: User = {
profile: {
address: {
city: 'New York'
}
}
};
const user2: null = null;
printCity(user1);
printCity(user2);
在 printCity
函数中,我们首先使用 in
类型守卫判断 user
是否存在且有 profile
属性。然后使用可选链操作符安全地获取 city
属性,避免了潜在的 null
或 undefined
引用错误。
类型守卫在模块导入导出中的应用
在模块中,当导入或导出联合类型的值时,类型守卫同样可以发挥作用。
// utils.ts
export type StringOrNumber = string | number;
export function isString(value: StringOrNumber): value is string {
return typeof value ==='string';
}
// main.ts
import { StringOrNumber, isString } from './utils';
function printValue(value: StringOrNumber) {
if (isString(value)) {
console.log(value + ' World');
} else {
console.log(value + 1);
}
}
const myValue: StringOrNumber = 42;
printValue(myValue);
在这个例子中,我们在 utils.ts
模块中定义了 StringOrNumber
联合类型和 isString
类型守卫,并在 main.ts
模块中导入使用。这展示了类型守卫在模块间传递和处理联合类型的方式。
通过以上对 TypeScript 中使用类型守卫处理联合类型的详细介绍,我们深入了解了类型守卫的概念、应用场景以及与其他 TypeScript 特性的结合使用。在实际开发中,合理运用类型守卫可以提高代码的健壮性和可维护性,减少运行时错误。无论是简单的联合类型,还是涉及接口、类、泛型等复杂结构的联合类型,类型守卫都为我们提供了有效的类型处理手段。