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

TypeScript类型术语概念剖析

2023-05-083.2k 阅读

类型系统基础概念

在深入 TypeScript 类型术语之前,我们先来回顾一下类型系统的一些基础概念。类型系统是编程语言的一个重要组成部分,它负责定义值的种类以及这些值上允许执行的操作。

类型(Type)

类型是对值的一种分类,它定义了值的集合以及可以对这些值执行的操作。例如,在 TypeScript 中,number 类型表示所有的数字值,我们可以对 number 类型的值进行加、减、乘、除等算术运算。

let num: number = 10;
let result: number = num + 5;

在上述代码中,我们声明了一个 number 类型的变量 num,并对其进行加法运算。TypeScript 的类型系统会确保我们的操作在类型上是安全的。

类型检查(Type Checking)

类型检查是在编译时或运行时验证程序中值的类型是否符合预期的过程。TypeScript 主要是在编译时进行类型检查,这有助于在代码运行之前发现类型相关的错误。

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

// 正确调用
let sum1: number = addNumbers(5, 3);

// 错误调用,参数类型不匹配
// let sum2: number = addNumbers('5', 3); // 报错:Argument of type '"5"' is not assignable to parameter of type 'number'.

在上面的代码中,TypeScript 编译器会检查 addNumbers 函数的调用,确保传入的参数类型为 number。如果传入的参数类型不匹配,就会抛出编译错误。

TypeScript 类型术语详解

原始类型(Primitive Types)

TypeScript 支持与 JavaScript 相同的原始类型,这些类型代表了最基本的数据类型。

  1. 布尔类型(boolean):表示逻辑值 truefalse
let isDone: boolean = false;
  1. 数字类型(number):表示所有的数字,包括整数和浮点数。
let age: number = 25;
let pi: number = 3.14;
  1. 字符串类型(string):表示文本数据,由一系列字符组成。
let name: string = 'John Doe';
  1. 空值类型(void):通常用于表示函数没有返回值。
function logMessage(message: string): void {
    console.log(message);
}
  1. null 和 undefinednull 表示一个空值,undefined 表示变量未初始化。在严格模式下,它们是各自独立的类型。
let myNull: null = null;
let myUndefined: undefined = undefined;
  1. 任意类型(any)any 类型表示可以是任何类型的值。当我们不确定一个值的类型,或者希望绕过类型检查时,可以使用 any 类型。
let data: any = 'initial value';
data = 10; // 合法,因为 data 是 any 类型

但过度使用 any 类型会失去 TypeScript 类型检查的优势,尽量在明确类型的情况下避免使用它。

类型别名(Type Aliases)

类型别名是给类型定义一个新的名字,它可以使代码更易读和维护。通过 type 关键字来定义类型别名。

type UserId = number;
type UserName = string;

let userId: UserId = 123;
let userName: UserName = 'Alice';

类型别名也可以用于定义更复杂的类型,比如联合类型和交叉类型。

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

接口(Interfaces)

接口是一种用于定义对象形状的类型。它描述了对象应该具有哪些属性以及这些属性的类型。

interface User {
    name: string;
    age: number;
    email: string;
}

let user: User = {
    name: 'Bob',
    age: 30,
    email: 'bob@example.com'
};

接口还支持可选属性和只读属性。

interface Options {
    width?: number; // 可选属性
    readonly height: number; // 只读属性
}

let options: Options = { height: 100 };
// options.height = 200; // 报错:Cannot assign to 'height' because it is a read - only property.

接口之间还可以继承,以复用和扩展类型定义。

interface Employee extends User {
    employeeId: number;
}

let employee: Employee = {
    name: 'Eve',
    age: 28,
    email: 'eve@example.com',
    employeeId: 456
};

联合类型(Union Types)

联合类型表示一个值可以是多种类型中的一种。通过 | 符号来定义联合类型。

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

printValue('hello');
printValue(123.45);

在上述代码中,printValue 函数接受一个 stringnumber 类型的参数,并根据实际类型进行不同的操作。

交叉类型(Intersection Types)

交叉类型表示一个值同时具有多种类型的特性。通过 & 符号来定义交叉类型。

interface A {
    a: string;
}

interface B {
    b: number;
}

let ab: A & B = { a: 'test', b: 10 };

在这个例子中,ab 变量必须同时满足 AB 接口的要求。

