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

TypeScript联合类型在复杂场景中的使用

2021-04-105.7k 阅读

一、联合类型基础概念回顾

在深入探讨 TypeScript 联合类型在复杂场景中的使用之前,让我们先简单回顾一下联合类型的基础概念。

联合类型允许我们在一个变量中表示多种类型。语法上,使用竖线(|)来分隔不同的类型。例如,假设有一个函数,它既可以接受字符串,也可以接受数字,那么我们可以这样定义参数的类型:

function printValue(value: string | number) {
    console.log(value);
}

printValue('Hello');
printValue(42);

在这个例子中,value 参数可以是 string 类型或者 number 类型。TypeScript 会根据实际传入的值来进行类型检查。如果我们传入一个其他类型的值,比如布尔值,TypeScript 就会抛出类型错误。

二、联合类型在函数参数中的复杂应用

(一)处理多种可能的输入类型

在实际开发中,函数可能会面临多种不同类型的输入情况。例如,一个解析函数,它可能接受字符串形式的 JSON 数据,也可能接受已经解析好的对象。

function parseData(data: string | object) {
    let result;
    if (typeof data ==='string') {
        try {
            result = JSON.parse(data);
        } catch (error) {
            console.error('解析字符串失败:', error);
        }
    } else {
        result = data;
    }
    return result;
}

const jsonString = '{"name":"Alice","age":30}';
const jsonObject = {name: 'Bob', age: 25};

console.log(parseData(jsonString));
console.log(parseData(jsonObject));

这里,parseData 函数接受 stringobject 类型的参数。如果传入的是字符串,函数会尝试将其解析为 JSON 对象;如果传入的是对象,函数直接返回该对象。通过这种方式,我们可以灵活地处理不同类型的输入,同时借助 TypeScript 的类型检查,确保代码的健壮性。

(二)基于联合类型的重载

函数重载是 TypeScript 中一个强大的特性,它允许我们为同一个函数定义多个不同的签名。联合类型在函数重载中有着重要的应用。

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;
    }
    throw new Error('不支持的类型');
}

console.log(add(1, 2));
console.log(add('Hello, ', 'world'));

在这个例子中,我们定义了两个重载签名。第一个签名表示 add 函数接受两个数字参数并返回一个数字;第二个签名表示接受两个字符串参数并返回一个字符串。实际的函数实现根据传入参数的类型来执行相应的逻辑。如果传入的参数类型不符合任何一个重载签名,TypeScript 会抛出类型错误,并且在运行时,函数会抛出 不支持的类型 的错误。

三、联合类型在对象属性中的复杂场景

(一)可选属性与联合类型结合

在定义对象类型时,我们常常会遇到某些属性是可选的,并且这些可选属性可能有多种类型。

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

const user1: User = {name: 'Charlie'};
const user2: User = {name: 'David', age: 28};
const user3: User = {name: 'Eve', age: 'twenty - five'};

User 接口中,age 属性是可选的,并且它可以是 number 类型或者 string 类型。这样的定义使得我们在创建 User 对象时更加灵活,同时通过 TypeScript 的类型系统,我们可以在开发过程中避免错误地设置 age 属性的类型。

(二)联合类型作为对象属性值的集合

有时候,对象的某个属性可能会有一组固定的可能值,这些值具有不同的类型。例如,一个表示用户状态的对象,状态可能是数字类型的代码,也可能是字符串类型的描述。

interface UserStatus {
    status: 1 | 2 | 'active' | 'inactive';
}

const status1: UserStatus = {status: 1};
const status2: UserStatus = {status: 'active'};

这里,status 属性的类型是一个联合类型,包含了数字 12 以及字符串 'active''inactive'。通过这种方式,我们可以明确地限制 status 属性的取值范围,确保在赋值时不会出现意外的类型。

四、联合类型与数组

(一)异构数组

异构数组是指数组中元素的类型不完全相同。联合类型可以很好地描述异构数组的类型。

let mixedArray: (string | number)[] = ['apple', 1, 'banana', 2];

在这个例子中,mixedArray 是一个异构数组,它的元素类型是 string 或者 number。TypeScript 会根据数组元素的实际类型进行检查。当我们向数组中添加元素时,如果元素类型不符合联合类型,就会报错。

(二)基于联合类型的数组操作

在处理异构数组时,我们可能需要编写一些通用的操作函数。例如,一个函数用于过滤出数组中的数字元素。

function filterNumbers(arr: (string | number)[]): number[] {
    return arr.filter((item): item is number => typeof item === 'number');
}

const mixedArray2: (string | number)[] = ['apple', 1, 'banana', 2];
const numbersOnly = filterNumbers(mixedArray2);
console.log(numbersOnly);

filterNumbers 函数中,我们使用了类型谓词 item is number 来明确地告诉 TypeScript,经过 filter 操作后返回的数组元素类型是 number。这样,我们可以安全地返回 number 类型的数组。

五、联合类型在条件类型中的应用

(一)条件类型与联合类型的结合

条件类型是 TypeScript 2.8 引入的一个强大特性,它允许我们根据类型关系来选择不同的类型。联合类型在条件类型中有着广泛的应用。

