1. 引言

在上一篇学习笔记中,我们介绍了 TypeScript 的基础知识,包括类型注解、接口、类和函数。本篇将深入探讨 TypeScript 的类型系统,学习如何使用高级类型、泛型和类型推断等特性,以便在复杂应用场景中更好地利用 TypeScript。

2. 高级类型

2.1 交叉类型

交叉类型(Intersection Types)用于将多个类型合并为一个类型,它表示一个对象可以同时具有这些类型的所有成员。

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

interface Employee {
    employeeId: number;
}

type PersonEmployee = Person & Employee;

let personEmployee: PersonEmployee = {
    name: "John",
    age: 30,
    employeeId: 12345
};
2.2 联合类型

联合类型(Union Types)表示一个值可以是几种类型之一。使用竖线 (|) 分隔不同的类型。

let value: string | number;
value = "Hello";
value = 42;

function format(value: string | number): string {
    if (typeof value === "string") {
        return value.toUpperCase();
    }
    return value.toFixed(2);
}
2.3 类型别名

类型别名(Type Aliases)用于为类型创建一个新名字,可以是基础类型、联合类型、交叉类型甚至是对象类型。

type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;

function getName(n: NameOrResolver): Name {
    if (typeof n === "string") {
        return n;
    }
    return n();
}

3. 泛型

泛型(Generics)允许我们在定义函数、接口或类时不预先指定具体的类型,而是在使用时再指定类型,从而提高代码的通用性和可重用性。

3.1 泛型函数

泛型函数可以在函数名后面添加一个尖括号来定义泛型参数。

function identity<T>(arg: T): T {
    return arg;
}

let output1 = identity<string>("myString");
let output2 = identity<number>(100);
3.2 泛型接口

接口也可以使用泛型来定义。

interface GenericIdentityFn<T> {
    (arg: T): T;
}

function identity<T>(arg: T): T {
    return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
3.3 泛型类

类也可以使用泛型来定义。

class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = (x, y) => x + y;
3.4 泛型约束

有时候我们希望限制泛型的类型,这时候可以使用泛型约束。

interface Lengthwise {
    length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}

// 正确
loggingIdentity({ length: 10, value: 3 });

// 错误,类型没有 length 属性
// loggingIdentity(3);

4. 类型推断

TypeScript 会根据代码的上下文自动推断类型,从而减少显式的类型注解。

let x = 3; // x 被推断为 number 类型

function add(a: number, b: number) {
    return a + b; // 返回值被推断为 number 类型
}

let result = add(2, 5); // result 被推断为 number 类型

5. 类型守卫

类型守卫(Type Guards)用于在运行时检查类型,从而在代码中进行类型保护。

function isNumber(x: any): x is number {
    return typeof x === "number";
}

function padLeft(value: string, padding: string | number) {
    if (isNumber(padding)) {
        return Array(padding + 1).join(" ") + value;
    }
    if (typeof padding === "string") {
        return padding + value;
    }
    throw new Error(`Expected string or number, got '${typeof padding}'.`);
}

6. 映射类型

映射类型(Mapped Types)用于根据已有类型创建新的类型。它们经常与类型别名和索引签名一起使用。

type Readonly<T> = {
    readonly [P in keyof T]: T[P];
};

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

let readonlyPerson: Readonly<Person> = {
    name: "John",
    age: 30
};

// readonlyPerson.age = 31; // Error: Cannot assign to 'age' because it is a read-only property.

7. 实用工具类型

TypeScript 提供了一些实用工具类型,用于处理类型操作。

  • Partial<T>:将类型 T 的所有属性设为可选。
  • Required<T>:将类型 T 的所有属性设为必选。
  • Readonly<T>:将类型 T 的所有属性设为只读。
  • Pick<T, K>:从类型 T 中选择部分属性构成新的类型。
  • Omit<T, K>:从类型 T 中排除部分属性构成新的类型。
interface Person {
    name: string;
    age: number;
    address?: string;
}

let partialPerson: Partial<Person> = {
    name: "John"
};

let requiredPerson: Required<Person> = {
    name: "John",
    age: 30,
    address: "123 Main St"
};

let readonlyPerson: Readonly<Person> = {
    name: "John",
    age: 30
};

// readonlyPerson.age = 31; // Error

let pickedPerson: Pick<Person, "name" | "age"> = {
    name: "John",
    age: 30
};

let omittedPerson: Omit<Person, "address"> = {
    name: "John",
    age: 30
};

8. 总结

在本篇学习笔记中,我们深入探讨了 TypeScript 的高级类型系统,包括交叉类型、联合类型、类型别名、泛型、类型推断、类型守卫、映射类型和实用工具类型。通过这些高级特性,我们可以编写出更强大、灵活和可维护的代码。

下一篇学习笔记将介绍 TypeScript 中的模块系统和命名空间,以及如何与 JavaScript 进行互操作。希望你能继续关注本系列的学习笔记,进一步提升 TypeScript 编程技能。