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

TypeScript联合类型在条件判断中的高效使用

2022-09-262.4k 阅读

一、TypeScript联合类型基础

在TypeScript中,联合类型(Union Types)是一种强大的类型工具,它允许一个变量具有多种类型中的一种。联合类型通过竖线(|)来分隔不同的类型。例如,我们可以定义一个变量,它要么是string类型,要么是number类型:

let value: string | number;
value = 'hello';
value = 42;

这里value变量可以被赋值为字符串或者数字,这就是联合类型的基本表现形式。联合类型在很多场景下都非常有用,尤其是在函数参数和返回值的类型定义上。

假设我们有一个函数,它接收一个参数,这个参数可以是字符串或者数字,然后我们根据参数类型执行不同的操作:

function printValue(val: string | number) {
    if (typeof val ==='string') {
        console.log(val.length);
    } else {
        console.log(val.toFixed(2));
    }
}
printValue('world');
printValue(123.456);

在上述代码中,printValue函数接收一个联合类型string | number的参数val。通过typeof操作符进行类型判断,根据不同的类型执行不同的逻辑。

二、联合类型在条件判断中的简单应用

  1. typeof类型判断
    • 在JavaScript中,typeof是一个非常有用的操作符,在TypeScript联合类型的条件判断中同样重要。它可以帮助我们在运行时判断变量的类型。
    • 除了前面例子中使用typeof判断字符串和数字类型,它在更复杂的联合类型中也能发挥作用。比如,我们有一个联合类型包含stringnumberboolean
function handleValue(val: string | number | boolean) {
    if (typeof val ==='string') {
        console.log('It is a string:', val);
    } else if (typeof val === 'number') {
        console.log('It is a number:', val);
    } else {
        console.log('It is a boolean:', val);
    }
}
handleValue('test');
handleValue(100);
handleValue(true);
  1. instanceof类型判断
    • 当联合类型中包含对象类型时,instanceof操作符就派上用场了。instanceof用于判断一个对象是否是某个类的实例。
    • 假设我们有两个类DogCat,定义一个联合类型Dog | Cat,然后通过instanceof来判断:
class Dog {
    bark() {
        console.log('Woof!');
    }
}
class Cat {
    meow() {
        console.log('Meow!');
    }
}
function handlePet(pet: Dog | Cat) {
    if (pet instanceof Dog) {
        pet.bark();
    } else {
        pet.meow();
    }
}
const myDog = new Dog();
const myCat = new Cat();
handlePet(myDog);
handlePet(myCat);

三、类型保护(Type Guards)

  1. 自定义类型保护函数
    • 虽然typeofinstanceof在很多情况下能够满足需求,但在一些复杂场景下,我们可能需要自定义类型保护函数。
    • 类型保护函数的特点是返回一个类型谓词,语法形式为parameterName is Type。例如,我们定义一个判断是否是字符串的类型保护函数:
function isString(val: any): val is string {
    return typeof val ==='string';
}
function processValue(val: string | number) {
    if (isString(val)) {
        console.log(val.length);
    } else {
        console.log(val.toFixed(2));
    }
}
processValue('hello');
processValue(123);
- 在上述代码中,`isString`函数就是一个自定义类型保护函数。它接收一个`any`类型的参数(因为我们要在多种类型中进行判断),然后返回一个类型谓词`val is string`。在`processValue`函数中,当调用`isString(val)`为`true`时,TypeScript就知道`val`此时是`string`类型,从而可以安全地访问`string`类型的属性和方法。

2. 类型断言与类型保护的区别 - 类型断言(Type Assertion)是告诉编译器“我知道这个变量是什么类型,你就按我说的来”。例如let str = someValue as string,这里我们直接断言someValuestring类型。 - 而类型保护是通过运行时的检查来确保变量的类型。类型保护函数会在运行时进行判断,然后根据结果来确定类型。 - 类型断言更像是一种开发者对编译器的“声明”,不会进行运行时检查,所以如果断言错误可能会导致运行时错误。而类型保护是安全的,因为它基于运行时的实际类型进行判断。

