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

TypeScript联合类型与类型推断的优化技巧

2021-10-067.7k 阅读

TypeScript联合类型基础

在TypeScript中,联合类型(Union Types)允许我们在一个变量中表示多种类型。这在很多场景下非常有用,比如当一个函数可以接受不同类型的参数,或者一个变量在不同阶段可能具有不同类型时。 联合类型使用竖线(|)来分隔不同的类型。例如:

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

这里value变量可以被赋值为字符串或者数字类型。

联合类型在函数参数中的应用

当一个函数可以接受多种类型的参数时,联合类型就派上用场了。

function printValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.toUpperCase());
    } else {
        console.log(value.toFixed(2));
    }
}

printValue('world');
printValue(3.14159);

printValue函数中,根据传入参数的实际类型,我们使用typeof进行类型检查并执行不同的操作。

类型推断与联合类型

TypeScript的类型推断机制在联合类型的场景下也发挥着重要作用。当我们为一个变量赋予联合类型的值时,TypeScript会根据赋值的上下文来推断变量的具体类型。

let myValue: string | number;
myValue = 'initial string';
// 此时TypeScript推断myValue为string类型
let length: number = myValue.length; 

但是,如果我们之后再为myValue赋一个数字值,就会出现类型错误。

myValue = 42; 
// 报错:类型“number”上不存在属性“length”
let length: number = myValue.length; 

这是因为TypeScript在推断类型时,会根据最近一次赋值的类型来确定变量的类型。

函数返回值的类型推断与联合类型

函数的返回值也可以是联合类型,并且TypeScript会根据函数内部的逻辑进行类型推断。

function getValue(random: boolean): string | number {
    if (random) {
        return 'random string';
    } else {
        return 123;
    }
}

let result = getValue(true);
// 此时TypeScript推断result为string类型
let length: number = result.length; 

这里result的类型根据getValue函数的返回值被推断为string,因为我们传入的参数true导致函数返回了字符串。

联合类型与类型保护

类型保护(Type Guards)是一种在运行时检查类型的机制,在联合类型的场景下非常重要。通过类型保护,我们可以在代码中安全地处理联合类型的不同情况。

typeof类型保护

typeof是最常用的类型保护之一,前面我们在printValue函数中已经使用过。

function handleValue(value: string | number) {
    if (typeof value ==='string') {
        // 在这个代码块中,TypeScript知道value是string类型
        console.log(value.split(' '));
    } else {
        // 在这个代码块中,TypeScript知道value是number类型
        console.log(Math.sqrt(value));
    }
}

通过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 | Dog | Cat) {
    if (animal instanceof Dog) {
        animal.bark();
    } else if (animal instanceof Cat) {
        animal.meow();
    } else {
        console.log(animal.name);
    }
}

let myDog = new Dog('Buddy');
let myCat = new Cat('Whiskers');
let myAnimal = new Animal('Generic Animal');

handleAnimal(myDog);
handleAnimal(myCat);
handleAnimal(myAnimal);

handleAnimal函数中,通过instanceof我们可以区分不同的动物类型并调用相应的方法。

in类型保护

in操作符也可以作为类型保护,用于检查对象是否包含某个属性。

interface Circle {
    radius: number;
}

interface Square {
    sideLength: number;
}

function calculateArea(shape: Circle | Square) {
    if ('radius' in shape) {
        return Math.PI * shape.radius * shape.radius;
    } else {
        return shape.sideLength * shape.sideLength;
    }
}

let circle: Circle = { radius: 5 };
let square: Square = { sideLength: 4 };

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

这里通过in操作符检查对象是否包含radius属性,从而确定shape的具体类型并计算相应的面积。

联合类型的优化技巧

尽量缩小联合类型的范围

在定义联合类型时,应尽量精确地指定可能的类型,避免包含不必要的类型。例如,如果一个函数只接受数字或者字符串,且字符串必须是特定的几个值,我们可以这样定义:

function processValue(value: number | 'option1' | 'option2') {
    if (typeof value ==='string') {
        if (value === 'option1') {
            console.log('Processing option1');
        } else {
            console.log('Processing option2');
        }
    } else {
        console.log('Processing number:', value);
    }
}

processValue(10);
processValue('option1');
processValue('option2');

这样不仅让代码逻辑更清晰,也有助于TypeScript进行更准确的类型推断。

使用类型别名和接口来管理联合类型

对于复杂的联合类型,使用类型别名(Type Alias)或接口(Interface)可以使代码更具可读性和可维护性。

type StringOrNumber = string | number;

function handleStringOrNumber(value: StringOrNumber) {
    if (typeof value ==='string') {
        console.log(value.length);
    } else {
        console.log(value.toFixed(2));
    }
}

handleStringOrNumber('test');
handleStringOrNumber(3.14);

使用类型别名StringOrNumber,使得handleStringOrNumber函数的参数类型更易于理解。

利用函数重载与联合类型

函数重载(Function Overloading)可以与联合类型结合使用,为不同类型的参数提供更精确的类型定义。

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;
}

let numResult = add(2, 3);
let strResult = add('Hello, ', 'world');

这里通过函数重载,我们为add函数定义了两种不同的参数类型和返回类型,使得调用add函数时TypeScript可以进行更准确的类型检查。

联合类型与类型推断的高级应用

条件类型与联合类型

条件类型(Conditional Types)可以与联合类型结合使用,实现更复杂的类型转换和推断。

type Exclude<T, U> = T extends U? never : T;
type T1 = Exclude<string | number | boolean, number>; 
// T1的类型为string | boolean

