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

TypeScript中的数据类型详解

2023-09-017.7k 阅读

基本数据类型

在TypeScript中,基本数据类型是构成程序的基础,它们代表了最基本的数据单元。这些基本数据类型包括布尔值(boolean)、数字(number)、字符串(string)、空值(null)、未定义(undefined)、任意值(any)和 void 类型。

布尔值(boolean)

布尔值只有两个可能的值:truefalse,用于表示逻辑上的真与假。在JavaScript中,布尔值广泛应用于条件判断语句中,TypeScript 继承了这一特性,并提供了类型检查。

let isDone: boolean = false;
// 正确,布尔值赋值
isDone = true;

// 错误,不能将非布尔值赋值给布尔类型变量
// isDone = 1; 

在上述代码中,我们定义了一个布尔类型的变量 isDone,并初始化为 false。随后,我们可以将其赋值为 true,但如果尝试将如数字 1 这样的非布尔值赋给它,TypeScript 编译器就会报错。

数字(number)

TypeScript中的数字类型与JavaScript一样,所有数字都是以 64 位浮点型格式存储。这意味着它可以表示整数和小数。

let myNumber: number = 42;
let pi: number = 3.14;
let binaryNumber: number = 0b1010; // 二进制
let octalNumber: number = 0o755;  // 八进制
let hexadecimalNumber: number = 0x1F; // 十六进制

console.log(myNumber);
console.log(pi);
console.log(binaryNumber);
console.log(octalNumber);
console.log(hexadecimalNumber);

在这段代码中,我们定义了不同类型的数字变量,包括普通整数 myNumber、小数 pi,以及通过二进制、八进制和十六进制表示的数字。TypeScript 允许我们以这些不同的进制来书写数字,并且会正确地进行类型检查。

字符串(string)

字符串用于表示文本数据,它是由零个或多个Unicode字符组成的序列,用单引号(')、双引号(")或模板字面量(```)括起来。

let myString1: string = 'Hello, world!';
let myString2: string = "This is also a string.";
let templateString: string = `This is a template string. The value of myNumber is ${myNumber}`;

console.log(myString1);
console.log(myString2);
console.log(templateString);

在上述代码中,myString1myString2 分别用单引号和双引号定义了普通字符串。而 templateString 则使用了模板字面量,它允许我们在字符串中嵌入表达式,这里嵌入了之前定义的 myNumber 变量的值。

空值(null)和未定义(undefined)

null 表示一个空值,而 undefined 表示一个未定义的值。在TypeScript中,它们各自有对应的类型。默认情况下,nullundefined 是所有类型的子类型。这意味着你可以将 nullundefined 赋值给其他类型的变量。

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

let num: number;
// 可以将undefined赋值给number类型变量,因为undefined是所有类型的子类型
num = undefined; 

let str: string;
// 可以将null赋值给string类型变量,因为null是所有类型的子类型
str = null; 

然而,在严格模式下(strictNullChecks 开启),nullundefined 只能赋值给 void 和它们各自的类型。

// 开启严格模式(tsconfig.json中 "strictNullChecks": true)
let myNull: null = null;
let myUndefined: undefined = undefined;

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

let str: string;
// 错误,在严格模式下不能将null赋值给string类型变量
// str = null; 

这种严格模式的设定有助于避免一些在运行时可能出现的 nullundefined 相关的错误。

任意值(any)

any 类型表示任意类型。当你不确定一个变量的类型,或者你想要一个可以接受任何类型值的变量时,可以使用 any 类型。

let myAny: any = 'Hello';
console.log(myAny.length);

myAny = 42;
console.log(myAny.toFixed(2));

在上述代码中,myAny 变量被声明为 any 类型,因此我们可以先将其赋值为字符串,调用字符串的 length 属性,然后又将其赋值为数字,并调用数字的 toFixed 方法。虽然 any 类型提供了很大的灵活性,但过度使用它会削弱TypeScript的类型检查优势,可能导致运行时错误难以发现。

void 类型

void 类型通常用于表示函数没有返回值。它也可以用于声明变量,但这种情况下变量只能被赋值为 nullundefined(在非严格模式下)。

function printMessage(): void {
    console.log('This function has no return value.');
}

let myVoid: void;
// 在非严格模式下
myVoid = null; 
myVoid = undefined; 

// 在严格模式下,只能是
myVoid = undefined; 

在上述代码中,printMessage 函数定义为返回 void 类型,表明该函数没有返回值。而 myVoid 变量声明为 void 类型,在不同的模式下有不同的赋值规则。

复杂数据类型

除了基本数据类型,TypeScript还支持多种复杂数据类型,这些类型允许我们构建更丰富的数据结构。复杂数据类型包括数组(Array)、元组(Tuple)、枚举(Enum)、接口(Interface)、类型别名(Type Alias)、函数类型(Function Type)和类(Class)。

数组(Array)

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

第一种方式是在元素类型后面加上 []

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

上述代码中,numbers 数组存储 number 类型的元素,strings 数组存储 string 类型的元素。

第二种方式是使用泛型数组类型 Array<elementType>

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

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

我们也可以定义多维数组。

let matrix: number[][] = [
    [1, 2, 3],
    [4, 5, 6],
    [7, 8, 9]
];

这里的 matrix 是一个二维数组,每个元素又是一个 number 类型的数组。

元组(Tuple)

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

let myTuple: [string, number] = ['Hello', 42];
// 正确,访问元组元素
console.log(myTuple[0]); 
console.log(myTuple[1]); 

// 错误,越界访问
// console.log(myTuple[2]); 

// 错误,类型不匹配
// myTuple = [42, 'Hello']; 

在上述代码中,myTuple 被定义为一个元组,第一个元素是 string 类型,第二个元素是 number 类型。我们可以通过索引访问元组的元素,但要注意不能越界访问,并且重新赋值时元素类型必须与定义时一致。

枚举(Enum)

枚举是一种用于定义命名常量的方式,它可以让我们用更友好的方式表示一组相关的值。TypeScript 支持数字枚举和字符串枚举。

数字枚举

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}