type ToString<T> = T extends string? string : T extends number? string : never;

type StringOrNumberToString = ToString<string | number>;
// StringOrNumberToString 的类型为 string

在这个例子中,ToString 是一个条件类型。如果类型 Tstring,则返回 string;如果 Tnumber,也返回 string;否则返回 never。当我们将联合类型 string | number 作为 ToString 的参数时,TypeScript 会分别对联合类型中的每个类型应用条件类型,最终得到 string 类型。

(二)分布式条件类型与联合类型

分布式条件类型是条件类型在联合类型上的一种特殊行为。当条件类型的参数是联合类型时,会自动对联合类型中的每个成员进行条件类型的计算,并将结果合并为一个新的联合类型。

type Exclude<T, U> = T extends U? never : T;

type Numbers = Exclude<1 | 2 | 3 | 4, 3 | 4>;
// Numbers 的类型为 1 | 2

Exclude 条件类型中,对于联合类型 1 | 2 | 3 | 4 中的每个成员,判断其是否属于 3 | 4。如果属于,则返回 never;如果不属于,则保留该成员。最终,Exclude 操作返回了 1 | 2,即从原始联合类型中排除了 34

六、联合类型在类型别名中的复杂使用

(一)通过联合类型创建复杂类型别名

类型别名是给类型定义一个新的名字,方便在代码中复用。联合类型可以与其他类型一起组合成复杂的类型别名。

type MaybeStringOrNumber = string | number | null | undefined;

function printMaybeValue(value: MaybeStringOrNumber) {
    if (value!== null && value!== undefined) {
        console.log(value);
    }
}

printMaybeValue('Hello');
printMaybeValue(42);
printMaybeValue(null);
printMaybeValue(undefined);

在这个例子中,MaybeStringOrNumber 类型别名表示一个值可能是 stringnumbernull 或者 undefinedprintMaybeValue 函数在处理这种复杂类型时,先进行了 nullundefined 的检查,然后再进行打印操作。

(二)类型别名与条件类型、联合类型的综合应用

我们可以将类型别名、条件类型和联合类型结合起来,创建非常灵活和强大的类型定义。

type IsString<T> = T extends string? true : false;

type StringOrNumberInfo<T> = T extends string | number? {type: IsString<T>, value: T} : never;

type StringInfo = StringOrNumberInfo<string>;
// StringInfo 的类型为 { type: true; value: string; }

type NumberInfo = StringOrNumberInfo<number>;
// NumberInfo 的类型为 { type: false; value: number; }

在这个例子中,IsString 是一个条件类型,用于判断类型 T 是否为 stringStringOrNumberInfo 是一个更复杂的类型别名,它根据传入的类型 T 是否为 stringnumber,返回一个包含 typevalue 属性的对象类型。其中,type 属性根据 IsString 条件类型来确定,value 属性就是传入的类型 T

七、联合类型在 React 开发中的复杂场景

(一)React 组件属性的联合类型

在 React 开发中,组件的属性可能有多种类型。例如,一个按钮组件,它的 size 属性可能是字符串类型的 'small''medium''large',也可能是数字类型来表示自定义大小。

import React from'react';

interface ButtonProps {
    size: 'small' |'medium' | 'large' | number;
    children: React.ReactNode;
}

const Button: React.FC<ButtonProps> = ({size, children}) => {
    let style = {};
    if (typeof size === 'number') {
        style = {fontSize: size};
    } else {
        style = {fontSize: size ==='small'? 12 : size ==='medium'? 16 : 20};
    }
    return <button style={style}>{children}</button>;
};

export default Button;

在这个 Button 组件中,size 属性的类型是一个联合类型。组件内部根据 size 的实际类型来设置不同的样式。这样,我们可以在使用 Button 组件时,根据需求灵活地设置 size 属性。

(二)处理联合类型的 React 事件

React 事件处理函数也可能会遇到联合类型的情况。例如,一个输入框组件,它可能触发 change 事件,事件对象的 target 属性在不同的输入类型下可能有不同的类型。

import React, {ChangeEvent} from'react';

interface InputProps {
    value: string | number;
    onChange: (event: ChangeEvent<HTMLInputElement | HTMLTextAreaElement>) => void;
}

const Input: React.FC<InputProps> = ({value, onChange}) => {
    return <input value={value} onChange={onChange} />;
};

export default Input;

在这个 Input 组件中,onChange 事件处理函数的参数 event 的类型是 ChangeEvent<HTMLInputElement | HTMLTextAreaElement>。这是因为输入框可能是 HTMLInputElement 类型(如文本输入框、数字输入框等),也可能是 HTMLTextAreaElement 类型(多行文本输入框)。通过这种联合类型的定义,我们可以在事件处理函数中正确地处理不同类型输入框的 change 事件。

八、联合类型在代码迁移和兼容中的应用

(一)从 JavaScript 迁移到 TypeScript 时处理类型兼容

当我们将 JavaScript 代码迁移到 TypeScript 时,常常会遇到变量类型不确定的情况。联合类型可以帮助我们逐步添加类型注释,同时保持代码的兼容性。 假设我们有一段 JavaScript 代码:

function formatValue(value) {
    if (typeof value ==='string') {
        return value.toUpperCase();
    } else if (typeof value === 'number') {
        return value.toString();
    }
    return null;
}

将其迁移到 TypeScript 时,可以这样添加类型注释:

function formatValue(value: string | number) {
    if (typeof value ==='string') {
        return value.toUpperCase();
    } else if (typeof value === 'number') {
        return value.toString();
    }
    return null;
}

通过使用联合类型 string | number,我们既明确了 value 参数可能的类型,又保持了与原有 JavaScript 代码逻辑的一致性。在后续的开发中,我们可以根据实际情况进一步细化类型,或者添加更多的类型检查。

(二)处理不同版本 API 的兼容性

在使用第三方库或者处理不同版本的 API 时,可能会遇到接口返回值类型不同的情况。联合类型可以用来处理这种兼容性问题。 假设我们使用一个图片加载库,旧版本的 loadImage 函数返回一个 Promise<string>,表示图片的 URL;而新版本返回一个 Promise<HTMLImageElement>,表示加载后的图片 DOM 元素。

type LoadImageResult = string | HTMLImageElement;

async function loadImage(): Promise<LoadImageResult> {
    // 这里根据实际使用的库版本进行不同的逻辑处理
    // 示例逻辑,假设使用旧版本库
    const url = 'https://example.com/image.jpg';
    return url;
}

async function displayImage() {
    const result = await loadImage();
    if (typeof result ==='string') {
        const img = new Image();
        img.src = result;
        document.body.appendChild(img);
    } else {
        document.body.appendChild(result);
    }
}

通过定义 LoadImageResult 联合类型,我们可以在调用 loadImage 函数后,根据返回值的实际类型进行不同的处理,从而兼容不同版本的 API。

九、联合类型在错误处理中的应用

(一)函数返回值的错误类型联合

在编写函数时,除了正常的返回值类型,我们还需要考虑可能出现的错误情况。联合类型可以很好地表示函数返回值可能包含的错误类型。

function divide(a: number, b: number): number | Error {
    if (b === 0) {
        return new Error('除数不能为零');
    }
    return a / b;
}

const result1 = divide(10, 2);
if (result1 instanceof Error) {
    console.error(result1.message);
} else {
    console.log(result1);
}

const result2 = divide(5, 0);
if (result2 instanceof Error) {
    console.error(result2.message);
} else {
    console.log(result2);
}

divide 函数中,返回值类型是 number | Error。如果除数为零,函数返回一个 Error 对象;否则返回正常的除法结果。调用函数时,我们可以通过检查返回值是否为 Error 实例来进行相应的错误处理。

(二)异步操作中的错误联合类型

在异步操作中,同样可以使用联合类型来处理可能出现的错误。例如,使用 fetch 进行网络请求时,可能会遇到网络错误或者服务器返回错误状态码。

async function fetchData(url: string): Promise<string | Error> {
    try {
        const response = await fetch(url);
        if (!response.ok) {
            throw new Error(`HTTP error! status: ${response.status}`);
        }
        return await response.text();
    } catch (error) {
        return error as Error;
    }
}

fetchData('https://example.com/api/data').then(result => {
    if (result instanceof Error) {
        console.error(result.message);
    } else {
        console.log(result);
    }
});

fetchData 函数中,返回值类型是 string | Error。如果网络请求成功且响应状态码正常,返回响应的文本内容;如果出现错误,返回一个 Error 对象。在处理异步操作的结果时,通过检查返回值类型来进行错误处理。

十、联合类型与类型保护

(一)类型保护的概念

类型保护是一种运行时检查机制,用于缩小联合类型的范围。TypeScript 提供了几种类型保护的方式,如 typeof 检查、instanceof 检查等。当我们使用这些类型保护机制时,可以在特定的代码块中确定联合类型中实际的类型。

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

printValue2 函数中,通过 typeof 类型保护,我们可以在 if 代码块中明确 value 的类型是 string,从而可以安全地访问 length 属性;在 else 代码块中明确 value 的类型是 number,可以安全地调用 toFixed 方法。

(二)自定义类型保护

除了内置的类型保护,我们还可以自定义类型保护函数。自定义类型保护函数需要返回一个类型谓词。

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

function processValue(value: string | number) {
    if (isString(value)) {
        console.log(value.toUpperCase());
    } else {
        console.log(value.toFixed(2));
    }
}

在这个例子中,isString 是一个自定义类型保护函数,它返回 value is string 类型谓词。在 processValue 函数中,通过调用 isString 函数,我们可以在 if 代码块中确定 valuestring 类型,进而进行相应的操作。

通过深入理解和应用联合类型在各种复杂场景中的使用,我们可以充分发挥 TypeScript 的类型系统优势,编写出更加健壮、可维护的前端代码。无论是处理函数参数、对象属性,还是在 React 开发、错误处理等方面,联合类型都为我们提供了强大的类型表达能力。同时,结合类型保护等机制,我们可以在运行时安全地处理联合类型中的不同类型,确保代码的正确性和稳定性。