四、联合类型在复杂条件判断中的应用

  1. 嵌套联合类型的条件判断
    • 有时候联合类型会嵌套得比较复杂。比如,我们有一个联合类型,它的成员又是联合类型:
type FirstLevel = (string | number) | boolean;
function handleFirstLevel(val: FirstLevel) {
    if (typeof val === 'boolean') {
        console.log('It is a boolean:', val);
    } else {
        if (typeof val ==='string') {
            console.log('It is a nested string:', val);
        } else {
            console.log('It is a nested number:', val);
        }
    }
}
handleFirstLevel(true);
handleFirstLevel('nested string');
handleFirstLevel(123);
- 在上述代码中,`FirstLevel`类型是一个嵌套的联合类型。`handleFirstLevel`函数首先判断外层的类型是否是`boolean`,如果不是,再进一步判断内层是`string`还是`number`。

2. 联合类型数组的条件判断 - 当联合类型出现在数组中时,条件判断也会变得复杂一些。例如,我们有一个数组,它的元素可以是string或者number

function processArray(arr: (string | number)[]) {
    arr.forEach((element) => {
        if (typeof element ==='string') {
            console.log('String element:', element);
        } else {
            console.log('Number element:', element);
        }
    });
}
const mixedArray: (string | number)[] = ['a', 1, 'b', 2];
processArray(mixedArray);
- 在`processArray`函数中,通过遍历数组,对每个元素使用`typeof`进行类型判断,然后根据不同类型执行不同操作。

五、条件类型与联合类型的结合

  1. 条件类型基础
    • 条件类型(Conditional Types)是TypeScript 2.8引入的一个强大特性。它允许我们根据类型关系选择不同的类型。语法形式为T extends U? X : Y,意思是如果类型T可以赋值给类型U,则结果为类型X,否则为类型Y
    • 例如,我们定义一个简单的条件类型:
type IsString<T> = T extends string? true : false;
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false
  1. 条件类型与联合类型的协同工作
    • 当条件类型与联合类型结合时,会产生一些有趣的效果。例如,我们有一个联合类型string | number,想要对其中的字符串类型提取其长度属性的类型,对数字类型提取其toFixed方法的返回类型:
type StringOrNumber = string | number;
type ExtractType<T> = T extends string? number : string;
type ExtractedTypes = {
    [P in StringOrNumber]: ExtractType<P>;
}[StringOrNumber];
// ExtractedTypes 是 number | string
- 在上述代码中,首先定义了`ExtractType`条件类型,它根据传入的类型是`string`还是其他类型返回不同的类型。然后通过映射类型和索引类型查询,对`StringOrNumber`联合类型中的每个类型应用`ExtractType`,最终得到`ExtractedTypes`类型。

六、联合类型在函数重载中的应用

  1. 函数重载基础
    • 函数重载(Function Overloading)允许我们为同一个函数定义多个不同参数列表和返回类型的函数签名。
    • 例如,我们定义一个add函数,它可以接收两个数字相加,也可以接收两个字符串拼接:
function add(a: number, b: number): number;
function add(a: string, b: string): string;
function add(a: any, b: any): any {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    } else if (typeof a ==='string' && typeof b ==='string') {
        return a + b;
    }
    return null;
}
const numResult = add(1, 2);
const strResult = add('hello', 'world');
- 在上述代码中,前面两个函数声明是函数重载签名,最后一个函数实现包含了具体的逻辑。TypeScript会根据传入的参数类型来选择合适的函数签名。

2. 联合类型在函数重载中的作用 - 联合类型在函数重载中经常用于参数类型和返回类型的定义。比如,我们可以将上述add函数的参数类型改为联合类型:

