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

TypeScript复合类型的构建与使用

2021-02-017.1k 阅读

联合类型(Union Types)

在 TypeScript 中,联合类型是一种复合类型,它允许一个变量具有多种类型中的一种。联合类型使用竖线(|)来分隔不同的类型。例如,假设我们有一个函数,它可以接受一个字符串或者一个数字作为参数,我们可以这样定义参数的类型:

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

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

在上面的代码中,printValue 函数的 value 参数可以是 string 类型或者 number 类型。当我们调用这个函数时,传入字符串或者数字都不会报错。

联合类型的类型检查

TypeScript 会对联合类型进行类型检查。当我们在使用联合类型的变量时,我们只能访问这些类型中共有的属性和方法。例如:

function printLength(value: string | number) {
    // 下面这行代码会报错,因为 number 类型没有 length 属性
    // console.log(value.length); 
}

printLength 函数中,我们不能直接访问 value.length,因为 number 类型没有 length 属性。要解决这个问题,我们可以使用类型守卫(Type Guards)。

使用类型守卫处理联合类型

类型守卫是一种运行时检查,它可以让我们在代码中缩小联合类型的范围。常见的类型守卫有 typeofinstanceof 等。

使用 typeof 作为类型守卫处理 string | number 联合类型:

function printLength(value: string | number) {
    if (typeof value ==='string') {
        console.log(value.length); 
    } else {
        console.log('It is a number, no length property.');
    }
}

printLength('Hello'); 
printLength(42); 

在上面的代码中,通过 typeof value ==='string' 这个类型守卫,我们在 if 代码块中可以确定 valuestring 类型,因此可以安全地访问 length 属性。

交叉类型(Intersection Types)

交叉类型是将多个类型合并为一个类型。它包含了所有类型的特性。交叉类型使用 & 符号来定义。例如,如果我们有两个类型 AB,我们可以创建一个新的类型 C,它同时拥有 AB 的属性:

type A = { name: string };
type B = { age: number };

type C = A & B;

let person: C = { name: 'John', age: 30 }; 

在上面的代码中,C 类型是 AB 的交叉类型,所以 person 对象必须同时具有 name 属性(类型为 string)和 age 属性(类型为 number)。

交叉类型的应用场景

交叉类型在很多场景下都非常有用,比如当我们需要一个对象既有某个接口的属性,又有其他额外属性时。假设我们有一个 Button 接口,定义了按钮的基本属性,然后我们想要创建一个 SubmitButton 类型,它不仅具有 Button 的属性,还具有 form 属性:

interface Button {
    label: string;
    onClick(): void;
}

interface Form {
    form: string;
}

type SubmitButton = Button & Form;

let submitButton: SubmitButton = {
    label: 'Submit',
    onClick() {
        console.log('Button clicked');
    },
    form: 'loginForm'
}; 

这样,SubmitButton 类型的对象就同时具备了 ButtonForm 的属性。

类型别名与接口的复合类型构建

类型别名构建复合类型

类型别名可以用来创建联合类型和交叉类型。我们前面已经看到了使用类型别名创建交叉类型的例子,下面再来看一个使用类型别名创建更复杂联合类型的情况。假设我们有不同类型的用户,普通用户和管理员用户,我们可以这样定义:

type RegularUser = {
    username: string;
    password: string;
};

type AdminUser = {
    username: string;
    password: string;
    isAdmin: boolean;
};

type User = RegularUser | AdminUser;

function displayUser(user: User) {
    if ('isAdmin' in user) {
        console.log(`${user.username} is an admin.`);
    } else {
        console.log(`${user.username} is a regular user.`);
    }
}

let regular: RegularUser = { username: 'user1', password: 'pass1' };
let admin: AdminUser = { username: 'admin1', password: 'adminpass', isAdmin: true };

displayUser(regular); 
displayUser(admin); 

在上面的代码中,我们通过类型别名 User 创建了一个联合类型,它可以是 RegularUser 或者 AdminUser。在 displayUser 函数中,我们使用 'isAdmin' in user 作为类型守卫来判断用户类型。

接口构建复合类型

接口也可以用于构建复合类型,特别是交叉类型。我们可以通过继承多个接口来创建一个具有多个接口属性的新接口。例如:

interface Shape {
    color: string;
}

interface Square {
    sideLength: number;
}

interface ColoredSquare extends Shape, Square { }

let mySquare: ColoredSquare = {
    color: 'blue',
    sideLength: 5
}; 

