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

TypeScript基本类型与复合类型:深入解析类型系统

2021-03-146.5k 阅读

基本类型

布尔类型(boolean)

布尔类型是最基础的数据类型之一,在 TypeScript 中,它只有两个值:truefalse。这与 JavaScript 中的布尔类型概念一致。

let isDone: boolean = false;
function toggle(value: boolean): boolean {
    return!value;
}
let newIsDone = toggle(isDone);

在上述代码中,我们声明了一个 isDone 变量并指定其类型为 boolean,同时定义了一个 toggle 函数,它接受一个布尔类型参数并返回一个布尔类型值。

数字类型(number)

TypeScript 中的数字类型与 JavaScript 一样,所有数字都是浮点型。可以表示整数、小数等。

let age: number = 25;
let pi: number = 3.14;
let binaryNumber: number = 0b1010; // 二进制表示
let octalNumber: number = 0o755; // 八进制表示
let hexadecimalNumber: number = 0xff; // 十六进制表示

这里我们声明了不同形式的数字变量,TypeScript 能够很好地识别并处理这些数字表示方式。

字符串类型(string)

字符串用于表示文本数据。TypeScript 支持用单引号(')、双引号(")或模板字面量(```)来定义字符串。

let name1: string = 'John';
let name2: string = "Doe";
let message: string = `My name is ${name1} ${name2}`;

使用模板字面量可以方便地进行字符串插值,将变量嵌入到字符串中。

空值类型(void)

void 类型通常用于表示函数没有返回值。

function logMessage(message: string): void {
    console.log(message);
}

上述 logMessage 函数只执行打印操作,不返回任何值,所以其返回类型为 void

null 和 undefined

在 TypeScript 中,nullundefined 有各自的类型,分别为 nullundefined。它们是所有类型的子类型。默认情况下,nullundefined 可以赋值给任何类型的变量。

let num: number | null | undefined = null;
num = 10;
num = undefined;

这里我们定义了一个 num 变量,它可以接受 numbernullundefined 类型的值。

never 类型

never 类型表示那些永不存在的值的类型。例如,一个总是抛出异常或根本不会有返回值的函数。

function throwError(message: string): never {
    throw new Error(message);
}
function infiniteLoop(): never {
    while (true) {}
}

throwError 函数总是抛出异常,而 infiniteLoop 函数会陷入无限循环,它们的返回类型都是 never

任意类型(any)

any 类型表示任意类型的数据。当你不确定一个值的类型时,可以使用 any。但过度使用 any 会削弱 TypeScript 的类型检查优势。

let value: any = 'hello';
value = 123;
function printValue(val: any) {
    console.log(val);
}
printValue(value);

在这个例子中,value 变量可以被赋予字符串或数字类型的值,printValue 函数也可以接受任意类型的参数。

字面量类型

字面量类型允许你指定变量只能是特定的字面量值。例如,你可以定义一个变量只能是 'success''error'

let status: 'loading' | 'completed' | 'failed' = 'loading';
function updateStatus(currentStatus: 'loading' | 'completed' | 'failed'): 'loading' | 'completed' | 'failed' {
    if (currentStatus === 'loading') {
        return 'completed';
    }
    return 'failed';
}
let newStatus = updateStatus(status);

这里 status 变量只能被赋值为 'loading''completed''failed' 中的一个,updateStatus 函数也只接受这三种字面量类型之一的参数,并返回同样类型的值。

复合类型

数组类型

TypeScript 提供了几种方式来定义数组类型。最常见的是在元素类型后面加上 []

let numbers: number[] = [1, 2, 3];
let strings: string[] = ['a', 'b', 'c'];

你还可以使用泛型数组类型 Array<Type> 来定义数组。

let booleans: Array<boolean> = [true, false];

如果数组中的元素类型不一致,可以使用联合类型。

let mixedArray: (number | string)[] = [1, 'two'];

此外,还可以定义多维数组。

let matrix: number[][] = [[1, 2], [3, 4]];

元组类型

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

let user: [string, number] = ['John', 25];
// 访问元组元素
let name: string = user[0];
let age: number = user[1];

元组的长度是固定的,当访问越界的元素时会有类型检查错误。

// 报错:元组类型 '[string, number]' 中缺少属性 '2'。
let unknownProp = user[2]; 

枚举类型(enum)

枚举类型是用于定义一组命名常量的方式。它可以让代码更具可读性和可维护性。

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
let myDirection: Direction = Direction.Up;

在上述代码中,Direction 枚举定义了四个方向常量,默认情况下,Up 的值为 1,后续成员的值会自动递增。你也可以手动指定每个成员的值。

enum Color {
    Red = 0,
    Green = 10,
    Blue = 20
}

枚举类型还支持反向映射,即可以通过值获取对应的名称。

enum Status {
    Active = 1,
    Inactive
}
let statusName: string = Status[1]; // "Active"

接口类型

接口是 TypeScript 中非常重要的概念,用于定义对象的形状(结构)。

interface User {
    name: string;
    age: number;
    email: string;
}
let userInfo: User = {
    name: 'Alice',
    age: 30,
    email: 'alice@example.com'
};

接口可以定义可选属性,只需在属性名后面加上 ?

interface Product {
    name: string;
    price: number;
    description?: string;
}
let product: Product = {
    name: 'Book',
    price: 20
};

只读属性可以使用 readonly 关键字定义,一旦对象被创建,这些属性的值就不能被修改。

interface Point {
    readonly x: number;
    readonly y: number;
}
let point: Point = { x: 10, y: 20 };
// 报错:无法分配到 "x" ,因为它是只读属性。
point.x = 30; 

接口还支持继承,通过 extends 关键字实现。

interface Animal {
    name: string;
}
interface Dog extends Animal {
    breed: string;
}
let myDog: Dog = { name: 'Buddy', breed: 'Golden Retriever' };

类型别名

类型别名可以给一个类型起一个新名字。它与接口有些类似,但语法略有不同,并且类型别名可以用于非对象类型。

type Gender ='male' | 'female';
let userGender: Gender ='male';
type StringOrNumber = string | number;
let value: StringOrNumber = 'hello';
value = 123;

对于函数类型,也可以使用类型别名来定义。

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

类型别名也支持泛型。

type Pair<T> = [T, T];
let numberPair: Pair<number> = [1, 1];
let stringPair: Pair<string> = ['a', 'a'];

联合类型与交叉类型

联合类型表示一个值可以是多种类型之一。

let data: string | number;
data = 'hello';
data = 123;

在使用联合类型的值时,需要进行类型保护,以确保在特定的代码块中,值的类型是明确的。

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

交叉类型则是将多个类型合并为一个类型,这个类型包含了所有类型的特性。

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

交叉类型常用于将多个接口的属性合并到一个类型中。

泛型

泛型是 TypeScript 中非常强大的特性,它允许你在定义函数、接口或类时不指定具体的类型,而是在使用时再确定类型。

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

在上述 identity 函数中,<T> 是类型参数,T 可以代表任何类型。在调用函数时,通过 <number><string> 来指定 T 的具体类型。 泛型也可以用于接口和类。

interface GenericIdentityFn<T> {
    (arg: T): T;
}
let myIdentity: GenericIdentityFn<number> = function (arg) {
    return arg;
};
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;
};

