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

Typescript中的数据类型

2024-02-196.4k 阅读

基础数据类型

在 TypeScript 中,基础数据类型是构成复杂数据结构的基石。这些数据类型直接由语言提供,开发人员可以在代码中直接使用它们来存储简单的值。

布尔类型(boolean)

布尔类型只有两个取值:truefalse。它常用于逻辑判断,比如在条件语句 if 中,用来决定代码的执行路径。

let isDone: boolean = false;
if (isDone) {
    console.log('任务已完成');
} else {
    console.log('任务未完成');
}

这里我们声明了一个名为 isDone 的布尔变量,并初始化为 false。然后在 if - else 语句中,根据 isDone 的值输出不同的信息。

数字类型(number)

TypeScript 中的数字类型用于表示整数和浮点数。与 JavaScript 类似,TypeScript 没有专门的整数类型,所有数字都是以 64 位双精度浮点格式存储。

let myNumber: number = 42;
let pi: number = 3.14159;
let binaryNumber: number = 0b1010; // 二进制表示,对应十进制的10
let octalNumber: number = 0o755; // 八进制表示,对应十进制的493
let hexadecimalNumber: number = 0x1F; // 十六进制表示,对应十进制的31

以上代码展示了不同进制数字的表示方法,在 TypeScript 中都统一用 number 类型表示。

字符串类型(string)

字符串类型用于表示文本数据,它由一系列字符组成,用单引号 '、双引号 " 或模板字符串 `` ` 包裹。

let myString1: string = 'Hello, world!';
let myString2: string = "这也是一个字符串";
let name: string = '张三';
let greeting: string = `你好,${name}`; // 模板字符串,可以嵌入变量

模板字符串允许我们在字符串中嵌入表达式,通过 ${} 语法,这在拼接复杂字符串时非常方便。

空值类型(void)

void 类型表示没有任何值,通常用于函数返回值类型。当一个函数不返回任何值时,它的返回值类型就是 void

function printMessage(): void {
    console.log('这是一条消息');
}
let result: void = printMessage(); // result 只能被赋值为 undefined 或者没有值

这里定义了一个 printMessage 函数,它不返回任何值,所以返回值类型为 void。变量 result 被声明为 void 类型,它只能被赋值为 undefined(在严格模式下,void 类型的值只能是 undefined) 或者不赋值。

null 和 undefined

在 TypeScript 中,nullundefined 都有自己的类型,分别为 nullundefined。它们通常用来表示值的缺失或未初始化状态。

let myNull: null = null;
let myUndefined: undefined = undefined;

默认情况下,nullundefined 是所有类型的子类型,也就是说可以把 nullundefined 赋值给其他类型的变量。

let num: number = null; // 这样的赋值在默认情况下是允许的
let str: string = undefined; // 同样,这样的赋值在默认情况下也被允许

但是,当开启 strictNullChecks 编译选项时,nullundefined 只能赋值给 void 和它们各自对应的类型。

// 开启 strictNullChecks 后
let num: number;
num = null; // 报错:不能将类型“null”分配给类型“number”

never 类型

never 类型表示永远不会出现的值的类型。例如,一个抛出异常或根本不会有返回值的函数,其返回值类型就是 never

function throwError(message: string): never {
    throw new Error(message);
}
function infiniteLoop(): never {
    while (true) {
        // 无限循环,永远不会结束
    }
}

throwError 函数总是抛出异常,所以它的返回值类型是 neverinfiniteLoop 函数进入无限循环,永远不会返回,其返回值类型也是 never

复杂数据类型

除了基础数据类型,TypeScript 还提供了多种复杂数据类型,这些类型可以用来表示更结构化的数据。

数组类型

数组是一种有序的数据集合,可以存储多个相同类型的值。在 TypeScript 中有两种方式来定义数组类型。

类型 + 方括号表示法

let numbers: number[] = [1, 2, 3, 4, 5];
let strings: string[] = ['apple', 'banana', 'cherry'];

这里 numbers 是一个 number 类型的数组,strings 是一个 string 类型的数组。

泛型数组表示法

let anyArray: Array<number> = [1, 2, 3];
let anotherArray: Array<string> = ['a', 'b', 'c'];

Array<T> 是泛型表示法,其中 T 是数组元素的类型。这两种方式本质上是等价的,开发人员可以根据个人喜好选择使用。

元组类型

元组类型允许我们定义一个固定长度的数组,并且每个位置的元素类型可以不同。

let user: [string, number] = ['张三', 25];
let point: [number, number] = [10, 20];

user 元组中,第一个元素是 string 类型,第二个元素是 number 类型。元组类型非常有用,比如当你需要返回多个不同类型的值时,元组可以提供一种类型安全的方式。

访问元组元素时,需要按照定义的类型来访问。

let name: string = user[0];
let age: number = user[1];

如果访问超出元组长度的索引,TypeScript 会报错。

枚举类型