在这个例子中,ColoredSquare 接口继承了 ShapeSquare 接口,所以 ColoredSquare 类型的对象必须同时具有 colorsideLength 属性。

函数类型的复合

联合函数类型

函数类型也可以组成联合类型。假设我们有两个不同参数类型的函数,我们可以将它们组成一个联合函数类型。例如:

type AddNumbers = (a: number, b: number) => number;
type ConcatenateStrings = (a: string, b: string) => string;

type MathOrStringOperation = AddNumbers | ConcatenateStrings;

function performOperation(operation: MathOrStringOperation) {
    if (typeof (operation as AddNumbers).length === 'number') {
        let result = (operation as AddNumbers)(5, 3);
        console.log('Number operation result:', result);
    } else {
        let result = (operation as ConcatenateStrings)('Hello, ', 'world');
        console.log('String operation result:', result);
    }
}

let add: AddNumbers = (a, b) => a + b;
let concat: ConcatenateStrings = (a, b) => a + b;

performOperation(add); 
performOperation(concat); 

在上面的代码中,MathOrStringOperation 是一个联合函数类型,它可以是 AddNumbers 类型的函数(接受两个数字并返回一个数字)或者 ConcatenateStrings 类型的函数(接受两个字符串并返回一个字符串)。在 performOperation 函数中,我们通过类型断言和一些简单的类型检查来确定调用哪个函数。

交叉函数类型

交叉函数类型相对较少见,但在某些特定场景下也很有用。它表示一个函数需要同时满足多个函数类型的签名。例如:

interface LoggableFunction {
    (message: string): void;
    log(message: string): void;
}

let myFunction: LoggableFunction = function (message) {
    console.log(message);
};

myFunction('Direct call'); 
myFunction.log('Call through log method'); 

在这个例子中,LoggableFunction 是一个交叉函数类型,它要求函数既可以像普通函数一样调用(接受一个字符串参数并打印),又要有一个 log 方法,该方法也接受一个字符串参数并打印。

数组与元组的复合类型

数组的联合类型

数组元素可以具有联合类型。例如,我们可以创建一个数组,它的元素可以是字符串或者数字:

let mixedArray: (string | number)[] = ['Hello', 42]; 

在上面的数组定义中,mixedArray 数组的元素可以是 string 类型或者 number 类型。

元组的联合类型

元组也可以与联合类型结合使用。元组是一种固定长度和固定类型顺序的数组。假设我们有一个函数,它可以返回不同类型的元组:

type Result1 = [string, number];
type Result2 = [boolean, string];

type CombinedResult = Result1 | Result2;

function getResult(): CombinedResult {
    const random = Math.random();
    if (random > 0.5) {
        return ['Success', 42]; 
    } else {
        return [false, 'Failure']; 
    }
}

let result = getResult();
if (typeof result[0] ==='string') {
    console.log(`Operation was successful. Value: ${result[1]}`);
} else {
    console.log(`Operation failed. Reason: ${result[1]}`);
}

在上面的代码中,CombinedResult 是一个联合类型,它可以是 Result1 类型的元组(第一个元素是字符串,第二个元素是数字)或者 Result2 类型的元组(第一个元素是布尔值,第二个元素是字符串)。在 getResult 函数中,根据随机数返回不同类型的元组,然后在使用 result 时,通过类型检查来处理不同类型的元组。

数组与交叉类型

数组元素也可以是交叉类型。例如,假设我们有一个数组,它的元素需要同时具有 name 属性(字符串类型)和 age 属性(数字类型):

type Person = { name: string } & { age: number };

let people: Person[] = [
    { name: 'Alice', age: 25 },
    { name: 'Bob', age: 30 }
]; 

在这个例子中,people 数组的元素是 Person 类型,而 Person 是一个交叉类型,它要求元素同时具有 nameage 属性。

类型推断与复合类型

联合类型的类型推断

当 TypeScript 进行类型推断时,对于联合类型也有特定的规则。例如,当我们给一个变量赋值为联合类型的字面量时,TypeScript 会推断出这个变量的类型为联合类型。

let value = 'Hello' || 42; 
// value 的类型被推断为 string | number

在上面的代码中,value 的类型被推断为 string | number,因为 'Hello' || 42 这个表达式的结果可能是字符串(如果 'Hello' 为真)或者数字(如果 'Hello' 为假,这里 'Hello' 恒为真,但 TypeScript 从逻辑上推断其可能为两种类型之一)。