泛型还支持约束,通过 extends 关键字实现。

interface Lengthwise {
    length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
    console.log(arg.length);
    return arg;
}
let result3 = loggingIdentity('hello');
// 报错:类型 "number" 缺少属性 "length",但类型 "Lengthwise" 需要该属性。
let result4 = loggingIdentity(10); 

这里 T 被约束为必须包含 length 属性的类型,这样在函数内部就可以安全地访问 length 属性。

条件类型

条件类型允许根据类型关系来选择不同的类型。它使用 T extends U? X : Y 的语法。

type IsString<T> = T extends string? true : false;
type StringCheck = IsString<string>; // true
type NumberCheck = IsString<number>; // false

条件类型常用于实现复杂的类型转换逻辑。例如,Exclude 类型可以从一个类型中排除另一个类型的成员。

type Exclude<T, U> = T extends U? never : T;
type T1 = Exclude<'a' | 'b' | 'c', 'a' | 'c'>; // 'b'

Extract 类型则相反,用于提取两个类型的共同成员。

type Extract<T, U> = T extends U? T : never;
type T2 = Extract<'a' | 'b' | 'c', 'a' | 'c'>; // 'a' | 'c'

条件类型还可以与泛型结合使用,实现更加灵活的类型处理。

type ReturnType<T extends (...args: any[]) => any> = T extends (...args: any[]) => infer R? R : any;
function add(a: number, b: number): number {
    return a + b;
}
type AddReturnType = ReturnType<typeof add>; // number

在这个例子中,ReturnType 类型通过条件类型和 infer 关键字来推断函数的返回类型。

映射类型

映射类型允许以一种类型为基础,通过对其属性进行变换来创建新的类型。

interface User {
    name: string;
    age: number;
    email: string;
}
type ReadonlyUser = {
    readonly [P in keyof User]: User[P];
};
let readonlyUser: ReadonlyUser = {
    name: 'Bob',
    age: 28,
    email: 'bob@example.com'
};
// 报错:无法分配到 "name" ,因为它是只读属性。
readonlyUser.name = 'Tom'; 

在上述代码中,ReadonlyUser 类型通过 keyof User 获取 User 接口的所有属性名,然后使用 infer 关键字为每个属性添加 readonly 修饰符。 还可以对属性进行其他变换,例如将所有属性变为可选。

type PartialUser = {
    [P in keyof User]?: User[P];
};
let partialUser: PartialUser = {
    name: 'Eve'
};

映射类型在处理对象类型的各种变换时非常有用,能够大大提高代码的可维护性和灵活性。

模板字面量类型

模板字面量类型是 TypeScript 4.1 引入的新特性,它允许通过模板字面量语法来创建新的类型。

type EmailDomain = 'gmail.com' | 'yahoo.com' | 'hotmail.com';
type EmailAddress<Username extends string, Domain extends EmailDomain> = `${Username}@${Domain}`;
type JohnEmail = EmailAddress<'john', 'gmail.com'>; // "john@gmail.com"

模板字面量类型可以与联合类型结合使用,生成一系列相关的类型。

type Direction = 'up' | 'down' | 'left' | 'right';
type MoveCommand = `move_${Direction}`;
type MoveUp = MoveCommand; // "move_up" | "move_down" | "move_left" | "move_right"

这种类型在定义特定格式的字符串类型或生成相关的枚举风格类型时非常方便。

通过深入理解 TypeScript 的基本类型和复合类型,开发者能够更加准确地定义和使用数据,充分发挥 TypeScript 类型系统的优势,编写出更健壮、可维护的前端代码。无论是简单的变量声明,还是复杂的泛型、条件类型和映射类型的应用,都能在 TypeScript 的类型系统中找到合适的解决方案。在实际项目中,合理运用这些类型知识,可以有效地减少运行时错误,提高代码质量和开发效率。