类型断言(Type Assertion)

类型断言是告诉编译器一个值的实际类型,当我们比编译器更明确一个值的类型时可以使用它。有两种语法形式:<Type>valuevalue as Type

let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;
// 或者
let strLength2: number = (someValue as string).length;

泛型(Generics)

泛型允许我们在定义函数、接口或类时使用类型参数,这样可以使代码更加通用,适用于多种类型。

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

let result1 = identity<number>(10);
let result2 = identity<string>('hello');

在上述 identity 函数中,T 是一个类型参数,它可以代表任何类型。通过传入不同的类型参数,函数可以处理不同类型的值。

泛型还可以用于接口和类。

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

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

let myIdentity: GenericIdentityFn<number> = identity;
class GenericNumber<T> {
    zeroValue: T;
    add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = function (x, y) { return x + y; };

类型推断(Type Inference)

TypeScript 具有类型推断能力,它可以根据代码的上下文自动推断出变量的类型。

let num = 10; // 推断 num 为 number 类型

function add(a, b) {
    return a + b;
}
let sum = add(5, 3); // 推断 sum 为 number 类型

在大多数情况下,TypeScript 能够准确地推断出类型,从而减少我们显式声明类型的工作量。

索引类型(Index Types)

索引类型允许我们通过索引来访问对象的属性,并且可以在类型层面进行操作。

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

function getProperty<T, K extends keyof T>(obj: T, key: K): T[K] {
    return obj[key];
}

let person: Person = { name: 'Charlie', age: 35, email: 'charlie@example.com' };
let name = getProperty(person, 'name');

在上述代码中,keyof T 获取 T 类型的所有属性名,K extends keyof T 确保 KT 的属性名之一。T[K] 获取 T 类型中 K 属性的类型。

映射类型(Mapped Types)

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

interface User {
    name: string;
    age: number;
    email: string;
}

type ReadonlyUser = {
    readonly [P in keyof User]: User[P];
};

let readonlyUser: ReadonlyUser = {
    name: 'David',
    age: 22,
    email: 'david@example.com'
};
// readonlyUser.name = 'New Name'; // 报错:Cannot assign to 'name' because it is a read - only property.

在这个例子中,我们通过 ReadonlyUser 映射类型将 User 接口的所有属性转换为只读属性。

高级类型操作

条件类型(Conditional Types)

条件类型允许我们根据类型关系进行类型选择。通过 T extends U? X : Y 语法来定义条件类型。

type IsString<T> = T extends string? true : false;

type Result1 = IsString<string>; // true
type Result2 = IsString<number>; // false

条件类型在处理复杂的类型关系时非常有用,例如,根据一个类型是否可赋值给另一个类型来选择不同的类型。

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R? R : never;

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

type AddNumbersReturnType = ReturnType<typeof addNumbers>; // number

在上述代码中,ReturnType 条件类型根据传入的函数类型推断出其返回值类型。

推断类型(Infer Types)

在条件类型中,infer 关键字用于在条件类型的 extends 子句中推断类型。

type UnpackPromise<T> = T extends Promise<infer U>? U : T;

type Result3 = UnpackPromise<Promise<string>>; // string
type Result4 = UnpackPromise<string>; // string

UnpackPromise 类型中,infer U 推断出 Promise 类型的解析值类型。

分布式条件类型(Distributive Conditional Types)

当条件类型作用于联合类型时,会自动对联合类型的每个成员进行条件类型运算,这就是分布式条件类型。

type ToArray<T> = T extends any? T[] : never;

type Result5 = ToArray<string | number>; // string[] | number[]

在上述代码中,ToArray 类型会对 string | number 联合类型的每个成员应用 T[],得到 string[] | number[]

类型兼容性

TypeScript 的类型兼容性是基于结构子类型系统的。结构子类型系统意味着类型的兼容性是由它们的结构决定,而不是由它们的声明决定。

基本类型兼容性

  1. 数字类型兼容性number 类型兼容 number 类型,number 类型不兼容 string 类型等其他非数字类型。

