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

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

2024-10-231.3k 阅读

基本类型

在 TypeScript 中,基本类型是构成类型系统的基础单元。这些类型直接对应于 JavaScript 中的基本数据类型,但增加了类型检查的能力,让开发者在编写代码时能更明确数据的类型,提前发现潜在的错误。

布尔类型(boolean)

布尔类型用于表示逻辑上的真或假,只有两个取值:truefalse。在 JavaScript 中,布尔值也是广泛用于条件判断等场景,而在 TypeScript 里,我们可以明确地声明一个变量为布尔类型。

let isDone: boolean = false;
// 正确,isDone 是布尔类型,可以赋值 true 或 false
isDone = true; 

// 错误,不能将字符串类型赋值给布尔类型
isDone = 'true'; 

在函数中,我们也可以指定参数和返回值的类型为布尔类型。

function isGreaterThanTen(num: number): boolean {
    return num > 10;
}
let result = isGreaterThanTen(15); 
// result 是布尔类型,因为 isGreaterThanTen 函数返回布尔值

数字类型(number)

TypeScript 中的数字类型和 JavaScript 一样,所有数字都是以 64 位浮点格式表示。无论是整数还是小数,都用 number 类型表示。

let age: number = 25;
let pi: number = 3.14159; 

// 八进制字面量,TypeScript 2.4 及以后版本支持,需开启 --esModuleInterop 或 --allowSyntheticDefaultImports
let octalLiteral: number = 0o744; 

// 十六进制字面量
let hexLiteral: number = 0xf00d; 

// 二进制字面量
let binaryLiteral: number = 0b1010; 

在函数参数和返回值中使用数字类型也很常见。

function addNumbers(a: number, b: number): number {
    return a + b;
}
let sum = addNumbers(5, 3); 
// sum 是数字类型,因为 addNumbers 函数返回数字值

字符串类型(string)

