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

TypeScript 基本语法:变量声明与数据类型详解

2023-07-124.5k 阅读

变量声明

在 TypeScript 中,变量声明是编程的基础操作之一,它允许我们在程序中存储和使用数据。TypeScript 提供了几种方式来声明变量,每种方式都有其特点和适用场景。

let 关键字

let 是 TypeScript 中用于声明块级作用域变量的关键字。与 JavaScript 中的 var 不同,let 声明的变量只在其所在的块级作用域内有效。块级作用域可以是 if 语句块、for 循环块、函数块等。

{
    let message: string = "Hello, TypeScript!";
    console.log(message); // 输出: Hello, TypeScript!
}
// console.log(message); // 这里会报错,message 超出了作用域

在上述代码中,message 变量在花括号内声明,其作用域仅限于这个花括号块。当在块外尝试访问 message 时,TypeScript 编译器会报错。

const 关键字

const 用于声明常量,一旦声明,其值就不能再被修改。常量同样具有块级作用域。

const PI: number = 3.14159;
// PI = 3.14; // 这会导致编译错误,常量不能重新赋值

在实际开发中,const 常用于声明那些在程序运行过程中不会改变的值,如数学常量、配置参数等。这有助于提高代码的可读性和可维护性,同时也能让编译器进行一些优化。

var 关键字

虽然 TypeScript 推荐使用 letconst,但它仍然支持传统 JavaScript 的 var 关键字。var 声明的变量具有函数作用域,而不是块级作用域。这意味着在函数内部声明的 var 变量在整个函数内都有效,即使在块级作用域之外。

function varScopeTest() {
    if (true) {
        var localVar: string = "Inside if block";
    }
    console.log(localVar); // 输出: Inside if block
}
varScopeTest();

从上面的代码可以看出,localVar 虽然在 if 块内声明,但在块外仍然可以访问,这是因为 var 的函数作用域特性。然而,这种特性有时会导致意外的行为,所以在 TypeScript 中应尽量避免使用 var

数据类型

TypeScript 是一种强类型语言,它支持多种数据类型,这使得代码在编译阶段就能发现类型不匹配的错误,从而提高代码的稳定性和可维护性。

基本数据类型

TypeScript 支持 JavaScript 的所有基本数据类型,包括 numberstringbooleannullundefinedvoid

number

number 类型用于表示数字,包括整数和浮点数。

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

TypeScript 中的 number 类型与 JavaScript 中的 Number 类型一致,它遵循 IEEE 754 标准,能够表示双精度 64 位浮点数。

string

string 类型用于表示文本数据。字符串可以用单引号 ' 或双引号 " 来表示。

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