交叉类型的类型推断

对于交叉类型,TypeScript 会根据赋值的对象来推断类型。例如:

let obj = { name: 'John', age: 30 }; 
// obj 的类型被推断为 { name: string; age: number; }

let combined: { name: string } & { age: number } = obj; 

在这个例子中,obj 的类型被推断为 { name: string; age: number; },然后我们可以将 obj 赋值给 combined,因为 combined 的类型是 { name: string } & { age: number }obj 的类型符合这个交叉类型的要求。

泛型与复合类型

泛型联合类型

泛型可以与联合类型结合使用,增强代码的灵活性。例如,我们可以创建一个函数,它可以接受一个数组,数组元素可以是不同类型,但这些类型都有一个共同的方法。假设我们有一个 Printable 接口,定义了 print 方法:

interface Printable {
    print(): void;
}

class Book implements Printable {
    constructor(public title: string) { }
    print() {
        console.log(`Book: ${this.title}`);
    }
}

class Magazine implements Printable {
    constructor(public name: string) { }
    print() {
        console.log(`Magazine: ${this.name}`);
    }
}

function printItems<T extends Printable>(items: T[]) {
    items.forEach(item => item.print());
}

let books: Book[] = [new Book('TypeScript in Action'), new Book('Effective TypeScript')];
let magazines: Magazine[] = [new Magazine('Tech Monthly'), new Magazine('Code World')];

printItems(books); 
printItems(magazines); 

在上面的代码中,printItems 函数是一个泛型函数,它接受一个数组,数组元素类型 T 必须是 Printable 类型或者其子类型。这里 BookMagazine 都实现了 Printable 接口,所以可以将 Book 数组和 Magazine 数组传递给 printItems 函数。

泛型交叉类型

泛型也可以与交叉类型结合。例如,我们可以定义一个泛型类型,它是两个类型的交叉类型:

type Combine<T, U> = T & U;

interface A {
    a: string;
}

interface B {
    b: number;
}

let combined: Combine<A, B> = { a: 'Hello', b: 42 }; 

在这个例子中,Combine 是一个泛型类型别名,它创建了两个类型 TU 的交叉类型。我们通过 Combine<A, B> 创建了一个新类型,这个类型同时具有 AB 的属性,然后 combined 对象符合这个交叉类型的要求。

复合类型在 React 中的应用

React 组件属性的联合类型

在 React 开发中,我们经常会使用联合类型来定义组件的属性。例如,假设我们有一个 Button 组件,它的 variant 属性可以是 'primary' 或者 'secondary'

import React from'react';

type ButtonVariant = 'primary' |'secondary';

interface ButtonProps {
    label: string;
    variant: ButtonVariant;
}

const Button: React.FC<ButtonProps> = ({ label, variant }) => {
    return <button className={`button-${variant}`}>{label}</button>;
};

export default Button; 

在上面的代码中,ButtonVariant 是一个联合类型,它限定了 Button 组件 variant 属性的取值范围。这样可以在开发过程中避免错误地传递其他值给 variant 属性。

React 组件属性的交叉类型

有时候,我们可能需要一个组件的属性同时满足多个接口的要求。例如,我们有一个 Input 组件,它既需要基本的 InputProps 接口的属性,又需要 WithErrorProps 接口的属性来处理错误显示:

import React from'react';

interface InputProps {
    type: string;
    value: string;
    onChange: (e: React.ChangeEvent<HTMLInputElement>) => void;
}

interface WithErrorProps {
    error: string | null;
}

type InputWithErrorProps = InputProps & WithErrorProps;

const InputWithError: React.FC<InputWithErrorProps> = ({ type, value, onChange, error }) => {
    return (
        <div>
            <input type={type} value={value} onChange={onChange} />
            {error && <span style={{ color:'red' }}>{error}</span>}
        </div>
    );
};

export default InputWithError; 

在这个例子中,InputWithErrorPropsInputPropsWithErrorProps 的交叉类型,InputWithError 组件的属性必须同时满足这两个接口的要求。这样可以更好地组织和管理组件的属性,提高代码的可维护性。

通过以上对 TypeScript 复合类型的构建与使用的详细介绍,希望你能对联合类型、交叉类型等复合类型在不同场景下的应用有更深入的理解,并能在实际开发中灵活运用它们来提高代码的健壮性和可维护性。无论是简单的变量定义,还是复杂的函数和组件开发,复合类型都能发挥重要作用。