枚举类型是一种为一组命名常量提供更友好方式的类型。它允许我们定义一个命名的常量集合。

数字枚举

enum Color {
    Red,
    Green,
    Blue
}
let myColor: Color = Color.Green;
console.log(myColor); // 输出 1

在数字枚举中,如果没有显式赋值,枚举成员会从 0 开始自动递增。这里 Color.Red 的值为 0Color.Green 的值为 1Color.Blue 的值为 2

字符串枚举

enum Direction {
    Up = 'UP',
    Down = 'DOWN',
    Left = 'LEFT',
    Right = 'RIGHT'
}
let myDirection: Direction = Direction.Right;
console.log(myDirection); // 输出 RIGHT

字符串枚举中的成员必须显式赋值,它们的值就是所赋的字符串。

联合类型

联合类型允许一个变量可以是多种类型中的一种。我们使用 | 来分隔不同的类型。

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

这里 value 变量可以是 string 类型,也可以是 number 类型。在使用联合类型的变量时,需要注意只能访问这些类型共有的属性和方法。

function printValue(v: string | number) {
    // 这里只能访问 string 和 number 共有的属性和方法,比如 toString()
    console.log(v.toString());
}
printValue('abc');
printValue(123);

交叉类型

交叉类型是将多个类型合并为一个类型。它包含了所有类型的特性,使用 & 来表示。

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

这里 ab 变量的类型是 A & B,它必须同时满足 A 接口和 B 接口的要求,即同时具有 a 属性(类型为 string)和 b 属性(类型为 number)。

类型别名与接口

在 TypeScript 中,类型别名和接口是用于定义自定义类型的两种重要方式。

类型别名

类型别名可以为任意类型定义一个新的名字,提高代码的可读性和可维护性。

type UserId = number | string;
let userId: UserId = 123;
userId = '456';

这里定义了一个 UserId 类型别名,它可以是 numberstring 类型。然后我们声明了一个 userId 变量,它可以被赋值为 numberstring 类型的值。

类型别名也可以用于更复杂的类型,比如函数类型。

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

这里 AddFunction 是一个函数类型别名,它表示一个接受两个 number 类型参数并返回一个 number 类型值的函数。

接口

接口主要用于定义对象的形状,即对象所具有的属性和方法。

interface User {
    name: string;
    age: number;
    email: string;
}
let user: User = {
    name: '李四',
    age: 30,
    email: 'lisi@example.com'
};

在上述代码中,User 接口定义了一个对象应该具有 namestring 类型)、agenumber 类型)和 emailstring 类型)属性。user 变量的类型为 User,它必须满足 User 接口的定义。

接口还可以定义可选属性和只读属性。

可选属性

interface Product {
    name: string;
    price: number;
    description?: string; // 可选属性
}
let product: Product = {
    name: '手机',
    price: 5999
};

这里 description 是一个可选属性,在创建 Product 类型的对象时,可以不提供该属性。

只读属性

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

xy 是只读属性,一旦对象被创建,这些属性的值就不能被修改。

函数类型

在 TypeScript 中,函数也是一种类型。我们可以为函数定义输入参数的类型和返回值的类型。

函数声明

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

这里 add 函数接受两个 number 类型的参数 ab,并返回一个 number 类型的值。

函数表达式

let subtract: (a: number, b: number) => number = function (x, y) {
    return x - y;
};

这是一个函数表达式,subtract 变量的类型是一个函数类型,它接受两个 number 类型参数并返回一个 number 类型值。

可选参数和默认参数

函数参数可以是可选的,通过在参数名后加 ? 来表示。

function greet(name: string, message?: string) {
    if (message) {
        console.log(`${name}, ${message}`);
    } else {
        console.log(`Hello, ${name}`);
    }
}
greet('王五');
greet('赵六', '最近怎么样?');

这里 message 是一个可选参数,调用 greet 函数时可以不提供该参数。

我们还可以为参数提供默认值。

function multiply(a: number, b: number = 1) {
    return a * b;
}
console.log(multiply(5)); // 输出 5
console.log(multiply(5, 3)); // 输出 15

b 参数有默认值 1,如果调用 multiply 函数时不提供 b 参数的值,它会使用默认值。

剩余参数

剩余参数允许我们将多个参数收集到一个数组中。

function sum(...numbers: number[]) {
    return numbers.reduce((acc, num) => acc + num, 0);
}
console.log(sum(1, 2, 3)); // 输出 6
console.log(sum(4, 5, 6, 7)); // 输出 22

...numbers 是剩余参数,它将所有传入的参数收集到一个 number 类型的数组中,然后我们可以对这个数组进行操作。

类型推断

TypeScript 具有强大的类型推断能力,它可以在很多情况下自动推断出变量的类型,而无需我们显式地声明类型。

基础类型推断

let num = 42; // TypeScript 自动推断 num 为 number 类型
let str = 'hello'; // 自动推断 str 为 string 类型

这里 numstr 没有显式声明类型,但 TypeScript 根据初始化值推断出了它们的类型。