function add(a: string | number, b: string | number): string | number;
function add(a: any, b: any): any {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    } else if (typeof a ==='string' && typeof b ==='string') {
        return a + b;
    }
    return null;
}
const numResult = add(1, 2);
const strResult = add('hello', 'world');
- 这里将参数类型定义为`string | number`联合类型,使得函数可以接收更灵活的参数,同时返回类型也是`string | number`联合类型,以匹配不同的操作结果。

七、联合类型条件判断的性能考虑

  1. 多次类型判断的性能影响
    • 在复杂的联合类型条件判断中,如果存在多次类型判断,可能会对性能产生一定影响。例如,在一个包含多个分支的if - else语句中,每次判断都需要消耗一定的时间。
    • 假设我们有一个联合类型A | B | C | D | E,并且需要对每个类型进行不同的处理:
function complexHandle(val: 'A' | 'B' | 'C' | 'D' | 'E') {
    if (val === 'A') {
        // 执行A的处理逻辑
    } else if (val === 'B') {
        // 执行B的处理逻辑
    } else if (val === 'C') {
        // 执行C的处理逻辑
    } else if (val === 'D') {
        // 执行D的处理逻辑
    } else {
        // 执行E的处理逻辑
    }
}
- 在这种情况下,随着联合类型分支的增多,`if - else`链会变长,每次判断都需要一定的时间开销。如果性能要求较高,可以考虑使用`switch`语句替代`if - else`链,因为`switch`语句在某些JavaScript引擎中可能有更好的性能表现。

2. 类型保护函数的性能 - 自定义类型保护函数在运行时会进行额外的检查,这也会带来一定的性能开销。虽然类型保护函数能保证类型安全,但在性能敏感的场景下,需要权衡使用。 - 例如,一个频繁调用的函数中使用了类型保护函数:

function isBigNumber(num: number): num is number {
    return num > 1000;
}
function processNumbers(arr: number[]) {
    arr.forEach((num) => {
        if (isBigNumber(num)) {
            // 处理大数字的逻辑
        }
    });
}
- 在上述代码中,`isBigNumber`类型保护函数在每次调用时都会执行判断逻辑。如果数组元素较多,这个判断的性能开销可能会变得明显。在这种情况下,可以考虑在数据进入函数之前进行预处理,减少在循环中使用类型保护函数的次数。

八、实际项目中联合类型条件判断的场景

  1. API数据处理
    • 在前端开发中,从API获取的数据可能具有多种类型。例如,一个API可能返回用户信息,用户信息中的年龄字段可能是数字,也可能是字符串(比如在某些错误情况下返回'unknown')。
interface User {
    name: string;
    age: string | number;
}
function processUser(user: User) {
    if (typeof user.age === 'number') {
        console.log(`${user.name} is ${user.age} years old.`);
    } else {
        console.log(`${user.name}'s age is unknown.`);
    }
}
const user1: User = { name: 'Alice', age: 25 };
const user2: User = { name: 'Bob', age: 'unknown' };
processUser(user1);
processUser(user2);
- 在`processUser`函数中,通过对`user.age`的类型判断,进行不同的处理,以适应API可能返回的不同数据类型。

2. 组件属性处理 - 在React或Vue等前端框架中,组件的属性也可能具有联合类型。例如,一个按钮组件的size属性可以是'small''medium''large'字符串,也可以是自定义的数字类型表示尺寸。

import React from'react';
type ButtonSize ='small' |'medium' | 'large' | number;
interface ButtonProps {
    size: ButtonSize;
    children: React.ReactNode;
}
const Button: React.FC<ButtonProps> = ({ size, children }) => {
    let style: React.CSSProperties = {};
    if (typeof size ==='string') {
        if (size ==='small') {
            style = { fontSize: '12px' };
        } else if (size ==='medium') {
            style = { fontSize: '14px' };
        } else {
            style = { fontSize: '16px' };
        }
    } else {
        style = { fontSize: `${size}px` };
    }
    return <button style={style}>{children}</button>;
};
export default Button;
- 在上述React组件代码中,`Button`组件根据`size`属性的不同类型,设置不同的样式,以实现灵活的按钮尺寸设置。