let myDirection: Direction = Direction.Up;
console.log(myDirection); 

在上述代码中,我们定义了一个 Direction 枚举,Up 初始化为 1,其余成员会自动递增。所以 Down2Left3Right4

字符串枚举

enum Status {
    Success = 'success',
    Failure = 'failure'
}

let operationStatus: Status = Status.Success;
console.log(operationStatus); 

字符串枚举中,每个成员必须手动赋值为字符串类型。

接口(Interface)

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

interface Person {
    name: string;
    age: number;
    greet(): string;
}

let tom: Person = {
    name: 'Tom',
    age: 30,
    greet() {
        return `Hello, I'm ${this.name}`;
    }
};

console.log(tom.greet()); 

在上述代码中,Person 接口定义了 name(字符串类型)、age(数字类型)属性以及 greet 方法(返回字符串)。tom 对象符合 Person 接口的形状,因此可以赋值给 Person 类型的变量。

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

可选属性

interface User {
    username: string;
    email?: string;
}

let user1: User = { username: 'john' };
let user2: User = { username: 'jane', email: 'jane@example.com' };

User 接口中,email 属性是可选的,所以 user1 可以没有 email 属性,而 user2 可以有。

只读属性

interface Point {
    readonly x: number;
    readonly y: number;
}

let p: Point = { x: 10, y: 20 };
// 错误,只读属性不能被重新赋值
// p.x = 30; 

Point 接口中,xy 属性被定义为只读,一旦赋值后就不能再更改。

类型别名(Type Alias)

类型别名与接口类似,也用于给类型起一个新名字。不同的是,类型别名可以用于任何类型,而接口主要用于描述对象的形状。

type MyNumber = number;
let num: MyNumber = 42;

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

type Callback = (data: string) => void;
function execute(callback: Callback) {
    callback('Some data');
}

execute((data) => {
    console.log(data);
});

在上述代码中,MyNumbernumber 类型的别名。StringOrNumber 是联合类型(后面会详细介绍)的别名,表示可以是 stringnumber 类型。Callback 是一个函数类型的别名,它定义了一个接受 string 类型参数且没有返回值的函数类型。

函数类型(Function Type)

在TypeScript中,函数也有类型。函数类型由参数类型和返回值类型组成。

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

let result: number = add(3, 5);
console.log(result); 

在上述代码中,add 变量被定义为一个函数类型,它接受两个 number 类型的参数,并返回一个 number 类型的值。

函数类型还可以有可选参数和默认参数。

可选参数

function greet(name: string, message?: string) {
    if (message) {
        return `Hello, ${name}! ${message}`;
    } else {
        return `Hello, ${name}!`;
    }
}

let greeting1: string = greet('Tom');
let greeting2: string = greet('Jerry', 'How are you?');

greet 函数中,message 参数是可选的,调用函数时可以提供也可以不提供。

默认参数

function multiply(a: number, b: number = 2) {
    return a * b;
}

let product1: number = multiply(3);
let product2: number = multiply(3, 4);

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

类(Class)