函数返回值推断

function add(a, b) {
    return a + b;
}
let result = add(1, 2); // result 被推断为 number 类型

add 函数中,虽然没有显式声明参数和返回值类型,但 TypeScript 根据函数体的逻辑推断出返回值类型为 number,因此 result 变量被推断为 number 类型。

上下文类型推断

上下文类型推断是指 TypeScript 根据变量使用的上下文来推断其类型。

document.addEventListener('click', function (event) {
    console.log(event.type); // event 被推断为 MouseEvent 类型
});

这里 addEventListener 的第二个参数是一个函数,根据上下文,TypeScript 推断出 event 的类型为 MouseEvent,因为 click 事件的回调函数参数通常是 MouseEvent 类型。

类型兼容性

在 TypeScript 中,类型兼容性用于判断一个类型是否可以赋值给另一个类型。

基本规则

TypeScript 的类型兼容性基于结构类型系统。在结构类型系统中,如果 A 类型的结构包含了 B 类型的结构,那么 B 类型可以赋值给 A 类型。

interface Animal {
    name: string;
}
interface Dog extends Animal {
    breed: string;
}
let animal: Animal;
let dog: Dog = { name: 'Buddy', breed: 'Golden Retriever' };
animal = dog; // 可以赋值,因为 Dog 包含了 Animal 的结构

这里 Dog 接口扩展自 Animal 接口,Dog 类型的对象可以赋值给 Animal 类型的变量,因为 Dog 包含了 Animal 的所有属性。

函数参数的类型兼容性

对于函数参数的类型兼容性,是逆变的。也就是说,如果 A 类型的函数参数可以赋值给 B 类型的函数参数,那么 B 类型的函数可以赋值给 A 类型的函数。

let func1: (a: number) => void = function (num) { };
let func2: (a: any) => void = function (anyValue) { };
func1 = func2; // 可以赋值,因为 number 是 any 的子类型

这里 func2 的参数类型 anyfunc1 的参数类型 number 更宽泛,所以 func2 可以赋值给 func1

函数返回值的类型兼容性

函数返回值的类型兼容性是协变的。即如果 A 类型的函数返回值可以赋值给 B 类型的函数返回值,那么 A 类型的函数可以赋值给 B 类型的函数。

let func3: () => string = function () { return 'hello'; };
let func4: () => any = function () { return 42; };
func4 = func3; // 可以赋值,因为 string 是 any 的子类型

这里 func3 的返回值类型 stringfunc4 的返回值类型 any 的子类型,所以 func3 可以赋值给 func4

类型断言

类型断言是一种告诉编译器“相信我,我知道自己在做什么”的方式。当我们比编译器更清楚某个值的类型时,可以使用类型断言。

语法

有两种类型断言的语法:尖括号语法和 as 语法。

尖括号语法

let someValue: any = 'this is a string';
let strLength: number = (<string>someValue).length;

这里我们将 someValue 断言为 string 类型,然后访问其 length 属性。

as 语法

let someOtherValue: any = 'this is also a string';
let anotherStrLength: number = (someOtherValue as string).length;

这两种语法的效果是一样的,在 TypeScript 代码中使用 JSX 时,必须使用 as 语法进行类型断言。

类型断言的限制

类型断言并不是类型转换,它只是告诉编译器以某种类型来处理值,并不会在运行时改变值的实际类型。而且类型断言只能在兼容的类型之间进行,比如不能将一个 string 断言为 number

let value: string = 'abc';
// let num: number = value as number; // 报错:类型“string”的参数不能赋给类型“number”的参数

高级类型

除了前面介绍的基本和复杂类型,TypeScript 还提供了一些高级类型,用于处理更复杂的类型操作。

映射类型

映射类型允许我们基于一个已有的类型创建一个新类型,通过对已有类型的属性进行变换。

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

这里 ReadonlyUser 类型通过映射 User 类型的属性,并将每个属性变为只读属性。keyof User 获取 User 类型的所有属性名,P in keyof User 对每个属性名进行遍历,User[P] 获取属性的值类型。

条件类型

条件类型允许我们根据类型关系选择不同的类型。

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

IsString 是一个条件类型,它判断类型 T 是否为 string 类型,如果是则返回 true,否则返回 false

索引类型

索引类型允许我们通过索引来访问对象类型的属性类型。

interface Product {
    name: string;
    price: number;
    inStock: boolean;
}
type NameType = Product['name']; // string
type PriceType = Product['price']; // number

这里通过 Product['name'] 获取 Product 类型中 name 属性的类型 stringProduct['price'] 获取 price 属性的类型 number

通过对 TypeScript 中各种数据类型的深入了解,开发人员可以编写出更健壮、类型安全的代码,充分发挥 TypeScript 在大型项目中的优势。无论是基础数据类型的使用,还是复杂类型和高级类型的灵活运用,都有助于提高代码的可读性、可维护性和可靠性。