字符串类型用于表示文本数据,在 TypeScript 中通过 string 关键字声明。和 JavaScript 一样,字符串可以用单引号(')、双引号(")或模板字符串(```)表示。

let name: string = 'John Doe';
let greeting: string = "Hello, " + name; 

// 模板字符串
let message: string = `Welcome, ${name}`; 

当函数处理字符串相关逻辑时,我们需要准确指定参数和返回值的类型。

function getFullName(first: string, last: string): string {
    return `${first} ${last}`;
}
let fullName = getFullName('Jane', 'Smith'); 
// fullName 是字符串类型,因为 getFullName 函数返回字符串值

空值类型(void)

void 类型通常用于表示没有返回值的函数。在 JavaScript 中,函数如果没有 return 语句,实际上返回的是 undefined,而在 TypeScript 里,对于这种情况可以明确指定返回值类型为 void

function logMessage(message: string): void {
    console.log(message);
    // 这里没有 return 语句,返回值类型为 void
}
let result2 = logMessage('This is a log'); 
// result2 的类型是 void,不能对其进行其他操作

需要注意的是,虽然 void 类型表示没有返回值,但也可以声明一个 void 类型的变量,不过只能将 undefinednull 赋值给它(在严格模式下,只有 undefined 可以赋值给 void 类型变量)。

let myVoid: void;
myVoid = undefined; 
// 在严格模式下,以下赋值会报错
// myVoid = null; 

null 和 undefined

在 TypeScript 中,nullundefined 有自己的类型,分别为 nullundefined。它们是所有类型的子类型,这意味着可以将 nullundefined 赋值给其他类型的变量,但在严格模式下(strictNullChecks 开启),这种赋值会受到限制。

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

// 严格模式下
let num2: number; 
// num2 = null; 报错,不能将 null 赋值给 number 类型变量
// num2 = undefined; 报错,不能将 undefined 赋值给 number 类型变量

通常,当我们不确定一个值是否存在时,可以使用联合类型(后面会详细介绍联合类型),如 number | nullstring | undefined 来表示可能为 nullundefined 的值。

任意类型(any)

any 类型表示可以是任意类型的值。当我们不确定一个变量的类型,或者希望它能接受任何类型的值时,可以使用 any 类型。这在处理动态数据,如来自用户输入或第三方库的不确定类型数据时很有用。

let data: any;
data = 10;
data = 'Hello';
data = true; 

function printValue(value: any) {
    console.log(value);
}
printValue(123);
printValue('abc');

然而,过度使用 any 类型会失去 TypeScript 类型检查的优势,因为它绕过了类型检查机制。所以,在能确定类型的情况下,应尽量避免使用 any 类型。

never 类型

never 类型表示那些永不存在的值的类型。通常出现在函数抛出异常或永远不会有返回值的情况下。

function throwError(message: string): never {
    throw new Error(message);
}
// 以下代码会报错,因为 throwError 不会返回值,不能赋值给变量
let result3 = throwError('Something went wrong'); 

function infiniteLoop(): never {
    while (true) {
        // 无限循环,不会有返回值
    }
}

never 类型是所有类型的子类型,但没有类型是 never 的子类型(除了 never 自身)。这意味着 never 类型的值可以赋值给任何类型的变量,但其他类型的值不能赋值给 never 类型变量。

枚举类型(enum)

枚举类型是 TypeScript 为 JavaScript 增加的一种数据类型,用于定义一组命名常量。它允许我们用更友好、易读的方式表示一组相关的值。

// 数字枚举
enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
let myDirection: Direction = Direction.Up; 

// 字符串枚举
enum Status {
    Success = 'success',
    Failure = 'failure'
}
let operationStatus: Status = Status.Success; 

在数字枚举中,如果没有显式指定值,枚举成员会从 0 开始自动递增。在上述 Direction 枚举中,Down 的值为 2,Left 的值为 3,Right 的值为 4。 枚举类型在代码中可以提高可读性和可维护性,特别是在需要表示固定集合的场景,如状态码、方向等。

复合类型

复合类型是由基本类型或其他复合类型组合而成的类型,它们为我们构建复杂的数据结构和逻辑提供了强大的能力。

数组类型

在 TypeScript 中,数组类型用于表示一组相同类型元素的集合。有两种常见的声明数组类型的方式。

方式一:类型 + 方括号

let numbers: number[] = [1, 2, 3, 4]; 
let names: string[] = ['Alice', 'Bob', 'Charlie']; 

这里明确指定了数组元素的类型,只有符合该类型的元素才能放入数组中。

// 错误,不能将字符串放入数字数组
numbers.push('five'); 

方式二:使用泛型 Array<类型>

let ages: Array<number> = [20, 21, 22]; 

这两种方式本质上是等价的,只是语法形式略有不同。

当数组元素类型不确定,但又需要有一定约束时,可以使用联合类型。

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

此外,TypeScript 还支持多维数组,例如二维数组可以这样声明:

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

元组类型

元组类型是一种特殊的数组类型,它允许我们定义一个固定长度且每个位置元素类型都明确的数组。与普通数组不同,元组的元素类型和数量在定义时就确定了。

let userInfo: [string, number] = ['John', 25]; 
// 正确,符合元组定义
let userInfo2: [number, string] = [25, 'John']; 
// 错误,类型顺序不符合元组定义

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

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

// 错误,访问越界,元组只有两个元素
let invalidAccess: string = userInfo[2]; 

元组类型在需要表示一组相关但类型不同的数据时非常有用,比如函数返回多个不同类型的值。

function getUser(): [string, number] {
    return ['Jane', 30];
}
let [userName, userAge] = getUser(); 

联合类型

联合类型允许一个变量具有多种类型中的一种。使用竖线(|)来分隔不同的类型。

let value: string | number;
value = 'Hello';
value = 10; 

// 错误,不能将布尔值赋值给联合类型(只允许字符串或数字)
value = true; 

在函数参数中使用联合类型,可以让函数接受多种类型的参数。

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

需要注意的是,当使用联合类型时,只能访问这些类型共有的属性和方法。例如,stringlength 属性,number 没有,所以在上述 printValue2 函数中,需要先判断 value 的类型再访问 length 属性。

交叉类型

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

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

在上述例子中,ab 变量的类型是 A & B,它必须同时满足 A 接口和 B 接口的要求,即包含 a 属性(字符串类型)和 b 属性(数字类型)。 交叉类型常用于混合多个对象类型的特性,比如在 React 中,组件的属性可能需要混合多种不同的接口类型。

interface StyleProps {
    color: string;
}
interface LayoutProps {
    width: number;
}
interface ButtonProps extends StyleProps, LayoutProps {
    label: string;
}
function Button({ color, width, label }: ButtonProps) {
    // 组件逻辑
}

这里 ButtonPropsStylePropsLayoutProps 的交叉类型,同时还添加了自己的 label 属性。

接口类型

接口是 TypeScript 中非常重要的概念,用于定义对象的形状(结构)。接口可以描述对象拥有哪些属性以及这些属性的类型。

interface User {
    name: string;
    age: number;
    email?: string; 
}
let user: User = {
    name: 'Tom',
    age: 28,
    email: 'tom@example.com'
}; 

在上述 User 接口中,nameage 是必需属性,而 email 是可选属性,使用 ? 表示。如果对象缺少必需属性或属性类型不匹配,TypeScript 会报错。

// 错误,缺少 age 属性
let invalidUser: User = {
    name: 'Jerry'
}; 

// 错误,age 属性类型不匹配
let invalidUser2: User = {
    name: 'Jerry',
    age: 'twenty'
}; 

接口还可以定义函数类型。

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

这里 AddFunction 接口定义了一个接受两个数字参数并返回一个数字的函数类型。

接口之间还可以继承,通过继承可以复用其他接口的属性和方法。

interface Employee extends User {
    employeeId: string;
}
let employee: Employee = {
    name: 'Sam',
    age: 32,
    email:'sam@example.com',
    employeeId: '12345'
}; 

Employee 接口继承了 User 接口,所以它除了拥有自己的 employeeId 属性外,还必须包含 User 接口的所有属性。

类型别名

类型别名和接口类似,也用于给类型定义一个新的名字。但类型别名可以表示更复杂的类型,包括基本类型、联合类型、交叉类型等。

type StringOrNumber = string | number;
let data2: StringOrNumber = 'abc';
data2 = 123; 

type Point = {
    x: number;
    y: number;
};
let point: Point = {
    x: 10,
    y: 20
}; 

类型别名和接口有一些区别。接口只能用于定义对象类型,而类型别名可以表示任何类型。并且,类型别名在使用 &| 时可以更方便地组合复杂类型。

type ID = number | string;
interface UserInfo {
    name: string;
}
type UserWithID = UserInfo & { id: ID };
let userWithID: UserWithID = {
    name: 'Eve',
    id: 1
}; 

另外,当使用相同名称定义接口和类型别名时,接口会被视为类型别名的扩展,而不是冲突。

interface Animal {
    name: string;
}
type Animal = Animal & { age: number };
let animal: Animal = {
    name: 'Dog',
    age: 5
}; 

函数类型

在 TypeScript 中,函数也有自己的类型。函数类型包括参数列表和返回值类型。

function addNumbers2(a: number, b: number): number {
    return a + b;
}
let addFunction: (a: number, b: number) => number = addNumbers2; 

这里 (a: number, b: number) => number 就是函数类型,它表示接受两个数字参数并返回一个数字的函数。 当函数作为参数传递时,准确指定函数类型非常重要。

function operate(a: number, b: number, fn: (a: number, b: number) => number): number {
    return fn(a, b);
}
let result4 = operate(5, 3, addNumbers2); 

operate 函数中,fn 参数的类型是一个函数类型,它要求传入的函数必须接受两个数字参数并返回一个数字。

字面量类型

字面量类型是指具体的字面量值所代表的类型,如 1'hello'true 等。我们可以将变量声明为字面量类型,这使得变量只能被赋值为该字面量值。

let myNumber: 10 = 10;
// 错误,不能赋值为其他值
myNumber = 20; 

let myString: 'hello' = 'hello';
// 错误,不能赋值为其他字符串
myString = 'world'; 

字面量类型常与联合类型一起使用,用于限制变量的取值范围。

let status: 'active' | 'inactive' = 'active';
// 只能赋值为 'active' 或 'inactive'
status = 'inactive'; 

在函数参数中使用字面量类型联合,可以确保函数只接受特定的值。

function setStatus(state: 'online' | 'offline') {
    console.log(`Status set to ${state}`);
}
setStatus('online');
// 错误,不能传入其他值
setStatus('unknown'); 

通过深入理解 TypeScript 的基本类型和复合类型,开发者能够更精确地定义数据结构,利用类型系统的优势提高代码的可读性、可维护性以及减少运行时错误,从而编写出更健壮的前端代码。在实际项目中,合理运用这些类型,结合 TypeScript 的其他特性,能够显著提升开发效率和代码质量。