在这个例子中,Exclude类型帮助我们从联合类型string | number | boolean中排除了number类型。

映射类型与联合类型

映射类型(Mapped Types)也可以与联合类型协同工作,对对象的属性类型进行批量转换。

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

type ReadonlyPerson = {
    readonly [P in keyof Person]: Person[P];
};

let person: Person = { name: 'John', age: 30 };
let readonlyPerson: ReadonlyPerson = person; 
// 这里readonlyPerson的属性变为只读

虽然这个例子没有直接涉及联合类型,但我们可以想象在更复杂的场景下,结合联合类型对对象属性类型进行更灵活的操作。

分布式条件类型与联合类型

分布式条件类型(Distributive Conditional Types)在处理联合类型时具有特殊的行为。当条件类型作用于联合类型时,会自动对联合类型的每个成员进行条件判断。

type ToArray<T> = T extends any? T[] : never;
type StrOrNumArray = ToArray<string | number>; 
// StrOrNumArray的类型为string[] | number[]

这里ToArray类型将联合类型string | number中的每个类型都转换为对应的数组类型,形成新的联合类型string[] | number[]

联合类型在实际项目中的应用场景

API响应处理

在处理API响应时,响应数据的结构可能会因为各种原因而有所不同。例如,一个获取用户信息的API,可能在用户存在时返回完整的用户对象,在用户不存在时返回null

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

function getUser(): User | null {
    // 模拟API调用
    const random = Math.random();
    if (random > 0.5) {
        return { id: 1, name: 'Alice' };
    } else {
        return null;
    }
}

let user = getUser();
if (user!== null) {
    console.log(user.name);
}

这里getUser函数的返回值是User | null联合类型,通过user!== null的类型保护,我们可以安全地访问user的属性。

表单输入处理

在处理表单输入时,不同的表单字段可能有不同的类型。例如,一个包含姓名(字符串)和年龄(数字)的表单。

function processFormInput(name: string, age: string | number) {
    let ageValue: number;
    if (typeof age ==='string') {
        ageValue = parseInt(age);
    } else {
        ageValue = age;
    }
    console.log(`Name: ${name}, Age: ${ageValue}`);
}

processFormInput('Bob', '25');
processFormInput('Charlie', 30);

processFormInput函数中,age参数是string | number联合类型,我们根据其实际类型进行相应的处理。

组件属性处理

在前端组件开发中,组件的属性可能接受多种类型。例如,一个按钮组件,其disabled属性可以是布尔值,也可以是一个函数来动态决定是否禁用。

interface ButtonProps {
    label: string;
    disabled: boolean | (() => boolean);
}

function Button({ label, disabled }: ButtonProps) {
    let isDisabled: boolean;
    if (typeof disabled === 'function') {
        isDisabled = disabled();
    } else {
        isDisabled = disabled;
    }
    return (
        <button disabled={isDisabled}>
            {label}
        </button>
    );
}

<Button label="Click me" disabled={false} />
<Button label="Dynamic disable" disabled={() => Math.random() > 0.5} />

这里ButtonProps接口中的disabled属性是boolean | (() => boolean)联合类型,Button组件根据disabled的实际类型来决定按钮是否禁用。

联合类型与类型推断的常见问题及解决方法

类型推断不准确

有时候TypeScript的类型推断可能不够准确,导致代码出现类型错误。例如:

let myVar: string | number;
let result = myVar.length; 
// 报错:类型“string | number”上不存在属性“length”

解决方法是使用类型保护,如typeof进行明确的类型检查。

let myVar: string | number;
if (typeof myVar ==='string') {
    let result = myVar.length; 
}

联合类型过于宽泛

如果联合类型包含过多不必要的类型,会使代码逻辑变得复杂且难以维护。例如:

function handleData(data: string | number | boolean | null | undefined) {
    // 处理逻辑变得非常复杂
}

解决方法是尽量缩小联合类型的范围,只包含实际可能的类型。

function handleData(data: string | number) {
    // 处理逻辑更清晰
}

联合类型与函数重载冲突

在使用函数重载与联合类型时,可能会出现冲突。例如:

function processValue(a: number, b: number): number;
function processValue(a: string, b: string): string;
function processValue(a: any, b: any) {
    if (typeof a === 'number' && typeof b === 'number') {
        return a + b;
    } else if (typeof a ==='string' && typeof b ==='string') {
        return a + b;
    }
    return null;
}

let result = processValue('a', 1); 
// 这里会调用到最后一个函数定义,虽然应该报错

解决方法是确保函数重载的定义准确,并且在函数实现中进行严格的类型检查。

function processValue(a: number, b: number): number;
function processValue(a: string, b: string): string;
function processValue(a: any, b: any) {
    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 argument types');
}

通过深入理解联合类型与类型推断,并掌握这些优化技巧,我们可以在前端开发中更高效地使用TypeScript,编写出更健壮、可维护的代码。无论是处理复杂的业务逻辑,还是进行组件开发和API交互,联合类型与类型推断都能为我们提供强大的类型支持。在实际项目中,不断实践和总结经验,能让我们更好地利用TypeScript的这些特性,提升开发效率和代码质量。例如,在大型前端应用中,合理使用联合类型可以减少代码中的类型断言,增强类型安全性,同时利用类型推断减少冗余的类型声明,使代码更加简洁明了。希望通过本文的介绍,读者能在自己的项目中灵活运用这些知识,解决遇到的实际问题。