类是面向对象编程的核心概念,它用于封装数据和行为。在TypeScript中,类的定义与JavaScript类似,但增加了类型检查。

class Animal {
    name: string;
    constructor(name: string) {
        this.name = name;
    }
    speak() {
        return `My name is ${this.name}`;
    }
}

class Dog extends Animal {
    breed: string;
    constructor(name: string, breed: string) {
        super(name);
        this.breed = breed;
    }
    speak() {
        return `My name is ${this.name} and I'm a ${this.breed}`;
    }
}

let myDog: Dog = new Dog('Buddy', 'Golden Retriever');
console.log(myDog.speak()); 

在上述代码中,Animal 类定义了 name 属性和 speak 方法。Dog 类继承自 Animal 类,增加了 breed 属性,并重写了 speak 方法。通过 new 关键字创建 Dog 类的实例 myDog,并调用其 speak 方法。

高级数据类型

除了前面介绍的复杂数据类型,TypeScript还提供了一些高级数据类型,这些类型在处理复杂逻辑和抽象概念时非常有用。包括联合类型(Union Type)、交叉类型(Intersection Type)、字面量类型(Literal Type)、never 类型和 unknown 类型。

联合类型(Union Type)

联合类型表示一个值可以是多种类型中的一种。它使用 | 符号来分隔不同的类型。

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

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

printValue('World');
printValue(3.14);

在上述代码中,value 变量被定义为 string | number 联合类型,因此可以赋值为字符串或数字。printValue 函数接受 string | number 类型的参数,并根据参数的实际类型进行不同的操作。

交叉类型(Intersection Type)

交叉类型表示一个值必须同时满足多种类型的要求。它使用 & 符号来连接不同的类型。

interface A {
    a: string;
}

interface B {
    b: number;
}

let ab: A & B = { a: 'Hello', b: 42 };

在上述代码中,ab 变量被定义为 A & B 交叉类型,因此它必须同时具有 A 接口的 a 属性(字符串类型)和 B 接口的 b 属性(数字类型)。

字面量类型(Literal Type)

字面量类型允许我们将变量的类型限制为特定的字面量值。包括字符串字面量类型、数字字面量类型和布尔字面量类型。

let status: 'active' | 'inactive' = 'active';
// 错误,不能赋值为其他值
// status = 'pending'; 

function compare(a: 1 | 2 | 3, b: 1 | 2 | 3) {
    return a === b;
}

let result1: boolean = compare(1, 2);
let result2: boolean = compare(2, 2);

在上述代码中,status 变量只能被赋值为 'active''inactive'compare 函数接受 1 | 2 | 3 字面量联合类型的参数,只能传入 123

never 类型

never 类型表示永远不会出现的值的类型。通常在函数抛出异常或无限循环的情况下会返回 never 类型。

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

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

在上述代码中,throwError 函数永远不会正常返回,因为它总是抛出异常,所以返回类型是 neverinfiniteLoop 函数进入无限循环,也永远不会返回,返回类型也是 never

unknown 类型

unknown 类型表示任何类型的值,但与 any 类型不同的是,unknown 类型的值在使用前必须进行类型检查。

let myUnknown: unknown = 'Hello';
// 错误,不能直接访问unknown类型值的属性
// console.log(myUnknown.length); 

if (typeof myUnknown ==='string') {
    console.log(myUnknown.length);
}

在上述代码中,myUnknown 变量声明为 unknown 类型,不能直接访问其属性。通过 typeof 进行类型检查后,确定其为 string 类型,才能访问 length 属性。

通过对TypeScript中各种数据类型的详细了解,我们可以更好地利用TypeScript的类型系统,编写出更健壮、可维护的前端代码。无论是简单的基本数据类型,还是复杂的高级数据类型,都在不同的场景下发挥着重要作用,帮助我们在开发过程中尽早发现错误,提高代码质量。在实际项目中,合理运用这些数据类型,结合TypeScript的其他特性,将大大提升开发效率和代码的可靠性。同时,随着项目规模的增大和业务逻辑的复杂化,对数据类型的准确把握和灵活运用将显得尤为关键。例如,在大型前端应用中,接口和类型别名可以用于统一数据结构的定义,使得不同模块之间的数据交互更加清晰和可控;联合类型和交叉类型则可以在处理多种可能情况或组合多种类型需求时发挥重要作用。而对于一些底层的工具函数或者异常处理逻辑,never 类型和 unknown 类型能帮助我们更准确地表达函数的行为和数据的不确定性。总之,深入理解和熟练运用TypeScript的数据类型是成为一名优秀前端开发者的重要基础。