TypeScript 类型操作实战:类型守卫、谓词、断言与兼容全解析

260 阅读4分钟

类型守卫(类型保护)

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;

参考资料