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

TypeScript类型系统全面解读

2024-09-087.2k 阅读

一、TypeScript 类型系统基础

1.1 基本类型

TypeScript 支持多种基本类型,这些类型是构建复杂类型的基石。

  • 布尔类型(boolean):只有两个值 truefalse。例如:
let isDone: boolean = false;
  • 数字类型(number):在 TypeScript 中,所有数字都是浮点数。可以使用十进制、十六进制(以 0x 开头)或八进制(以 0o 开头,ES2015 引入)来表示数字。
let myNumber: number = 42;
let hexNumber: number = 0xf00d;
let octalNumber: number = 0o755;
  • 字符串类型(string):用于表示文本数据。可以使用单引号(')、双引号(")或模板字符串(${})。
let name: string = 'John';
let greeting: string = `Hello, ${name}`;
  • 空值类型(void):通常用于表示函数没有返回值。
function logMessage(message: string): void {
    console.log(message);
}
  • Null 和 Undefinednullundefined 在 TypeScript 中有自己的类型,分别为 nullundefined。默认情况下,它们是所有类型的子类型。
let n: null = null;
let u: undefined = undefined;
  • Never 类型never 类型表示那些永远不会有返回值的函数的返回类型,或者是那些根本不存在的值的类型。例如,一个抛出异常或无限循环的函数:
function throwError(message: string): never {
    throw new Error(message);
}

function infiniteLoop(): never {
    while (true) {}
}

1.2 类型注解与类型推断

  • 类型注解:是一种显式地告诉编译器某个变量或函数参数、返回值类型的方式。
let age: number;
age = 30;
  • 类型推断:TypeScript 编译器会根据变量的初始化值自动推断其类型。例如:
let num = 10; // 这里 num 被推断为 number 类型

在函数中,返回值类型也可以通过类型推断得出:

function add(a, b) {
    return a + b;
}
// 这里函数 add 的返回值被推断为 number 类型

二、复合类型

2.1 数组类型

  • 明确元素类型的数组:可以使用 类型[] 的语法来定义数组。
let numbers: number[] = [1, 2, 3];
let strings: string[] = ['a', 'b', 'c'];
  • 泛型数组类型:使用 Array<类型> 的形式,这与 类型[] 等效。
let booleanArray: Array<boolean> = [true, false];
  • 数组中元素类型不一致:可以使用联合类型来表示数组中元素类型不一致的情况。
let mixedArray: (number | string)[] = [1, 'two'];

2.2 元组类型

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let tuple: [string, number] = ['test', 42];

访问元组元素时,TypeScript 会根据定义的类型进行检查:

let value: string = tuple[0]; // 正确
let wrongValue: number = tuple[0]; // 错误,类型不匹配

2.3 对象类型

  • 对象字面量类型:用于定义对象的形状(shape),即对象包含哪些属性以及属性的类型。
let user: { name: string; age: number } = { name: 'Alice', age: 25 };
  • 可选属性:在属性名后加上 ? 表示该属性是可选的。
let userWithOptionalProp: { name: string; age?: number } = { name: 'Bob' };
  • 只读属性:在属性名前加上 readonly 表示该属性只能在对象初始化时赋值。
let readonlyUser: { readonly name: string; age: number } = { name: 'Charlie', age: 30 };
// readonlyUser.name = 'New Name'; // 错误,不能重新赋值只读属性

三、函数类型

3.1 函数声明与类型

  • 函数参数与返回值类型:在函数声明中明确参数和返回值的类型。
function addNumbers(a: number, b: number): number {
    return a + b;
}
  • 函数表达式类型:对于函数表达式,也需要明确类型。
let subtract: (a: number, b: number) => number = function (a, b) {
    return a - b;
};

3.2 可选参数与默认参数

  • 可选参数:在参数名后加上 ? 表示该参数是可选的。
function greet(name: string, greeting?: string) {
    if (greeting) {
        return `${greeting}, ${name}`;
    }
    return `Hello, ${name}`;
}
greet('John');
greet('Jane', 'Hi');
  • 默认参数:为参数提供默认值,具有默认值的参数也可以不传递。
function multiply(a: number, b: number = 1) {
    return a * b;
}
multiply(5);
multiply(5, 3);

3.3 剩余参数

使用 ... 语法来表示剩余参数,它将所有剩余的参数收集到一个数组中。

function sum(...numbers: number[]): number {
    return numbers.reduce((acc, num) => acc + num, 0);
}
sum(1, 2, 3);

四、类型别名与接口

4.1 类型别名

类型别名可以给一个类型起一个新名字,它可以是基本类型、复合类型或联合类型等。

type UserID = number;
let id: UserID = 123;

type StringOrNumber = string | number;
let value: StringOrNumber = 'test';
value = 42;

对于函数类型,也可以使用类型别名:

type MathOperation = (a: number, b: number) => number;
let add: MathOperation = function (a, b) {
    return a + b;
};

4.2 接口

接口用于定义对象的形状,与类型别名类似,但在一些方面有所不同。

interface User {
    name: string;
    age: number;
}
let user: User = { name: 'David', age: 28 };
  • 接口的继承:接口可以继承其他接口,以扩展其功能。
interface Admin extends User {
    role: string;
}
let admin: Admin = { name: 'Eve', age: 35, role: 'admin' };
  • 接口与类型别名的区别
    • 扩展方式:接口可以通过继承来扩展,类型别名可以通过联合类型来组合。例如:
interface A {
    a: number;
}
interface B extends A {
    b: string;
}

type C = { c: boolean };
type D = A & C;
- **声明合并**:接口支持声明合并,多次声明同一个接口会自动合并其成员,而类型别名不能声明合并。
interface Point {
    x: number;
}
interface Point {
    y: number;
}
let point: Point = { x: 1, y: 2 };

五、联合类型与交叉类型

5.1 联合类型

联合类型表示一个值可以是多种类型之一,使用 | 分隔不同的类型。

let value: string | number;
value = 'test';
value = 42;

在使用联合类型的值时,需要进行类型检查:

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

5.2 交叉类型

交叉类型表示一个值同时具有多种类型的特性,使用 & 连接不同的类型。

interface A {
    a: number;
}
interface B {
    b: string;
}
let ab: A & B = { a: 1, b: 'test' };

交叉类型常用于组合多个接口的功能。例如,将一个具有 name 属性的接口和一个具有 age 属性的接口组合:

interface Nameable {
    name: string;
}
interface Ageable {
    age: number;
}
let person: Nameable & Ageable = { name: 'Frank', age: 32 };

六、类型断言

类型断言是一种告诉编译器“我知道自己在做什么”的方式,用于手动指定一个值的类型。有两种语法:

  • 尖括号语法(<类型>值)
let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;
  • as 语法(值 as 类型)
let someOtherValue: any = 'this is another string';
let otherStrLength: number = (someOtherValue as string).length;

在 JSX 中,只能使用 as 语法进行类型断言。类型断言并不是类型转换,它只是告诉编译器以特定类型来处理值,并不会在运行时改变值的实际类型。

七、泛型

7.1 泛型函数

泛型函数允许我们创建可复用的函数,这些函数可以接受多种类型的参数,而不需要为每种类型都编写一个单独的函数。

function identity<T>(arg: T): T {
    return arg;
}
let result1 = identity<number>(42);
let result2 = identity<string>('hello');

这里的 <T> 是类型参数,它可以在函数中代表任何类型。调用函数时,可以显式指定类型参数,也可以让 TypeScript 进行类型推断:

let inferredResult = identity(10); // 这里 T 被推断为 number 类型

7.2 泛型接口

可以定义泛型接口,用于描述泛型函数的形状或对象的泛型属性。

interface GenericIdentityFn {
    <T>(arg: T): T;
}
let myIdentity: GenericIdentityFn = function <T>(arg: T): T {
    return arg;
};

也可以将类型参数放在接口名称之后:

interface GenericInterface<T> {
    value: T;
    getValue(): T;
}
class GenericClass<T> implements GenericInterface<T> {
    constructor(public value: T) {}
    getValue(): T {
        return this.value;
    }
}
let instance = new GenericClass<number>(42);

7.3 泛型约束

有时我们需要对泛型类型进行一些约束,以确保它具有某些属性或方法。

interface Lengthwise {
    length: number;
}
function printLength<T extends Lengthwise>(arg: T) {
    console.log(arg.length);
}
printLength('hello');
printLength([1, 2, 3]);
// printLength(10); // 错误,number 类型没有 length 属性

通过 extends 关键字,我们约束了泛型 T 必须是具有 length 属性的类型。

八、条件类型

8.1 基本条件类型

条件类型基于一个条件来选择类型,使用 T extends U? X : Y 的语法,意思是如果 TU 的子类型,则选择 X 类型,否则选择 Y 类型。

type IsString<T> = T extends string? true : false;
type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false

8.2 映射类型

映射类型允许我们基于现有的类型创建新的类型,通过对现有类型的属性进行遍历和转换。

interface User {
    name: string;
    age: number;
}
type ReadonlyUser = {
    readonly [P in keyof User]: User[P];
};
let readonlyUser: ReadonlyUser = { name: 'Grace', age: 27 };
// readonlyUser.name = 'New Name'; // 错误,只读属性不能重新赋值

这里使用 keyof User 获取 User 接口的所有属性名,P in keyof User 遍历这些属性名,然后将每个属性变为只读。

8.3 索引类型

索引类型用于通过索引访问对象类型的属性类型。

interface Person {
    name: string;
    age: number;
}
type NameType = Person['name']; // string
type AgeType = Person['age']; // number

结合条件类型和索引类型,可以实现更复杂的类型操作。例如,获取对象中特定类型属性的类型:

interface AllTypes {
    a: string;
    b: number;
    c: boolean;
}
type StringKeys<T> = {
    [P in keyof T]: T[P] extends string? P : never;
}[keyof T];
type StringKey = StringKeys<AllTypes>; // 'a'

九、类型守卫与类型缩小

9.1 类型守卫函数

类型守卫是一个返回 boolean 的函数,用于在运行时检查一个值的类型。

function isString(value: any): value is string {
    return typeof value ==='string';
}
function printValue(value: string | number) {
    if (isString(value)) {
        console.log(value.length);
    } else {
        console.log(value.toFixed(2));
    }
}

这里的 isString 函数就是一个类型守卫,value is string 语法告诉 TypeScript,在函数返回 true 时,value 的类型为 string

9.2 typeof 类型守卫

typeof 操作符在 TypeScript 中也可以用作类型守卫。

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

通过 typeof 检查,TypeScript 能够在相应的代码块中缩小 value 的类型范围。

9.3 instanceof 类型守卫

instanceof 用于检查一个对象是否是某个类的实例,也可以作为类型守卫。

class Animal {}
class Dog extends Animal {}
function handleAnimal(animal: Animal) {
    if (animal instanceof Dog) {
        console.log('This is a dog');
    } else {
        console.log('This is some other animal');
    }
}

if 块中,animal 的类型被缩小为 Dog

十、实用类型

10.1 Partial

Partial<T> 实用类型将类型 T 的所有属性变为可选。

interface User {
    name: string;
    age: number;
}
let partialUser: Partial<User> = {};
partialUser.name = 'Henry';

10.2 Required

Required<T>Partial<T> 相反,它将类型 T 的所有可选属性变为必选。

interface OptionalUser {
    name?: string;
    age?: number;
}
let requiredUser: Required<OptionalUser> = { name: 'Ivy', age: 31 };

10.3 Readonly

Readonly<T> 将类型 T 的所有属性变为只读。

interface MutableUser {
    name: string;
    age: number;
}
let readonlyUser: Readonly<MutableUser> = { name: 'Leo', age: 29 };
// readonlyUser.name = 'New Name'; // 错误,只读属性不能重新赋值

10.4 Pick

Pick<T, K> 从类型 T 中选择属性集合 K 来创建一个新类型。

interface FullUser {
    name: string;
    age: number;
    email: string;
}
type NameAndAge = Pick<FullUser, 'name' | 'age'>;
let nameAndAgeUser: NameAndAge = { name: 'Nina', age: 26 };

10.5 Omit

Omit<T, K> 从类型 T 中排除属性集合 K 来创建一个新类型。

interface AllInfo {
    name: string;
    age: number;
    address: string;
}
type WithoutAddress = Omit<AllInfo, 'address'>;
let withoutAddressUser: WithoutAddress = { name: 'Oscar', age: 33 };

通过全面了解 TypeScript 的类型系统,开发者能够编写出更健壮、可维护的代码,充分发挥 TypeScript 在大型项目中的优势。无论是基础类型、复合类型,还是复杂的泛型、条件类型等,每个部分都紧密配合,为代码的类型安全提供了有力保障。