TypeScript的高级类型:深入理解泛型、联合类型、交叉类型和类型守卫在大型项目中的应用。

TypeScript 高级类型:大型项目中的应用

各位同学,大家好。今天我们来深入探讨 TypeScript 的高级类型,重点关注泛型、联合类型、交叉类型以及类型守卫,并结合大型项目中的实际应用场景进行讲解。这些高级特性是编写类型安全、可维护和可扩展的 TypeScript 代码的关键。

1. 泛型 (Generics)

泛型允许我们编写可以处理多种类型的组件,而无需为每种类型编写单独的代码。它提供了一种参数化类型的方式,使得组件可以根据传入的类型参数进行调整。

1.1 泛型的基本概念

想象一下,我们需要一个函数,它可以接受任何类型的数组,并返回数组的第一个元素。如果没有泛型,我们可能会使用 any 类型,但这会丧失类型安全性。

function firstElement(arr: any[]): any {
  return arr[0];
}

const numArr = [1, 2, 3];
const strArr = ["a", "b", "c"];

const firstNum = firstElement(numArr); // firstNum 的类型是 any
const firstStr = firstElement(strArr); // firstStr 的类型是 any

console.log(firstNum.toFixed(2)); // 编译通过,但运行时错误!

上面的代码在编译时不会报错,因为 firstNumfirstStr 的类型都是 any,TypeScript 不会对 any 类型进行类型检查。但是,在运行时,firstNum.toFixed(2) 会抛出错误,因为数字类型没有 toFixed 方法。

使用泛型可以解决这个问题:

function firstElement<T>(arr: T[]): T | undefined {
  return arr[0];
}

const numArr = [1, 2, 3];
const strArr = ["a", "b", "c"];

const firstNum = firstElement(numArr); // firstNum 的类型是 number | undefined
const firstStr = firstElement(strArr); // firstStr 的类型是 string | undefined

// console.log(firstNum.toFixed(2)); // 编译时报错!

现在,firstNumfirstStr 的类型分别是 number | undefinedstring | undefined。如果尝试对 firstNum 调用 toFixed 方法,TypeScript 会在编译时报错,因为 number | undefined 类型没有 toFixed 方法。

1.2 泛型类型

除了函数,我们也可以定义泛型接口、泛型类和泛型类型别名。

泛型接口:

interface Result<T, E> {
  data: T | null;
  error: E | null;
}

const successResult: Result<number, string> = {
  data: 123,
  error: null,
};

const errorResult: Result<number, string> = {
  data: null,
  error: "Something went wrong",
};

泛型类:

class DataStore<T> {
  private data: T[] = [];

  add(item: T) {
    this.data.push(item);
  }

  get(index: number): T | undefined {
    return this.data[index];
  }
}

const numberStore = new DataStore<number>();
numberStore.add(1);
numberStore.add(2);
const firstNumber = numberStore.get(0); // firstNumber 的类型是 number | undefined

const stringStore = new DataStore<string>();
stringStore.add("a");
stringStore.add("b");
const firstString = stringStore.get(0); // firstString 的类型是 string | undefined

泛型类型别名:

type StringArray<T> = Array<T>;

const names: StringArray<string> = ["Alice", "Bob", "Charlie"];
const ages: StringArray<number> = [20, 25, 30];

1.3 泛型约束 (Generic Constraints)

有时,我们希望限制泛型类型参数的范围。例如,我们可能希望确保泛型类型参数具有某个特定的属性或方法。 这时可以使用 extends 关键字来约束泛型类型。

interface Lengthwise {
  length: number;
}

function logLength<T extends Lengthwise>(obj: T): void {
  console.log(obj.length);
}

logLength("hello"); // OK
logLength([1, 2, 3]); // OK
logLength({ length: 10, value: "test" }); // OK

// logLength(123); // 编译时报错,因为 number 类型没有 length 属性

在这个例子中,T extends Lengthwise 表示 T 必须是 Lengthwise 类型或其子类型,也就是说,T 必须具有 length 属性。

1.4 默认类型参数

我们可以为泛型类型参数指定默认值。

function createArray<T = string>(length: number, value: T): T[] {
  const result: T[] = [];
  for (let i = 0; i < length; i++) {
    result.push(value);
  }
  return result;
}

const stringArray = createArray(3, "hello"); // stringArray 的类型是 string[]
const numberArray = createArray<number>(3, 123); // numberArray 的类型是 number[]
const defaultArray = createArray(3, 123); // defaultArray的类型是 string[],因为没有显式指定类型参数,使用了默认值