TypeScript 还支持模板字符串,通过反引号 ``` 来定义。模板字符串允许在字符串中嵌入表达式。

let age: number = 30;
let message: string = `My age is ${age}`;
console.log(message); // 输出: My age is 30
boolean

boolean 类型只有两个值:truefalse,用于表示逻辑判断。

let isDone: boolean = false;
if (isDone) {
    console.log('Task is completed');
} else {
    console.log('Task is still in progress');
}
null 和 undefined

nullundefined 在 TypeScript 中分别表示空值和未定义值。它们是所有类型的子类型。默认情况下,nullundefined 可以赋值给任何类型的变量。

let value1: string | null = null;
let value2: number | undefined = undefined;

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

// 开启 strictNullChecks
let value3: string;
// value3 = null; // 这会导致编译错误
void

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

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

在上述代码中,logMessage 函数只负责打印消息,没有返回值,所以其返回类型为 void

复杂数据类型

除了基本数据类型,TypeScript 还支持一些复杂数据类型,如数组、元组、对象、枚举等。

数组

数组是一种有序的数据集合,TypeScript 提供了两种方式来声明数组。

  1. 类型 + 方括号
let numbers: number[] = [1, 2, 3, 4, 5];

在这种方式中,number[] 表示这是一个元素类型为 number 的数组。

  1. Array 泛型
let fruits: Array<string> = ['apple', 'banana', 'cherry'];

这里 Array<string> 同样表示一个字符串类型的数组。

数组的长度是可变的,我们可以通过索引来访问和修改数组元素。

let numbers: number[] = [1, 2, 3];
console.log(numbers[0]); // 输出: 1
numbers[1] = 20;
console.log(numbers); // 输出: [1, 20, 3]
元组

元组是一种特殊的数组,它允许存储不同类型的元素,并且元素的数量和类型在声明时就固定下来。

let user: [string, number] = ['John', 30];

在上述代码中,user 元组的第一个元素是 string 类型,第二个元素是 number 类型。如果尝试给元组赋不符合类型或数量的值,会导致编译错误。

// user = ['John']; // 错误,元素数量不足
// user = ['John', 30, 'Male']; // 错误,元素数量过多
// user = [30, 'John']; // 错误,元素类型不匹配

元组可以通过索引访问元素,也可以使用解构赋值来提取元素。

let user: [string, number] = ['John', 30];
let [name, age] = user;
console.log(name); // 输出: John
console.log(age); // 输出: 30
对象

对象是一种无序的键值对集合。在 TypeScript 中,我们可以定义对象的类型,指定每个属性的名称和类型。

let person: { name: string; age: number } = { name: 'Jane', age: 28 };

在上述代码中,person 对象必须包含 name 属性,类型为 string,以及 age 属性,类型为 number。如果对象缺少某个属性或属性类型不匹配,会导致编译错误。

// let wrongPerson: { name: string; age: number } = { name: 'Bob' }; // 错误,缺少 age 属性
// let wrongPerson: { name: string; age: number } = { name: 'Bob', age: 'twenty' }; // 错误,age 类型不匹配

对象还可以有可选属性,通过在属性名后加 ? 来表示。

let user: { name: string; age?: number } = { name: 'Alice' };
// 这里 user 可以没有 age 属性
枚举

枚举是一种用于定义命名常量集合的类型。它可以让我们用更具描述性的名称来表示数值。

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

在上述代码中,Color 枚举定义了三个常量 RedGreenBlue。默认情况下,它们的值从 0 开始依次递增。我们也可以手动指定枚举成员的值。

enum Direction {
    Up = 1,
    Down,
    Left,
    Right
}
let myDirection: Direction = Direction.Right;
console.log(myDirection); // 输出: 4

枚举在需要使用一组相关常量的场景中非常有用,比如表示状态码、方向等。

类型推论

TypeScript 具有类型推论机制,这意味着在某些情况下,我们不需要显式地指定变量的类型,TypeScript 编译器可以根据变量的初始值推断出其类型。

let num = 10; // TypeScript 推断 num 为 number 类型
let str = 'Hello'; // TypeScript 推断 str 为 string 类型

类型推论在函数返回值的推断中也很常见。

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

然而,在某些复杂情况下,显式地指定类型可以使代码更清晰,也有助于避免潜在的错误。

类型断言

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

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

在上述代码中,someValue 的类型是 any,我们通过类型断言 (someValue as string) 告诉编译器 someValue 实际上是一个字符串,这样就可以访问其 length 属性。

类型断言还有另一种语法 (<type>value),但在 JSX 中只能使用 as 语法。

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

联合类型与交叉类型

联合类型

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

let value: string | number;
value = 'Hello';
console.log(value.length); // 输出: 5
value = 10;
// console.log(value.length); // 这会导致编译错误,number 类型没有 length 属性

在上述代码中,value 可以是 string 类型或 number 类型。当 valuestring 类型时,可以访问 length 属性,但当 valuenumber 类型时,访问 length 属性会导致编译错误。

在使用联合类型时,我们需要注意只能访问联合类型中所有类型共有的属性和方法。

function printValue(val: string | number) {
    // 这里只能访问 string 和 number 共有的属性和方法,如 toString()
    console.log(val.toString());
}
printValue('Hello'); // 输出: Hello
printValue(10); // 输出: 10

交叉类型

交叉类型用于将多个类型合并为一个类型,新类型具有所有交叉类型的特性。我们使用 & 来表示交叉类型。

interface A {
    name: string;
}
interface B {
    age: number;
}
let person: A & B = { name: 'Tom', age: 25 };

在上述代码中,person 类型是 AB 的交叉类型,所以它必须同时包含 name 属性(类型为 string)和 age 属性(类型为 number)。

交叉类型常用于需要一个对象同时满足多个接口定义的场景,它可以让我们更灵活地组合不同的类型定义。

类型别名与接口

类型别名

类型别名是给一个类型起一个新的名字,它可以是基本类型、复杂类型或联合类型等。

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

在上述代码中,MyNumbernumber 类型的别名,num 变量的类型实际上就是 number

类型别名对于联合类型和复杂类型的定义非常有用,可以提高代码的可读性。

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

类型别名也可以用于定义函数类型。

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

接口

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

interface User {
    name: string;
    age: number;
}
let user: User = { name: 'John', age: 30 };

接口可以有可选属性和只读属性。

interface Product {
    name: string;
    price: number;
    description?: string; // 可选属性
    readonly id: number; // 只读属性
}
let product: Product = { name: 'Book', price: 20, id: 1 };
// product.id = 2; // 这会导致编译错误,id 是只读属性

接口还可以继承其他接口,以扩展其定义。

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

在上述代码中,Dog 接口继承了 Animal 接口,所以 Dog 接口除了有自己的 breed 属性外,还必须包含 Animal 接口的 name 属性。

函数类型

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

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

在上述代码中,add 函数的参数 ab 都是 number 类型,返回值也是 number 类型。

我们还可以使用类型别名来定义函数类型,然后用这个类型别名来声明函数。

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

函数参数可以有默认值,并且默认值参数后面的参数也可以省略。

function greet(name: string = 'Guest') {
    console.log(`Hello, ${name}`);
}
greet(); // 输出: Hello, Guest
greet('John'); // 输出: Hello, John

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

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

总结

通过深入了解 TypeScript 的变量声明和数据类型,我们可以编写出更健壮、更易于维护的前端代码。变量声明的不同方式,如 letconstvar,各自适用于不同的场景,合理选择可以避免许多潜在的错误。丰富的数据类型,从基本数据类型到复杂数据类型,再到联合类型、交叉类型等,为我们精确描述程序中的数据提供了强大的工具。同时,类型推论、类型断言、类型别名、接口以及函数类型等特性,进一步增强了 TypeScript 的类型系统,使我们能够在编译阶段就捕获许多类型相关的错误,从而提高开发效率和代码质量。在实际项目中,灵活运用这些知识,将有助于我们打造高质量的前端应用。