MK
摩柯社区 - 一个极简的技术知识社区
AI 面试

TypeScript中的类型收缩现象解析

2024-12-173.0k 阅读

一、类型收缩的基本概念

在 TypeScript 中,类型收缩(Type Narrowing)是指根据某些运行时条件,将一个较为宽泛的类型范围缩小到一个更具体的类型范围的过程。这种机制允许开发者在代码执行过程中,利用已知的信息来更精确地确定变量的类型,从而使 TypeScript 编译器能够进行更准确的类型检查,减少类型错误的发生。

例如,假设有一个函数接收一个参数 value,其类型为 string | number。在函数内部,通过 typeof 操作符检查 value 的类型,根据不同的检查结果,value 的类型会被“收缩”到更具体的 stringnumber 类型。这使得我们在后续针对 value 的操作中,可以调用该具体类型所特有的方法或属性,而不用担心类型不匹配的问题。

二、基于 typeof 的类型收缩

1. typeof 用于基本类型的收缩

typeof 是 JavaScript 中用于检测变量类型的操作符,在 TypeScript 中,它同样可以用于类型收缩。当我们使用 typeof 对变量进行类型检查时,TypeScript 编译器会根据 typeof 的返回值,智能地缩小变量的类型范围。

function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.length); // 这里 value 的类型被收缩为 string,可以访问 length 属性
    } else {
        console.log(value.toFixed(2)); // 这里 value 的类型被收缩为 number,可以调用 toFixed 方法
    }
}

在上述代码中,printValue 函数接收一个 string | number 类型的参数 value。通过 typeof 检查 value 是否为 string 类型,如果是,则 valueif 代码块内的类型被收缩为 string,此时可以安全地访问 string 类型特有的 length 属性。如果 value 不是 string 类型,那么在 else 代码块内,value 的类型被收缩为 number,可以调用 number 类型的 toFixed 方法。

2. typeof 对复杂类型属性的收缩

typeof 不仅可以用于基本类型的收缩,对于对象类型中属性的类型判断也能起到类型收缩的作用。

interface User {
    name: string;
    age: number;
    email?: string;
}

function printUserInfo(user: User | null) {
    if (user && typeof user.email ==='string') {
        console.log(`User ${user.name} has email: ${user.email}`);
    } else {
        console.log(`User ${user?.name} has no email`);
    }
}

在这个例子中,printUserInfo 函数接收一个 User | null 类型的参数 user。首先通过 user && 检查 user 是否为 null,确保 user 存在。然后使用 typeof user.email ==='string' 进一步检查 user 对象中 email 属性是否为 string 类型。如果满足条件,在相应代码块内,user 的类型会被收缩为 Useremail 属性存在且为 string 类型,从而可以安全地访问 email 属性。

三、基于 instanceof 的类型收缩

1. 类实例的类型收缩

instanceof 操作符用于判断一个对象是否是某个类的实例。在 TypeScript 中,它同样能够实现类型收缩的功能。

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}

class Dog extends Animal {
    bark() {
        console.log(`${this.name} is barking`);
    }
}

class Cat extends Animal {
    meow() {
        console.log(`${this.name} is meowing`);
    }
}

function handleAnimal(animal: Animal) {
    if (animal instanceof Dog) {
        animal.bark(); // 这里 animal 的类型被收缩为 Dog,可以调用 bark 方法
    } else if (animal instanceof Cat) {
        animal.meow(); // 这里 animal 的类型被收缩为 Cat,可以调用 meow 方法
    }
}

const myDog = new Dog('Buddy');
const myCat = new Cat('Whiskers');

handleAnimal(myDog);
handleAnimal(myCat);

在上述代码中,handleAnimal 函数接收一个 Animal 类型的参数 animal。通过 instanceof 分别判断 animal 是否是 Dog 类或 Cat 类的实例。如果是 Dog 类的实例,在相应 if 代码块内,animal 的类型被收缩为 Dog,可以调用 Dog 类特有的 bark 方法;如果是 Cat 类的实例,animal 的类型被收缩为 Cat,可以调用 Cat 类特有的 meow 方法。

2. 内置对象的类型收缩

instanceof 同样适用于内置对象的类型判断与收缩。

function processValue(value: any) {
    if (value instanceof Date) {
        console.log(`The date is ${value.toISOString()}`); // value 的类型被收缩为 Date
    } else if (Array.isArray(value)) {
        console.log(`The array length is ${value.length}`); // value 的类型被收缩为数组类型
    }
}