1.5 在大型项目中的应用

  • React 组件: 泛型可以用于创建类型安全的 React 组件,例如:

    interface Props<T> {
      data: T;
      renderItem: (item: T) => React.ReactNode;
    }
    
    function List<T>(props: Props<T>) {
      return (
        <ul>
          {props.data.map((item, index) => (
            <li key={index}>{props.renderItem(item)}</li>
          ))}
        </ul>
      );
    }
    
    interface User {
      id: number;
      name: string;
    }
    
    const users: User[] = [{ id: 1, name: "Alice" }, { id: 2, name: "Bob" }];
    
    function UserItem(user: User) {
      return <div>{user.name}</div>;
    }
    
    <List<User> data={users} renderItem={UserItem} />;
  • Redux actions and reducers: 泛型可以用于定义类型安全的 Redux actions 和 reducers。

  • API 客户端: 泛型可以用于创建类型安全的 API 客户端,可以根据不同的 API 响应类型进行调整。

2. 联合类型 (Union Types)

联合类型表示一个变量可以具有多种类型中的一种。 使用 | 符号分隔不同的类型。

2.1 联合类型的基本概念

type StringOrNumber = string | number;

let value: StringOrNumber;

value = "hello"; // OK
value = 123; // OK
// value = true; // 编译时报错,因为 boolean 类型不在 StringOrNumber 中

2.2 联合类型和字面量类型结合

联合类型经常与字面量类型结合使用,以创建更精确的类型。

type Direction = "north" | "east" | "south" | "west";

function move(direction: Direction) {
  console.log(`Moving ${direction}`);
}

move("north"); // OK
// move("up"); // 编译时报错,因为 "up" 不是 Direction 类型

2.3 联合类型的类型收窄

当我们使用联合类型时,TypeScript 需要根据上下文推断变量的实际类型。 这个过程称为类型收窄 (Type Narrowing)。

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // 在这个分支中,value 的类型是 string
  } else {
    console.log(value.toFixed(2)); // 在这个分支中,value 的类型是 number
  }
}

printValue("hello");
printValue(123.456);

2.4 在大型项目中的应用

  • 处理 API 响应: API 响应可能返回不同类型的错误,联合类型可以用于表示这些不同的错误类型。

    interface SuccessResponse {
      status: "success";
      data: any;
    }
    
    interface ErrorResponse {
      status: "error";
      message: string;
    }
    
    type ApiResponse = SuccessResponse | ErrorResponse;
    
    function handleResponse(response: ApiResponse) {
      if (response.status === "success") {
        console.log("Data:", response.data);
      } else {
        console.error("Error:", response.message);
      }
    }
  • React 组件的 props: 联合类型可以用于定义可以接受不同类型值的 React 组件 props。

    interface Props {
      value: string | number;
      onChange: (newValue: string | number) => void;
    }
    
    function Input(props: Props) {
      const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
        const newValue = event.target.value;
        props.onChange(newValue); // 注意:你需要根据实际情况进行类型转换
      };
    
      return <input type="text" value={props.value} onChange={handleChange} />;
    }

3. 交叉类型 (Intersection Types)

交叉类型将多个类型合并为一个类型。 生成的类型拥有所有类型的成员。 使用 & 符号分隔不同的类型。

3.1 交叉类型的基本概念

interface Colorful {
  color: string;
}

interface Circle {
  radius: number;
}

type ColorfulCircle = Colorful & Circle;

const circle: ColorfulCircle = {
  color: "red",
  radius: 10,
};

3.2 交叉类型和联合类型的区别

  • 联合类型: 表示一个变量可以是多种类型中的 一种
  • 交叉类型: 表示一个变量必须同时满足 所有 类型的要求。

3.3 在大型项目中的应用

  • 组合多个接口: 交叉类型可以用于组合多个接口,创建更复杂的类型。 例如,在 React 中,我们可以使用交叉类型来组合组件的 props 和 state。

    interface Props {
      name: string;
    }
    
    interface State {
      age: number;
    }
    
    type MyComponentProps = Props & State;
    
    class MyComponent extends React.Component<MyComponentProps> {
      render() {
        return (
          <div>
            {this.props.name} is {this.props.age} years old.
          </div>
        );
      }
    }
  • Mixin 模式: 交叉类型可以用于实现 Mixin 模式,允许我们将多个类的功能组合到一个类中。

    function applyMixins(derivedCtor: any, baseCtors: any[]) {
      baseCtors.forEach((baseCtor) => {
        Object.getOwnPropertyNames(baseCtor.prototype).forEach((name) => {
          Object.defineProperty(
            derivedCtor.prototype,
            name,
            Object.getOwnPropertyDescriptor(baseCtor.prototype, name) ||
              Object.create(null)
          );
        });
      });
    }
    
    class CanFly {
      fly() {
        console.log("I can fly!");
      }
    }
    
    class CanSwim {
      swim() {
        console.log("I can swim!");
      }
    }
    
    class Bird implements CanFly, CanSwim {
      fly: () => void;
      swim: () => void;
      constructor() {
          // Empty Constructor
      }
    }
    
    applyMixins(Bird, [CanFly, CanSwim]);
    
    const bird = new Bird();
    bird.fly();
    bird.swim();

