TypeScript自定义类型保护函数设计模式
什么是类型保护函数
在TypeScript中,类型保护函数是一种特殊的函数,它允许我们在运行时检查某个值的类型,并基于这个检查结果在后续代码中缩小该值的类型范围。这对于处理联合类型(union types
)时非常有用。例如,我们有一个联合类型string | number
,如果没有类型保护,很难在代码中安全地对这个联合类型的值执行特定类型的操作。类型保护函数就是用来解决这个问题的。
类型保护函数的基本语法
类型保护函数的返回值必须是一个类型谓词。类型谓词的语法是parameterName is Type
,其中parameterName
是函数参数的名称,Type
是要检查的类型。下面是一个简单的示例:
function isString(value: string | number): value is string {
return typeof value ==='string';
}
let myValue: string | number = 'hello';
if (isString(myValue)) {
console.log(myValue.length); // 这里myValue的类型被缩小为string,可以安全访问length属性
}
在上述代码中,isString
函数接受一个string | number
类型的参数value
,并返回一个类型谓词value is string
。当isString
函数返回true
时,TypeScript编译器就知道在if
块内,myValue
的类型是string
,从而允许我们安全地访问string
类型的属性和方法,如length
。
设计类型保护函数的模式
基于类型检查的模式
这是最常见的模式,通过typeof
、instanceof
等操作符来检查值的类型。
- 使用
typeof
进行类型检查- 除了前面的
isString
示例,我们还可以创建检查number
类型的函数:
- 除了前面的
function isNumber(value: string | number): value is number {
return typeof value === 'number';
}
let anotherValue: string | number = 42;
if (isNumber(anotherValue)) {
console.log(anotherValue.toFixed(2)); // 在if块内,anotherValue的类型被缩小为number
}
- 对于更复杂的联合类型,比如
string | number | boolean
,我们可以分别创建类型保护函数:
function isBoolean(value: string | number | boolean): value is boolean {
return typeof value === 'boolean';
}
let complexValue: string | number | boolean = true;
if (isBoolean(complexValue)) {
console.log(complexValue? 'Yes' : 'No');
}
- 使用
instanceof
进行类型检查- 当处理类的实例时,
instanceof
操作符非常有用。假设我们有两个类Dog
和Cat
,并且有一个联合类型Dog | Cat
:
- 当处理类的实例时,
class Dog {
bark() {
console.log('Woof!');
}
}
class Cat {
meow() {
console.log('Meow!');
}
}
function isDog(pet: Dog | Cat): pet is Dog {
return pet instanceof Dog;
}
let myPet: Dog | Cat = new Dog();
if (isDog(myPet)) {
myPet.bark(); // 在if块内,myPet的类型被缩小为Dog
}
基于属性检查的模式
有时候,对象可能具有特定的属性,我们可以通过检查这些属性来确定对象的类型。
- 简单属性检查
- 假设我们有两种类型的对象,一种是具有
name
属性的Person
类型,另一种是具有model
属性的Car
类型,它们组成联合类型Person | Car
:
- 假设我们有两种类型的对象,一种是具有
interface Person {
name: string;
age: number;
}
interface Car {
model: string;
year: number;
}
function isPerson(obj: Person | Car): obj is Person {
return 'name' in obj;
}
let myObj: Person | Car = { name: 'John', age: 30 };
if (isPerson(myObj)) {
console.log(`Hello, ${myObj.name}`);
}
- 在上述代码中,
isPerson
函数通过检查obj
对象是否具有name
属性来确定它是否是Person
类型。
- 复杂属性检查
- 对于更复杂的情况,我们可能需要检查多个属性或属性的类型。比如,假设我们有
Rectangle
和Circle
两种形状,它们组成联合类型Rectangle | Circle
:
- 对于更复杂的情况,我们可能需要检查多个属性或属性的类型。比如,假设我们有
interface Rectangle {
type: 'rectangle';
width: number;
height: number;
}
interface Circle {
type: 'circle';
radius: number;
}
function isRectangle(shape: Rectangle | Circle): shape is Rectangle {
return shape.type ==='rectangle' && typeof shape.width === 'number' && typeof shape.height === 'number';
}
let myShape: Rectangle | Circle = { type:'rectangle', width: 10, height: 20 };
if (isRectangle(myShape)) {
console.log(`Rectangle area: ${myShape.width * myShape.height}`);
}
基于函数调用的模式
有时候,我们可以通过调用一个函数来确定值的类型。这种模式在处理异步操作或需要额外逻辑判断时很有用。
- 异步函数调用
- 假设我们有一个异步函数
fetchData
,它可能返回User
类型的数据或null
。我们可以创建一个类型保护函数来确保返回的数据是User
类型:
- 假设我们有一个异步函数
interface User {
id: number;
name: string;
}
async function fetchData(): Promise<User | null> {
// 模拟异步操作,这里返回一个模拟的User对象
return { id: 1, name: 'Alice' };
}
function isUser(data: User | null): data is User {
return data!== null;
}
fetchData().then(data => {
if (isUser(data)) {
console.log(`User name: ${data.name}`);
}
});
- 在上述代码中,
isUser
函数通过检查data
是否为null
来确定它是否是User
类型。
- 基于复杂逻辑的函数调用
- 假设我们有一个函数
parseValue
,它可能返回number
或string
,并且根据值的内容有不同的解析逻辑。我们可以创建一个类型保护函数:
- 假设我们有一个函数
function parseValue(input: string): number | string {
if (/^\d+$/.test(input)) {
return parseInt(input);
}
return input;
}
function isParsedNumber(value: number | string): value is number {
return typeof value === 'number';
}
let parsed = parseValue('42');
if (isParsedNumber(parsed)) {
console.log(`Parsed number: ${parsed * 2}`);
}
类型保护函数与泛型
类型保护函数与泛型结合可以实现更通用的类型检查。
- 简单泛型类型保护
- 假设我们有一个函数
identity
,它返回传入的值。我们可以创建一个类型保护函数来检查返回值是否是特定类型:
- 假设我们有一个函数
function identity<T>(arg: T): T {
return arg;
}
function isStringValue<T>(value: T): value is string & T {
return typeof value ==='string';
}
let result = identity<string | number>('test');
if (isStringValue(result)) {
console.log(result.length);
}
- 在上述代码中,
isStringValue
函数使用泛型T
,并且通过类型谓词value is string & T
来检查value
是否是string
类型。
- 泛型类与类型保护
- 考虑一个简单的泛型类
Box
,它可以存储任意类型的值。我们可以创建类型保护函数来检查Box
中存储的值的类型:
- 考虑一个简单的泛型类
class Box<T> {
constructor(private value: T) {}
getValue() {
return this.value;
}
}
function isNumberBox(box: Box<number | string>): box is Box<number> {
return typeof box.getValue() === 'number';
}
let numberBox = new Box<number | string>(42);
if (isNumberBox(numberBox)) {
console.log(`Box contains number: ${numberBox.getValue()}`);
}
类型保护函数在数组中的应用
当处理数组中的联合类型时,类型保护函数同样非常有用。
- 过滤数组元素
- 假设我们有一个数组,其中元素是
string | number
类型,我们想过滤出string
类型的元素:
- 假设我们有一个数组,其中元素是
function isString(value: string | number): value is string {
return typeof value ==='string';
}
let mixedArray: (string | number)[] = ['hello', 42, 'world'];
let stringArray = mixedArray.filter(isString);
console.log(stringArray.map(str => str.length));
- 在上述代码中,
filter
方法使用isString
类型保护函数来过滤出string
类型的元素,从而得到一个纯string
类型的数组。
- 遍历数组并执行特定类型操作
- 假设我们有一个数组,元素是
Dog | Cat
类型,我们想遍历数组并对Dog
类型的元素调用bark
方法,对Cat
类型的元素调用meow
方法:
- 假设我们有一个数组,元素是
class Dog {
bark() {
console.log('Woof!');
}
}
class Cat {
meow() {
console.log('Meow!');
}
}
function isDog(pet: Dog | Cat): pet is Dog {
return pet instanceof Dog;
}
let pets: (Dog | Cat)[] = [new Dog(), new Cat()];
pets.forEach(pet => {
if (isDog(pet)) {
pet.bark();
} else {
pet.meow();
}
});
类型保护函数与类型别名和接口
类型保护函数与类型别名和接口结合使用,可以更清晰地定义和使用类型。
- 与类型别名结合
- 假设我们有一个类型别名
MaybeNumber
,它表示number
或null
。我们可以创建一个类型保护函数来检查值是否是number
:
- 假设我们有一个类型别名
type MaybeNumber = number | null;
function isNumberValue(value: MaybeNumber): value is number {
return value!== null;
}
let numOrNull: MaybeNumber = 42;
if (isNumberValue(numOrNull)) {
console.log(numOrNull.toFixed(2));
}
- 与接口结合
- 假设我们有两个接口
Square
和Triangle
,它们组成联合类型Shape
。我们可以创建类型保护函数来区分它们:
- 假设我们有两个接口
interface Square {
type:'square';
sideLength: number;
}
interface Triangle {
type: 'triangle';
base: number;
height: number;
}
type Shape = Square | Triangle;
function isSquare(shape: Shape): shape is Square {
return shape.type ==='square';
}
let myShape: Shape = { type:'square', sideLength: 10 };
if (isSquare(myShape)) {
console.log(`Square area: ${myShape.sideLength * myShape.sideLength}`);
}
类型保护函数的局限性
虽然类型保护函数非常强大,但也有一些局限性。
- 运行时检查
- 类型保护函数依赖于运行时检查。这意味着如果在编译时就能确定类型,类型保护函数就没有作用。例如:
let myNumber: number = 42;
function isNumber(value: string | number): value is number {
return typeof value === 'number';
}
// 这里myNumber在编译时就已知是number类型,isNumber函数不会起到作用
- 复杂类型的局限性
- 对于非常复杂的类型,特别是涉及到递归类型或深度嵌套类型,编写有效的类型保护函数可能会很困难。例如,假设我们有一个递归类型表示树结构:
interface TreeNode {
value: number;
children?: TreeNode[];
}
// 要编写一个有效的类型保护函数来检查特定类型的TreeNode子结构会比较复杂
- 与其他类型系统特性的交互
- 类型保护函数需要与其他TypeScript类型系统特性(如类型推断、泛型等)正确交互。有时候,不正确的使用可能会导致类型错误或难以理解的代码行为。例如,在复杂的泛型场景下,类型保护函数可能无法按预期缩小类型范围。
最佳实践
- 保持函数简洁
- 类型保护函数应该尽可能简洁,只专注于检查类型。复杂的逻辑应该放在其他函数中。例如:
function isPositiveNumber(value: number | string): value is number {
if (typeof value === 'number') {
return value > 0;
}
return false;
}
// 更好的方式是分离逻辑
function isNumberValue(value: number | string): value is number {
return typeof value === 'number';
}
function isPositive(num: number): boolean {
return num > 0;
}
let value: number | string = 42;
if (isNumberValue(value) && isPositive(value)) {
console.log('Positive number');
}
-
命名规范
- 类型保护函数的命名应该清晰地表明它检查的类型。例如,
isString
、isNumber
、isUser
等命名能够让代码阅读者快速理解函数的用途。
- 类型保护函数的命名应该清晰地表明它检查的类型。例如,
-
测试类型保护函数
- 由于类型保护函数在运行时对类型检查起关键作用,对它们进行单元测试是很重要的。可以使用测试框架(如Jest)来测试类型保护函数的各种输入情况,确保它们按预期工作。例如:
import { isString } from './typeGuards';
test('isString should return true for string values', () => {
expect(isString('test')).toBe(true);
});
test('isString should return false for non - string values', () => {
expect(isString(42)).toBe(false);
});
- 文档化
- 对于复杂的类型保护函数,特别是那些依赖于特定业务逻辑或复杂类型检查的函数,应该进行文档化。可以使用JSDoc等工具来添加注释,解释函数的作用、输入和输出。例如:
/**
* Checks if the given value is a valid email address string.
* @param value The value to check.
* @returns True if the value is a valid email address string, false otherwise.
*/
function isEmail(value: string | number): value is string {
if (typeof value ==='string' && /^[\w -]+(\.[\w -]+)*@([\w -]+\.)+[a-zA - Z]{2,7}$/.test(value)) {
return true;
}
return false;
}
通过合理设计和使用TypeScript自定义类型保护函数,我们能够在处理联合类型和复杂类型时,编写出更健壮、类型安全的代码。同时,了解其局限性并遵循最佳实践,可以让我们更好地利用这一强大的功能。