九、联合类型条件判断的常见错误及解决方法

  1. 遗漏类型判断分支
    • 当联合类型的分支较多时,很容易遗漏某个类型的判断分支。例如,我们有一个联合类型'red' | 'green' | 'blue' | 'yellow',在处理颜色的函数中遗漏了对'yellow'的处理:
function handleColor(color: 'red' | 'green' | 'blue' | 'yellow') {
    if (color ==='red') {
        console.log('It is red');
    } else if (color === 'green') {
        console.log('It is green');
    } else if (color === 'blue') {
        console.log('It is blue');
    }
    // 遗漏了对 'yellow' 的处理
}
- 解决方法是在编写代码时仔细检查联合类型的所有可能值,并确保每个值都有相应的处理逻辑。可以使用`default`分支(在`switch`语句中)或者最后一个`else`分支来处理未预期的情况。例如:
function handleColor(color: 'red' | 'green' | 'blue' | 'yellow') {
    switch (color) {
        case'red':
            console.log('It is red');
            break;
        case 'green':
            console.log('It is green');
            break;
        case 'blue':
            console.log('It is blue');
            break;
        case 'yellow':
            console.log('It is yellow');
            break;
        default:
            console.log('Unknown color');
    }
}
  1. 类型判断错误
    • 有时候可能会因为对类型判断方法的错误使用而导致逻辑错误。比如,在判断对象类型时错误地使用typeof
class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
function handleObject(obj: Person | string) {
    if (typeof obj === 'object') {
        // 这里不能用 typeof 判断对象实例,应该用 instanceof
        console.log(obj.name);
    } else {
        console.log(obj);
    }
}
const person = new Person('John');
handleObject(person);
handleObject('test');
- 解决方法是要清楚不同类型判断方法的适用场景。对于对象类型,应该使用`instanceof`进行判断,而对于基本类型,使用`typeof`。修改后的代码如下:
class Person {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
}
function handleObject(obj: Person | string) {
    if (obj instanceof Person) {
        console.log(obj.name);
    } else {
        console.log(obj);
    }
}
const person = new Person('John');
handleObject(person);
handleObject('test');

十、联合类型条件判断的优化技巧

  1. 使用类型别名和接口来简化联合类型
    • 当联合类型变得复杂时,使用类型别名(Type Alias)和接口(Interface)可以使其更易读和维护。例如,我们有一个复杂的联合类型:
type ComplexUnion = (string | number)[] | { key: string; value: number | boolean } | null;
- 可以通过定义接口和类型别名来简化:
interface KeyValue {
    key: string;
    value: number | boolean;
}
type ArrayUnion = (string | number)[];
type SimplifiedUnion = ArrayUnion | KeyValue | null;
- 这样在使用联合类型时,代码会更加清晰,也便于修改和扩展。

2. 提前过滤联合类型 - 在一些情况下,可以在进入复杂条件判断之前提前过滤联合类型。例如,我们有一个联合类型string | number | null,并且在后续逻辑中只关心stringnumber类型:

function processValues(values: (string | number | null)[]) {
    const validValues = values.filter((value): value is string | number => value!== null);
    validValues.forEach((value) => {
        if (typeof value ==='string') {
            console.log('String:', value);
        } else {
            console.log('Number:', value);
        }
    });
}
const mixedValues: (string | number | null)[] = ['a', 1, null, 'b', 2];
processValues(mixedValues);
- 在上述代码中,通过`filter`方法提前过滤掉`null`值,使得后续的条件判断只需要处理`string`和`number`类型,简化了逻辑。

通过以上对TypeScript联合类型在条件判断中的深入探讨,我们可以看到联合类型在前端开发中提供了强大的类型灵活性,合理运用联合类型的条件判断可以使我们的代码更加健壮和高效。在实际项目中,根据具体场景选择合适的类型判断方法和优化技巧,能够更好地发挥联合类型的优势。