  2. 布尔类型兼容性boolean 类型兼容 boolean 类型,不兼容其他类型。

对象类型兼容性

对象类型的兼容性是基于属性的兼容性。如果 A 类型的所有属性都可以在 B 类型中找到,那么 A 类型兼容 B 类型。

interface A {
    a: string;
}

interface B {
    a: string;
    b: number;
}

let a: A = { a: 'test' };
let b: B = a; // 合法,因为 A 类型兼容 B 类型

但如果 A 类型有 B 类型没有的属性,那么 A 类型不兼容 B 类型。

interface C {
    c: string;
    d: number;
}

interface D {
    c: string;
}

let c: C = { c: 'test', d: 10 };
// let d: D = c; // 报错:Type 'C' is not assignable to type 'D'. Object literal may only specify known properties, and 'd' does not exist in type 'D'.

函数类型兼容性

函数类型的兼容性主要考虑参数和返回值类型。

  1. 参数兼容性:当 A 函数的参数类型可以赋值给 B 函数的参数类型时,A 函数类型兼容 B 函数类型。
let func1 = (a: number) => console.log(a);
let func2 = (b: number, c: string) => console.log(b, c);

func2 = func1; // 合法,因为 func1 的参数类型可以赋值给 func2 的参数类型
  1. 返回值兼容性A 函数的返回值类型必须兼容 B 函数的返回值类型。
let func3 = (): number => 10;
let func4 = (): string => 'test';

// func4 = func3; // 报错:Type '() => number' is not assignable to type '() => string'. Type 'number' is not assignable to type'string'.

类型声明文件

在 TypeScript 项目中,我们经常需要使用第三方库,这些库可能没有提供 TypeScript 类型定义。这时,我们可以使用类型声明文件(.d.ts)来为这些库添加类型支持。

// jquery.d.ts
declare function $ (selector: string): any;

在上述类型声明文件中,我们声明了一个 $ 函数,它接受一个字符串参数并返回 any 类型。这样,在 TypeScript 项目中使用 $ 函数时,就可以获得一定的类型提示。

对于更复杂的库,类型声明文件可能会包含接口、类型别名等更丰富的类型定义。

// axios.d.ts
interface AxiosResponse<T = any> {
    data: T;
    status: number;
    statusText: string;
    headers: any;
    config: any;
    request: any;
}

interface AxiosError<T = any> extends Error {
    config: any;
    code: string | null;
    request: any;
    response: AxiosResponse<T> | null;
}

declare function axios<T = any>(config: any): Promise<AxiosResponse<T>>;
declare namespace axios {
    const create: (config: any) => (config: any) => Promise<AxiosResponse<any>>;
}

通过这样的类型声明文件,我们可以在 TypeScript 项目中更好地使用 axios 库,并获得准确的类型检查和代码提示。

在实际项目中,很多流行的第三方库都有官方或社区维护的类型声明文件,可以通过 @types 组织进行安装。例如,安装 @types/jquery 就可以为 jquery 库添加类型支持。

常见类型错误及解决方法

  1. 类型不匹配错误:当我们试图将一个类型的值赋值给另一个不兼容的类型变量时,会出现类型不匹配错误。
let num: number = '10'; // 报错:Type '"10"' is not assignable to type 'number'.

解决方法是确保赋值的类型与变量声明的类型一致。如果需要进行类型转换,可以使用类型转换函数,如 parseIntparseFloat

let num: number = parseInt('10');
  1. 属性不存在错误:当我们访问对象上不存在的属性时,会出现属性不存在错误。
interface Person {
    name: string;
}

let person: Person = { name: 'Ella' };
// console.log(person.age); // 报错:Property 'age' does not exist on type 'Person'.

解决方法是确保对象的类型定义包含我们要访问的属性,或者在访问属性之前进行检查。

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

let person: Person = { name: 'Ella' };
if ('age' in person) {
    console.log(person.age);
}
  1. 函数参数类型不匹配错误:当我们调用函数时传入的参数类型与函数定义的参数类型不匹配时,会出现函数参数类型不匹配错误。
function greet(name: string) {
    console.log('Hello,'+ name);
}

// greet(123); // 报错:Argument of type '123' is not assignable to parameter of type'string'.

解决方法是确保传入函数的参数类型与函数定义的参数类型一致。

通过深入理解 TypeScript 的类型术语概念,我们能够编写出更健壮、可维护的 TypeScript 代码,充分发挥 TypeScript 类型系统的优势。无论是简单的变量声明,还是复杂的泛型和条件类型应用,准确把握类型概念都是关键。在实际项目中,不断积累经验,熟练运用这些概念,将有助于提升开发效率和代码质量。