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

TypeScript联合类型的使用技巧与案例分享

2021-03-071.8k 阅读

联合类型基础概念

在 TypeScript 中,联合类型(Union Types)是一种非常有用的类型工具,它允许一个变量或者函数参数等接受多种不同类型的值。简单来说,联合类型就是将多个类型通过 | 运算符组合在一起,表示这个值可以是这些类型中的任意一种。

例如,假设我们有一个函数,它既可以接受字符串类型的参数,也可以接受数字类型的参数,就可以使用联合类型来定义参数类型:

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

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

在上述代码中,printValue 函数的 value 参数被定义为 string | number 联合类型,这意味着它可以接受字符串或者数字作为参数。这种灵活性在很多实际场景中非常实用。

联合类型的使用场景

  1. 函数参数的灵活性 许多函数在设计时需要处理不同类型的数据,但处理逻辑可能类似。比如一个格式化输出函数,既可以格式化数字为货币形式,也可以格式化字符串为特定的文本格式:
function formatOutput(input: string | number) {
    if (typeof input === 'number') {
        return input.toFixed(2).replace(/\d(?=(\d{3})+\.)/g, '$&,');
    } else {
        return input.toUpperCase();
    }
}

console.log(formatOutput(12345.67));
console.log(formatOutput('hello world'));

在这个 formatOutput 函数中,通过使用联合类型 string | number,使得函数能够处理两种不同类型的输入,并根据输入类型进行不同的处理。

  1. 表示可能为不同类型的属性 假设我们有一个配置对象,其中某个属性可能是字符串表示的路径,也可能是数字表示的端口号:
interface ServerConfig {
    host: string;
    portOrPath: string | number;
}

const config1: ServerConfig = {
    host: 'localhost',
    portOrPath: 8080
};

const config2: ServerConfig = {
    host: 'example.com',
    portOrPath: '/api'
};

ServerConfig 接口中,portOrPath 属性被定义为 string | number 联合类型,允许根据实际情况设置不同类型的值。

  1. 处理可能为 nullundefined 的值 在很多情况下,一个变量可能存在值,也可能是 nullundefined。例如,从一个可能返回 null 的 API 获取数据:
function getUserData(): string | null {
    // 模拟 API 调用
    const shouldReturnNull = Math.random() > 0.5;
    if (shouldReturnNull) {
        return null;
    }
    return 'user data';
}

const data = getUserData();
if (data!== null) {
    console.log(data.length);
}

在上述代码中,getUserData 函数返回 string | null 联合类型。调用该函数后,需要通过检查 data 是否为 null 来安全地使用返回的数据。

联合类型与类型保护

  1. typeof 类型保护 当处理联合类型时,我们经常需要根据值的实际类型来执行不同的逻辑。typeof 是一种常用的类型保护机制。例如,我们有一个函数,它接受 string | number 联合类型的参数,并根据类型输出不同的信息:
function handleValue(value: string | number) {
    if (typeof value ==='string') {
        console.log(`The string length is ${value.length}`);
    } else {
        console.log(`The number squared is ${value * value}`);
    }
}

handleValue('test');
handleValue(5);

handleValue 函数中,通过 typeof value ==='string' 这种类型保护判断,TypeScript 能够在 if 块内部明确 value 的类型为 string,从而可以安全地访问 value.length。在 else 块中,value 的类型被推断为 number

  1. instanceof 类型保护 当联合类型中包含类类型时,instanceof 可以用于类型保护。例如,假设我们有两个类 DogCat,并且有一个函数接受 Dog | Cat 联合类型的参数:
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);

handlePet 函数中,通过 pet instanceof Dog 这种类型保护判断,TypeScript 能够在 if 块内部明确 pet 的类型为 Dog,从而可以安全地调用 pet.bark()。在 else 块中,pet 的类型被推断为 Cat,可以调用 pet.meow()

  1. 自定义类型保护函数 除了 typeofinstanceof,我们还可以定义自己的类型保护函数。自定义类型保护函数的返回值必须是一个类型谓词,其形式为 parameterName is Type。例如,我们定义一个函数来判断一个值是否为字符串数组:
function isStringArray(value: any): value is string[] {
    return Array.isArray(value) && value.every(item => typeof item ==='string');
}

function processValue(value: string[] | number) {
    if (isStringArray(value)) {
        console.log(`The array length is ${value.length}`);
    } else {
        console.log(`The number is ${value}`);
    }
}

processValue(['a', 'b', 'c']);
processValue(10);

processValue 函数中,通过调用自定义类型保护函数 isStringArray,TypeScript 能够在 if 块内部明确 value 的类型为 string[],从而可以安全地访问 value.length。在 else 块中,value 的类型被推断为 number

联合类型与类型别名

  1. 使用类型别名简化联合类型 当联合类型比较复杂或者需要在多个地方使用时,使用类型别名可以使代码更加简洁和易于维护。例如,假设我们有一个表示文件类型的联合类型,它可以是图片文件(jpgpng)或者文本文件(txtmd):
type ImageFileType = 'jpg' | 'png';
type TextFileType = 'txt' |'md';
type FileType = ImageFileType | TextFileType;

function handleFile(fileType: FileType) {
    if (fileType === 'jpg' || fileType === 'png') {
        console.log('Processing image file');
    } else {
        console.log('Processing text file');
    }
}

handleFile('jpg');
handleFile('txt');

在上述代码中,通过定义 ImageFileTypeTextFileType 两个类型别名,然后再组合成 FileType 类型别名,使得联合类型更加清晰和易于管理。在 handleFile 函数中,可以直接使用 FileType 类型别名来定义参数类型。

  1. 类型别名中的条件类型与联合类型结合 条件类型与联合类型结合可以实现非常强大的类型推导。例如,我们有一个类型别名 GetReturnType,它根据传入的函数类型返回其返回值类型:
type GetReturnType<T> = T extends (...args: any[]) => infer R? R : never;

function add(a: number, b: number): number {
    return a + b;
}

function greet(name: string): string {
    return `Hello, ${name}`;
}

type AddReturnType = GetReturnType<typeof add>; // number
type GreetReturnType = GetReturnType<typeof greet>; // string

type AllReturnTypes = GetReturnType<typeof add | typeof greet>;
// number | string,联合类型会自动展开并推导每个函数的返回值类型

在这个例子中,GetReturnType 是一个条件类型,它通过 infer 关键字推断函数的返回值类型。当传入联合类型的函数类型(typeof add | typeof greet)时,GetReturnType 会自动推导每个函数的返回值类型,并组成联合类型 number | string

联合类型与接口

  1. 接口中的联合类型属性 接口中可以包含联合类型的属性,这在描述复杂对象结构时非常有用。例如,我们定义一个表示用户信息的接口,其中 phone 属性可以是字符串形式的电话号码,也可以是 null 表示没有电话号码:
interface User {
    name: string;
    phone: string | null;
}

const user1: User = {
    name: 'Alice',
    phone: '123 - 456 - 7890'
};

const user2: User = {
    name: 'Bob',
    phone: null
};

User 接口中,phone 属性被定义为 string | null 联合类型,允许根据用户实际情况设置不同的值。

  1. 接口合并与联合类型 当进行接口合并时,如果相同属性在不同接口中有不同类型,TypeScript 会将其合并为联合类型。例如:
interface BaseUser {
    name: string;
}

interface ExtendedUser {
    age: number;
    name: string | null;
}

interface CompleteUser extends BaseUser, ExtendedUser {}

const user: CompleteUser = {
    name: 'Charlie',
    age: 30
};

在上述代码中,BaseUser 接口定义了 name 属性为 string 类型,ExtendedUser 接口定义了 name 属性为 string | null 类型。当 CompleteUser 接口通过 extends 合并这两个接口时,name 属性的类型变为 string | null,这是因为 TypeScript 会将相同属性的不同类型合并为联合类型。

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

  1. 函数重载与联合类型参数 函数重载允许我们为同一个函数定义多个不同的参数列表和返回类型。联合类型在函数重载中可以用来处理更灵活的参数情况。例如,我们定义一个 printData 函数,它既可以接受单个字符串参数并打印,也可以接受一个字符串数组参数并逐个打印:
function printData(data: string): void;
function printData(data: string[]): void;
function printData(data: string | string[]) {
    if (Array.isArray(data)) {
        data.forEach(item => console.log(item));
    } else {
        console.log(data);
    }
}

printData('single string');
printData(['string1','string2']);

在上述代码中,通过函数重载,我们为 printData 函数定义了两种不同的参数类型:stringstring[]。实际实现函数时,参数类型为 string | string[] 联合类型,根据传入参数的实际类型进行不同的处理。

  1. 联合类型与函数重载返回值 函数重载的返回值也可以根据不同的参数类型是联合类型。例如,我们定义一个 processInput 函数,它根据输入是数字还是字符串返回不同类型的值:
function processInput(input: number): number;
function processInput(input: string): string;
function processInput(input: string | number) {
    if (typeof input === 'number') {
        return input * 2;
    } else {
        return input.toUpperCase();
    }
}

const result1 = processInput(5);
const result2 = processInput('hello');

在这个例子中,根据 processInput 函数参数类型的不同,返回值类型分别为 numberstring。通过函数重载和联合类型,我们可以清晰地定义这种不同输入对应不同输出的函数行为。

联合类型的解构与展开

  1. 联合类型的解构 当解构一个可能是联合类型的对象时,需要特别小心。例如,假设我们有一个函数,它接受一个可能是 { name: string }{ title: string } 的对象:
function printInfo(obj: { name: string } | { title: string }) {
    if ('name' in obj) {
        const { name } = obj;
        console.log(`Name: ${name}`);
    } else {
        const { title } = obj;
        console.log(`Title: ${title}`);
    }
}

printInfo({ name: 'Alice' });
printInfo({ title: 'Engineer' });

printInfo 函数中,通过 'name' in obj 这种类型保护判断,我们可以安全地解构 obj 对象。如果 objname 属性,就解构出 name;否则,解构出 title

  1. 联合类型的展开 在使用数组展开或者对象展开时,联合类型也需要注意。例如,我们有两个类型别名,一个表示数字数组,一个表示字符串数组,然后定义一个联合类型并进行展开:
type NumberArray = number[];
type StringArray = string[];
type MixedArray = NumberArray | StringArray;

function logArray(arr: number[] | string[]) {
    console.log(...arr);
}

const numArr: NumberArray = [1, 2, 3];
const strArr: StringArray = ['a', 'b', 'c'];

logArray(numArr);
logArray(strArr);

logArray 函数中,通过展开联合类型的数组 arr,可以根据实际传入的数组类型(number[]string[])进行正确的输出。不过,需要注意的是,如果联合类型更加复杂,例如包含不同结构的对象展开,可能需要更多的类型检查和处理。

联合类型的高级技巧

  1. 联合类型的交叉运算 虽然联合类型本身是“或”的关系,但有时候我们可以通过一些技巧实现类似“交叉”的效果。例如,假设我们有两个类型别名 AB,我们想要一个类型,它的值既满足 A 又满足 B
type A = { a: string };
type B = { b: number };

type ABIntersection = {
    [K in keyof (A & B)]: (A & B)[K];
};

const ab: ABIntersection = {
    a: 'value',
    b: 10
};

在上述代码中,通过 A & B 先进行交叉类型运算,然后利用映射类型 [K in keyof (A & B)]: (A & B)[K] 来构建一个新类型 ABIntersection,它的值同时满足 AB 的属性要求。这种技巧在处理需要同时满足多种类型条件的场景时非常有用。

  1. 联合类型与映射类型的结合 映射类型可以与联合类型结合,实现对联合类型中每个类型的属性进行变换。例如,我们有一个联合类型 UserType,它包含 AdminRegularUser 两种类型,我们想要为每个类型的所有属性都添加 readonly 修饰:
interface Admin {
    name: string;
    privileges: string[];
}

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

type UserType = Admin | RegularUser;

type ReadonlyUserType = {
    readonly [P in keyof UserType]: UserType[P];
};

const readonlyUser: ReadonlyUserType = {
    name: 'User',
    age: 25
} as const;