4. 类型守卫 (Type Guards)

类型守卫是一种在运行时检查变量类型,并缩小其类型范围的技术。 TypeScript 可以根据类型守卫的结果,在不同的代码分支中推断出更精确的类型。

4.1 typeof 类型守卫

typeof 操作符可以用于检查变量的基本类型 (string, number, boolean, symbol, bigint, object, function, undefined)。

function printValue(value: string | number) {
  if (typeof value === "string") {
    console.log(value.toUpperCase()); // value 的类型是 string
  } else {
    console.log(value.toFixed(2)); // value 的类型是 number
  }
}

4.2 instanceof 类型守卫

instanceof 操作符可以用于检查一个对象是否是某个类的实例。

class Animal {
  name: string;
  constructor(name: string) {
    this.name = name;
  }
}

class Dog extends Animal {
  bark() {
    console.log("Woof!");
  }
}

function makeSound(animal: Animal) {
  if (animal instanceof Dog) {
    animal.bark(); // animal 的类型是 Dog
  } else {
    console.log("Generic animal sound");
  }
}

const dog = new Dog("Buddy");
makeSound(dog);

4.3 自定义类型守卫

我们可以使用 is 关键字定义自定义类型守卫函数。

interface Bird {
  fly: () => void;
  layEggs: () => void;
}

interface Fish {
  swim: () => void;
  layEggs: () => void;
}

function isBird(animal: Bird | Fish): animal is Bird {
  return (animal as Bird).fly !== undefined;
}

function doSomething(animal: Bird | Fish) {
  if (isBird(animal)) {
    animal.fly(); // animal 的类型是 Bird
  } else {
    animal.swim(); // animal 的类型是 Fish
  }
}

在这个例子中,isBird 函数是一个自定义类型守卫。 它接受一个 Bird | Fish 类型的参数,并返回一个 animal is Bird 类型的结果。 如果 isBird 函数返回 true,则 TypeScript 会将 animal 的类型收窄为 Bird

4.4 in 操作符类型守卫

in 操作符可以用于检查一个对象是否具有某个属性。

interface Square {
  kind: "square";
  size: number;
}

interface Circle {
  kind: "circle";
  radius: number;
}

type Shape = Square | Circle;

function calculateArea(shape: Shape) {
  if ("radius" in shape) {
    return Math.PI * shape.radius * shape.radius; // shape 的类型是 Circle
  } else {
    return shape.size * shape.size; // shape 的类型是 Square
  }
}

4.5 在大型项目中的应用

  • 处理不同类型的 API 响应: 类型守卫可以用于处理不同类型的 API 响应,并确保代码的类型安全。

  • 处理 React 组件的 props: 类型守卫可以用于处理不同类型的 React 组件 props,并根据 props 的类型渲染不同的内容。

    interface PropsA {
      type: "A";
      valueA: string;
    }
    
    interface PropsB {
      type: "B";
      valueB: number;
    }
    
    type Props = PropsA | PropsB;
    
    function MyComponent(props: Props) {
      if (props.type === "A") {
        return <div>{props.valueA}</div>; // props 的类型是 PropsA
      } else {
        return <div>{props.valueB}</div>; // props 的类型是 PropsB
      }
    }

5. 类型推断增强代码可维护性

使用泛型、联合类型、交叉类型和类型守卫可以帮助我们编写更类型安全、可维护和可扩展的 TypeScript 代码。 这些高级类型特性可以提高代码的可读性,减少运行时错误,并简化大型项目的开发和维护。

特性 优点 适用场景
泛型 代码复用,类型安全,避免重复代码 处理多种类型的数据,例如集合、算法等
联合类型 表示一个变量可以具有多种类型,灵活,处理异构数据 API 响应,React 组件 props,处理不同类型的错误
交叉类型 将多个类型合并为一个类型,组合多个接口,实现 Mixin 模式 组合多个接口,创建更复杂的类型,例如 React 组件的 props 和 state
类型守卫 在运行时检查变量类型,缩小其类型范围,确保类型安全,提高代码可读性 处理不同类型的 API 响应,React 组件 props,处理联合类型

掌握这些高级类型特性,并灵活运用到实际项目中,可以让我们编写出更加健壮和可维护的 TypeScript 代码,从而提高开发效率和代码质量。

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注