const myDate = new Date();
const myArray = [1, 2, 3];

processValue(myDate);
processValue(myArray);

在这个例子中,processValue 函数接收一个 any 类型的参数 value。通过 instanceof Date 判断 value 是否为 Date 类型的实例,如果是,则在相应代码块内 value 的类型被收缩为 Date,可以调用 Date 对象的 toISOString 方法。通过 Array.isArray 判断 value 是否为数组,虽然这里不是严格意义上的 instanceof,但同样实现了类型收缩,在相应代码块内 value 的类型被收缩为数组类型,可以访问 length 属性。

四、基于真值检测的类型收缩

1. 基本类型的真值检测收缩

在 JavaScript 中,某些值在布尔上下文中具有特定的真值。例如,nullundefined0、空字符串 ''NaN 在布尔上下文中被视为 false,而其他值被视为 true。在 TypeScript 中,我们可以利用这种真值检测来进行类型收缩。

function printNonNullValue(value: string | null) {
    if (value) {
        console.log(value.length); // 这里 value 的类型被收缩为非 null 的 string
    }
}

printNonNullValue 函数中,参数 value 的类型为 string | null。通过 if (value) 进行真值检测,只有当 value 为非 null 时才会进入 if 代码块,此时 value 的类型被收缩为 string,可以安全地访问 length 属性。

2. 对象类型的真值检测收缩

对于对象类型,同样可以通过真值检测进行类型收缩。

interface User {
    name: string;
}

function printUserName(user: User | null) {
    if (user) {
        console.log(user.name); // 这里 user 的类型被收缩为 User
    }
}

在这个例子中,printUserName 函数接收一个 User | null 类型的参数 user。通过 if (user) 检查 user 是否为 null,如果不为 null,则在 if 代码块内 user 的类型被收缩为 User,可以访问 User 对象的 name 属性。

五、基于 in 操作符的类型收缩

1. 检查对象属性存在性的类型收缩

in 操作符用于检查对象是否包含某个属性。在 TypeScript 中,它可以根据属性的存在与否来收缩对象的类型。

interface Admin {
    name: string;
    role: string;
}

interface User {
    name: string;
    age: number;
}

function printUserDetails(user: Admin | User) {
    if ('role' in user) {
        console.log(`Admin ${user.name} has role ${user.role}`); // user 的类型被收缩为 Admin
    } else {
        console.log(`User ${user.name} is ${user.age} years old`); // user 的类型被收缩为 User
    }
}

const admin: Admin = { name: 'John', role: 'admin' };
const normalUser: User = { name: 'Jane', age: 25 };

printUserDetails(admin);
printUserDetails(normalUser);

在上述代码中,printUserDetails 函数接收一个 Admin | User 类型的参数 user。通过 'role' in user 检查 user 对象是否包含 role 属性。如果包含,则在相应代码块内 user 的类型被收缩为 Admin,可以访问 role 属性;如果不包含,则 user 的类型被收缩为 User,可以访问 age 属性。

2. 联合类型中 in 操作符的类型收缩应用场景

在更复杂的联合类型场景中,in 操作符的类型收缩也能发挥重要作用。

interface Circle {
    kind: 'circle';
    radius: number;
}

interface Square {
    kind:'square';
    sideLength: number;
}

function calculateArea(shape: Circle | Square) {
    if ('radius' in shape) {
        return Math.PI * shape.radius * shape.radius; // shape 的类型被收缩为 Circle
    } else {
        return shape.sideLength * shape.sideLength; // shape 的类型被收缩为 Square
    }
}

const circle: Circle = { kind: 'circle', radius: 5 };
const square: Square = { kind:'square', sideLength: 4 };

console.log(calculateArea(circle));
console.log(calculateArea(square));

这里 calculateArea 函数接收一个 Circle | Square 类型的参数 shape。通过 'radius' in shape 判断 shape 是否为 Circle 类型(因为只有 Circleradius 属性),从而实现类型收缩,分别针对 CircleSquare 类型进行面积计算。

六、自定义类型保护函数实现类型收缩

1. 简单类型保护函数

自定义类型保护函数是一种特殊的函数,其返回值类型是一个类型谓词。类型谓词的形式为 parameterName is Type,其中 parameterName 是函数参数名,Type 是要判断的类型。