在上述代码中,通过映射类型 { readonly [P in keyof UserType]: UserType[P]; },我们为 UserType 联合类型中的每个类型的属性都添加了 readonly 修饰。这样可以方便地对联合类型中的所有类型进行统一的属性变换。

  1. 联合类型的条件过滤 有时候我们需要从联合类型中过滤掉某些类型。例如,我们有一个联合类型 AllTypes,它包含 stringnumberboolean,我们想要过滤掉 boolean 类型:
type AllTypes = string | number | boolean;

type FilteredTypes = Exclude<AllTypes, boolean>;

const filteredValue: FilteredTypes = 'test';

在上述代码中,使用 Exclude 工具类型,它接受两个类型参数,第一个是联合类型,第二个是要排除的类型。Exclude<AllTypes, boolean> 会从 AllTypes 联合类型中过滤掉 boolean 类型,得到 string | number 类型。这种条件过滤在处理复杂联合类型时可以帮助我们得到更精确的类型。

联合类型在实际项目中的应用案例

  1. 前端表单验证 在前端开发中,表单数据的验证是一个常见的需求。假设我们有一个表单,其中某个字段可以是数字或者特定格式的字符串(例如日期字符串)。我们可以使用联合类型来定义该字段的值类型,并进行验证:
type FormFieldValue = number | string;

function validateFormField(value: FormFieldValue) {
    if (typeof value === 'number') {
        return value > 0;
    } else {
        const datePattern = /^\d{4}-\d{2}-\d{2}$/;
        return datePattern.test(value);
    }
}

const numberValue: FormFieldValue = 10;
const stringValue: FormFieldValue = '2023 - 10 - 01';

console.log(validateFormField(numberValue));
console.log(validateFormField(stringValue));

在这个例子中,FormFieldValue 定义为 number | string 联合类型。validateFormField 函数根据值的实际类型进行不同的验证逻辑,这在处理表单数据时可以确保数据的有效性。

  1. API 响应处理 在与后端 API 交互时,API 的响应数据可能有不同的结构。例如,一个获取用户信息的 API,当用户存在时返回用户对象,当用户不存在时返回 null。我们可以使用联合类型来处理这种情况:
interface User {
    id: number;
    name: string;
}

type UserResponse = User | null;

async function fetchUser(): Promise<UserResponse> {
    // 模拟 API 调用
    const shouldReturnNull = Math.random() > 0.5;
    if (shouldReturnNull) {
        return null;
    }
    return { id: 1, name: 'User' };
}

fetchUser().then(response => {
    if (response!== null) {
        console.log(`User ID: ${response.id}, Name: ${response.name}`);
    } else {
        console.log('User not found');
    }
});

在上述代码中,UserResponse 定义为 User | null 联合类型。fetchUser 函数返回这个联合类型的值,调用者在处理响应时通过检查 response 是否为 null 来进行不同的操作,这使得对 API 响应的处理更加灵活和安全。

  1. 组件属性处理 在前端组件开发中,组件的属性可能接受多种类型。例如,一个按钮组件,它的 size 属性可以是字符串(如 'small''medium''large'),也可以是数字表示自定义尺寸:
import React from'react';

type ButtonSize ='small' |'medium' | 'large' | number;

interface ButtonProps {
    label: string;
    size?: ButtonSize;
}

const Button: React.FC<ButtonProps> = ({ label, size ='medium' }) => {
    let sizeClass = '';
    if (typeof size ==='string') {
        sizeClass = `btn - ${size}`;
    } else {
        sizeClass = `btn - custom - ${size}`;
    }
    return <button className={sizeClass}>{label}</button>;
};

export default Button;

在这个 React 组件的例子中,ButtonSize 定义为联合类型,ButtonProps 中的 size 属性使用了这个联合类型。组件内部根据 size 的实际类型来设置不同的 CSS 类名,使得按钮组件可以适应多种尺寸设置方式。

通过以上详细的介绍、代码示例以及实际案例,我们对 TypeScript 联合类型的使用技巧有了较为深入的理解。联合类型在 TypeScript 编程中是一个非常强大的工具,合理运用它可以提高代码的灵活性和健壮性,使我们能够更好地处理复杂的类型场景。