function isString(value: any): value is string {
    return typeof value ==='string';
}

function processValue(value: string | number) {
    if (isString(value)) {
        console.log(value.length); // 这里 value 的类型被收缩为 string
    } else {
        console.log(value.toFixed(2)); // 这里 value 的类型被收缩为 number
    }
}

在上述代码中,isString 函数就是一个自定义类型保护函数。它接收一个 any 类型的参数 value,并返回一个类型谓词 value is string。在 processValue 函数中,通过调用 isString 函数,根据其返回结果实现了 value 类型的收缩。

2. 复杂对象类型的自定义类型保护函数

对于复杂对象类型,同样可以定义自定义类型保护函数来实现类型收缩。

interface Bird {
    fly: () => void;
    species: string;
}

interface Fish {
    swim: () => void;
    species: string;
}

function isBird(animal: Bird | Fish): animal is Bird {
    return 'fly' in animal;
}

function describeAnimal(animal: Bird | Fish) {
    if (isBird(animal)) {
        console.log(`${animal.species} can fly`); // animal 的类型被收缩为 Bird
    } else {
        console.log(`${animal.species} can swim`); // animal 的类型被收缩为 Fish
    }
}

const myBird: Bird = { fly: () => console.log('Flying'), species: 'Eagle' };
const myFish: Fish = { swim: () => console.log('Swimming'), species: 'Tuna' };

describeAnimal(myBird);
describeAnimal(myFish);

这里 isBird 函数是针对 Bird | Fish 联合类型的自定义类型保护函数。通过检查 animal 对象是否包含 fly 属性来判断是否为 Bird 类型,在 describeAnimal 函数中,依据 isBird 的返回结果实现了 animal 类型的收缩。

七、类型收缩中的注意事项

1. 类型收缩的范围限制

虽然类型收缩能够让我们在局部代码块中精确地确定变量类型,但这种收缩是有范围限制的。一旦离开相应的条件代码块,变量的类型会恢复到原来较宽泛的类型。

function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.length); // 这里 value 是 string 类型
    }
    // 这里 value 又变回 string | number 类型,不能直接访问 length 属性
}

在上述代码中,只有在 if (typeof value ==='string') 的代码块内,value 的类型被收缩为 string。离开这个代码块后,value 的类型恢复为 string | number,如果在此处尝试访问 length 属性,会导致类型错误。

2. 复杂类型收缩的潜在错误

在处理复杂的联合类型或对象类型时,不正确的类型收缩逻辑可能会导致潜在的错误。

interface A {
    a: string;
}

interface B {
    b: string;
}

function processAB(obj: A | B) {
    if ('a' in obj) {
        console.log(obj.a);
    } else {
        console.log(obj.b); // 这里如果 obj 实际上是 A 类型且没有 b 属性,会导致运行时错误
    }
}

在这个例子中,processAB 函数接收 A | B 类型的参数 obj。在 else 分支中,假设 obj 一定是 B 类型并访问 b 属性,但如果 obj 实际是 A 类型且没有 b 属性,就会导致运行时错误。因此,在进行复杂类型收缩时,需要确保逻辑的严谨性。

3. 与类型断言的区别

类型断言(Type Assertion)也是一种让开发者手动指定变量类型的方式,但它与类型收缩有本质区别。类型断言是开发者强制告诉编译器变量的类型,而不依赖于运行时的条件判断。而类型收缩是根据运行时的实际情况,由编译器自动进行类型范围的缩小。

function printLength(value: string | number) {
    // 类型断言
    const strValue = value as string;
    console.log(strValue.length); // 这里强制将 value 断言为 string 类型,可能导致运行时错误

    // 类型收缩
    if (typeof value ==='string') {
        console.log(value.length); // 这里根据运行时判断,安全地访问 length 属性
    }
}

在上述代码中,使用类型断言 value as string 可能会在 value 实际为 number 类型时导致运行时错误,而通过 typeof 进行的类型收缩则是基于运行时条件,更加安全可靠。

通过深入理解和正确运用 TypeScript 中的类型收缩机制,开发者能够编写出更健壮、类型安全的代码,减少潜在的运行时错误,提高代码的可维护性和可读性。无论是简单的基本类型判断,还是复杂的对象类型处理,类型收缩都为我们在开发过程中提供了强大的类型控制能力。