引言
TypeScript,作为JavaScript的超集,以其强大的静态类型系统、面向对象特性和卓越的工程化能力,在现代前端开发领域占据了重要地位。它不仅提升了大型项目的可维护性和代码质量,也为开发者提供了更丰富的工具支持和更好的开发体验。本文旨在为广大读者提供一份全面的TypeScript技术学习路线图,涵盖基础概念、工程实践、生态系统集成以及进阶主题,帮助您从入门到精通,全方位掌握这一备受推崇的编程语言。
一、静态类型系统
1. 基础类型与类型注解
// 基础类型示例
let isDone: boolean = true;
let myNumber: number = 42;
let myString: string = 'Hello, TypeScript';
let myNull: null = null;
let myUndefined: undefined = undefined;
// 字面量类型示例
let myTrue: true = true;
let myFourtyTwo: 42 = 42;
let myGreeting: 'Hello, TypeScript' = 'Hello, TypeScript';
// any 类型,允许赋值任何类型
let anything: any = 'Anything goes!';
anything = 42;
anything = false;
// unknown 类型,表示未知类型,需要显式类型断言或检查才能使用
let mysteryValue: unknown = 'This could be anything';
mysteryValue = 42; // 合法,但后续使用需谨慎
// void 类型,表示没有任何返回值的函数
function log(message: string): void {
console.log(message);
}
// never 类型,表示永远不会返回或抛出异常的函数
function throwError(message: string): never {
throw new Error(message);
}
2. 复合类型与类型推断
// 数组类型
let numbers: number[] = [1, 2, 3];
let strings: Array<string> = ['a', 'b', 'c'];
// 元组类型,固定长度和类型的数组
let coordinates: [number, number] = [40.7128, -74.0060];
// 枚举类型
enum Color {Red, Green, Blue}
let myColor: Color = Color.Green;
// 对象类型:接口与类型别名
// 接口定义
interface Person {
name: string;
age: number;
sayHello(): void;
}
// 类型别名定义
type Address = {
street: string;
city: string;
zipCode: number;
};
// 实例化对象
let person: Person = {
name: 'Alice',
age: 30,
sayHello() {
console.log(`Hello, I'm ${this.name}`);
}
};
let address: Address = {
street: '123 Main St.',
city: 'New York',
zipCode: 10001
};
3. 类型检查与类型兼容性
// 类型检查示例
let myNum: number = 'This will fail'; // 错误:类型“string”不能赋给类型“number”
// 类型兼容性示例
interface Square {
width: number;
height: number;
}
interface Rectangle {
width: number;
height: number;
}
let square: Square = { width: 10, height: 10 };
let rectangle: Rectangle = square; // 正确:Square 和 Rectangle 结构相同
// 函数类型兼容性示例
function add(a: number, b: number): number {
return a + b;
}
let anotherAdd: (x: number, y: number) => number = add; // 正确:函数类型兼容
// 协变与逆变示例
interface Animal {}
class Dog implements Animal {}
function processAnimal(animal: Animal): Animal {
return animal;
}
let dog: Dog = new Dog();
let processedDog: Dog = processAnimal(dog); // 错误:返回类型 Animal 不兼容 Dog
// 逆变:参数类型
function feed(animal: Animal) {}
feed(dog); // 正确:Dog 是 Animal 的子类型,可以在需要 Animal 参数的地方使用
// 协变:返回类型
function createAnimal(): Animal {
return new Dog();
}
let createdDog: Dog = createAnimal(); // 错误:返回的 Animal 类型不兼容 Dog
二、类(Classes)
TypeScript 中的 class 是实现面向对象编程(OOP)的重要组成部分,它提供了封装、继承、多态等特性。接下来通过代码示例和详细讲解来阐述 TypeScript 中 class 的使用:
1. 基础类定义与成员
class Person {
// 属性(字段)
name: string;
age: number;
// 构造函数
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
// 方法
introduceYourself() {
console.log(`Hi, I am ${this.name} and I am ${this.age} years old.`);
}
}
// 实例化类
const alice = new Person('Alice', 25);
alice.introduceYourself(); // 输出:Hi, I am Alice and I am 25 years old.
详解:
- class Person 定义了一个名为 Person 的类,代表一个具有姓名和年龄的人。
- 类的属性(字段)直接在类体中声明,这里定义了 name(字符串类型)和 age(数字类型)。
- constructor 是类的特殊方法,用于创建新对象时初始化对象的属性。在这个例子中,构造函数接收 name 和 age 参数,并通过 this 关键字将它们赋值给对应的类属性。
- introduceYourself 是类的一个普通方法,用于输出人物的自我介绍。方法内部通过 console.log 打印信息,并使用 this 访问实例的 name 和 age 属性。
- 通过 new Person('Alice', 25) 创建 Person 类的一个实例 alice,然后调用其 introduceYourself 方法。
2. 访问修饰符(Access Modifiers)
class Person {
private _name: string;
protected _age: number;
public occupation: string;
constructor(name: string, age: number, occupation: string) {
this._name = name;
this._age = age;
this.occupation = occupation;
}
get name() {
return this._name;
}
set name(value: string) {
if (value.trim().length > 0) {
this._name = value;
} else {
console.warn('Name cannot be empty.');
}
}
introduceYourself() {
console.log(`Hi, I am ${this._name} (${this.occupation}), and I am ${this._age} years old.`);
}
}
class Employee extends Person {
promote(employee: Employee) {
employee._age++; // 可以访问受保护的 _age 属性
}
}
const alice = new Person('Alice', 25, 'Software Engineer');
alice.introduceYourself();
// 下面的代码会引发编译错误,因为试图访问私有属性或方法
// console.log(alice._name);
// alice._name = 'Bob';
详解:
- 在类中添加了访问修饰符:private、protected 和 public。private 表示仅在类内部可见;protected 表示在类本身及其子类中可见;public 是默认修饰符,表示在任何地方都可见。
- 将 name 和 age 修改为私有属性 _name 和 _age,并在外部提供公有的 getter 和 setter 方法以控制对这些属性的访问。这样可以隐藏内部细节,确保数据一致性。
- 子类 Employee 继承自 Person,并定义了一个 promote 方法。由于 _age 是受保护的,子类可以直接访问它。
- 外部尝试直接访问私有属性或方法会导致编译错误,体现了访问修饰符的限制作用。
3. 继承与多态
abstract class Animal {
abstract makeSound(): void;
move(distanceInMeters: number = 0) {
console.log(`Moved ${distanceInMeters}m.`);
}
}
class Dog extends Animal {
makeSound() {
console.log('Woof!');
}
bark() {
console.log('Barking...');
}
}
class Cat extends Animal {
makeSound() {
console.log('Meow!');
}
purr() {
console.log('Purring...');
}
}
const doggy = new Dog();
doggy.makeSound(); // 输出:Woof!
doggy.move(10); // 输出:Moved 10m.
doggy.bark(); // 输出:Barking...
const kitty = new Cat();
kitty.makeSound(); // 输出:Meow!
kitty.move(); // 输出:Moved 0m. (默认距离)
kitty.purr(); // 输出:Purring...
详解:
- abstract class Animal 定义了一个抽象类,包含一个抽象方法 makeSound 和一个普通方法 move。抽象方法只有声明没有实现,要求子类必须提供具体实现。
- Dog 和 Cat 分别继承自 Animal,实现了父类的 makeSound 方法,并各自添加了特有的方法 bark 和 purr。
- 实例化 Dog 和 Cat 类,并分别调用它们的方法。虽然 Dog 和 Cat 类型不同,但都属于 Animal 类型,因此都可以调用 move 方法,这就是多态的表现。同时,每个类都能调用自己的特有方法。
三、接口(Interfaces)
TypeScript 中的 interface 是一种用于描述对象结构的方式,它定义了一组强制实现的属性、方法签名以及其他成员。接口为代码提供了更强的类型约束,确保对象符合预期的形状。接下来通过代码示例和详细讲解来阐述 TypeScript 中 interface 的使用:
1. 基本接口定义与实现
// 定义接口
interface Person {
name: string;
age: number;
introduceYourself(): void;
}
// 实现接口
class RealPerson implements Person {
name: string;
age: number;
constructor(name: string, age: number) {
this.name = name;
this.age = age;
}
introduceYourself() {
console.log(`Hi, I am ${this.name} and I am ${this.age} years old.`);
}
}
const alice = new RealPerson('Alice', 25);
alice.introduceYourself(); // 输出:Hi, I am Alice and I am 25 years old.
详解:
- interface Person 定义了一个名为 Person 的接口,它包含三个成员:一个字符串类型的 name 属性、一个数字类型的 age 属性,以及一个无返回值的 introduceYourself 方法。
- class RealPerson implements Person 表明 RealPerson 类实现了 Person 接口。这意味着 RealPerson 必须提供与 Person 接口中定义的所有成员相匹配的实现。
- RealPerson 类实现了 Person 接口所需的所有属性和方法。构造函数用于初始化属性,introduceYourself 方法实现了接口中声明的行为。
- 实例化 RealPerson 类,并调用其方法,验证其实现了 Person 接口。
2. 可选属性与只读属性
interface PersonDetails {
firstName: string;
lastName?: string; // 可选属性,可以省略
readonly birthYear: number; // 只读属性,只能在构造时或声明时赋值
}
const person1: PersonDetails = {
firstName: 'Alice',
birthYear: 1995,
};
const person2: PersonDetails = {
firstName: 'Bob',
lastName: 'Smith',
birthYear: 1988,
};
person1.firstName = 'Alicia'; // 正常修改可写属性
person2.lastName = 'Johnson'; // 正常修改可写属性
person1.birthYear = 1996; // 错误:birthYear 是只读属性
person2.birthYear = 1989; // 错误:birthYear 是只读属性
详解:
- PersonDetails 接口中,lastName 标记为 ? 表示它是可选属性,对象在实现该接口时可以选择是否包含这个属性。
- birthYear 前面加上 readonly 关键字,表明它是只读属性。一旦赋值后,就不能再修改其值。
- 实例化两个 PersonDetails 对象,其中一个未提供 lastName,验证了可选属性的灵活性。
- 尝试修改 birthYear 属性值,编译器会报错,说明只读属性受到了有效保护。
3. 函数类型接口与回调函数
interface SearchFunction {
(query: string, callback: (results: string[]) => void): void;
}
function performSearch(searchFn: SearchFunction, query: string) {
searchFn(query, (results) => {
console.log(`Found results for "${query}":`);
results.forEach((result) => console.log(result));
});
}
performSearch(
function (query, cb) {
const fakeResults = ['Result 1', 'Result 2'];
setTimeout(() => cb(fakeResults), 1000);
},
'typescript'
);
详解:
- SearchFunction 接口定义了一个函数类型,它接受一个字符串类型的 query 参数和一个回调函数作为第二个参数。回调函数的类型是 (results: string[]) => void,表示它接受一个字符串数组并返回 void。
- performSearch 函数接受一个 SearchFunction 类型的参数 searchFn 和一个查询字符串 query。在函数体内,调用 searchFn 并传入查询字符串和一个处理结果的回调函数。
- 调用 performSearch 函数,传递一个匿名函数作为 searchFn 参数。这个匿名函数符合 SearchFunction 接口定义,接收查询字符串和回调函数,并在模拟搜索后通过 setTimeout 触发回调,传回假定的搜索结果。
四、泛型(Generics)
TypeScript 中的泛型(Generics)是一种强大的工具,允许我们编写适用于多种类型的数据结构和函数。泛型通过引入类型参数(Type Parameters)来创建可重用的组件,这些组件可以在不同的上下文中使用不同的具体类型。以下是一些关于 TypeScript 泛型的代码示例和详细讲解:
1. 基础泛型函数
function identity<T>(arg: T): T {
return arg;
}
const strResult = identity('Hello'); // 返回类型为 string
const numResult = identity(42); // 返回类型为 number
// 类型推断:编译器自动识别 T 为传入的类型
const inferredResult = identity({ message: 'Generic greeting' }); // 返回类型为 { message: string; }
详解:
- identity 函数使用 <T> 定义了一个泛型类型参数 T。
- 函数参数 arg 的类型为 T,意味着它可以接收任何类型作为参数。
- 函数返回类型也为 T,保证返回值类型与传入参数类型一致。
- 当调用 identity 函数时,无需显式指定 T 的具体类型。编译器会根据传入参数的类型自动推断 T 的实际类型,如上例所示。
2. 泛型约束
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length);
return arg;
}
loggingIdentity({ length: 10, value: 'ten' }); // 正确:对象具有 length 属性
loggingIdentity('short'); // 错误:字符串没有 length 属性
详解:
- Lengthwise 接口定义了一个通用的约束条件,即拥有 length 属性且其类型为 number。
- loggingIdentity 函数使用泛型参数 T,并通过 extends 关键字指定了 T 必须满足 Lengthwise 接口的约束。
- 函数内可以安全地访问 arg.length,因为编译器知道所有满足 Lengthwise 约束的类型都有此属性。
- 当尝试传入不符合约束条件的参数时(如字符串),编译器会报错,确保类型安全。
3. 泛型类
class Box<T> {
contents: T;
constructor(contents: T) {
this.contents = contents;
}
inspect() {
return `Box containing ${this.contents}`;
}
}
const boxOfNumbers = new Box<number>(42);
boxOfNumbers.inspect(); // 输出:"Box containing 42"
const boxOfStrings = new Box<string>('Hello');
boxOfStrings.inspect(); // 输出:"Box containing Hello"
详解:
- Box 类使用泛型 T 定义了其 contents 属性的类型。
- 构造函数接收一个 T 类型的参数,并将其赋值给 contents。
- inspect 方法返回一个描述 Box 内容的字符串,其中包含了 contents 的值。
- 创建 Box 类的实例时,指定 T 的具体类型。在本例中,创建了分别存储数字和字符串的 Box 实例,并调用 inspect 方法展示其内容。
4. 泛型接口与泛型类型别名
// 泛型接口
interface Container<T> {
items: T[];
addItem(item: T): void;
}
// 泛型类型别名
type Pair<K, V> = {
key: K;
value: V;
};
// 实现与使用
const numberContainer: Container<number> = {
items: [],
addItem(item: number) {
this.items.push(item);
},
};
const stringPair: Pair<string, number> = {
key: 'example',
value: 42,
};
详解:
- Container 接口使用泛型 T 定义了 items 属性的元素类型以及 addItem 方法的参数类型。
- Pair 类型别名使用泛型 K 和 V 定义了一个键值对对象,其中 key 属性类型为 K,value 属性类型为 V。
- 创建 Container 和 Pair 的实例时,指定泛型参数的具体类型。这里分别为 Container<number> 和 Pair<string, number>。
五、模块(Modules)
TypeScript 中的模块(Modules)用于组织和管理代码,提供了一种有效的手段来分割大型程序为可复用的、独立的块,同时避免全局命名空间污染。模块之间可以通过导入(import)和导出(export)语句来共享和引用彼此的变量、函数、类等定义。以下是 TypeScript 模块相关的代码示例和详细讲解:
1. 内部模块(Namespace)
// file: math.ts
namespace MathLib {
export function add(x: number, y: number): number {
return x + y;
}
export function subtract(x: number, y: number): number {
return x - y;
}
}
// file: main.ts
import * as Math from './math';
console.log(Math.add(3, 5)); // 输出:8
console.log(Math.subtract(10, 3)); // 输出:7
详解:
- 在 math.ts 文件中,使用 namespace MathLib 定义了一个内部模块(也称为命名空间)。内部模块有助于将相关功能组合在一起,防止全局作用域内的命名冲突。
- MathLib 模块内定义了两个导出函数 add 和 subtract,使用 export 关键字标记为对外可见。
- 在 main.ts 文件中,使用 import * as Math from './math' 导入整个 MathLib 模块。这里使用了命名空间导入语法,将模块作为一个对象引入,并赋予别名 Math。
- 在主文件中,通过 Math.add 和 Math.subtract 调用导入的模块内函数。
2. ES6 模块(ECMAScript Modules, ESM)
// file: utility.ts
export function capitalize(str: string): string {
return str.charAt(0).toUpperCase() + str.slice(1);
}
export const PI = 3.14159;
// file: main.ts
import { capitalize, PI } from './utility';
console.log(capitalize('typescript')); // 输出:TypeScript
console.log(PI); // 输出:3.14159
详解:
- 在 utility.ts 文件中,使用 export 关键字分别导出了函数 capitalize 和常量 PI。
- 在 main.ts 文件中,使用 import { capitalize, PI } from './utility' 导入所需的特定模块成员。这里采用的是 ES6 模块的解构导入语法,直接导入并命名所要使用的函数和常量。
- 主文件中直接使用导入的 capitalize 函数和 PI 常量。
3. 默认导出与默认导入
// file: greet.ts
export default function greet(name: string): string {
return `Hello, ${name}!`;
}
// file: main.ts
import greetFunc from './greet';
console.log(greetFunc('World')); // 输出:Hello, World!
详解:
- 在 greet.ts 文件中,使用 export default 关键字导出了一个默认函数 greet。每个模块只能有一个默认导出。
- 在 main.ts 文件中,使用 import greetFunc from './greet' 导入默认导出。此时无需使用大括号指定具体的导入项,导入的名称可以任意选择。
- 主文件中直接使用导入的默认函数 greetFunc。
4. 重新导出(Re-exporting)
// file: colors.ts
export const Red = '#FF0000';
export const Green = '#00FF00';
export const Blue = '#0000FF';
// file: color-utils.ts
export function mixColors(color1: string, color2: string): string {
// 实现颜色混合逻辑...
}
// file: index.ts
export * from './colors';
export * from './color-utils';
// file: main.ts
import { Red, Green, mixColors } from './index';
console.log(Red); // 输出:'#FF0000'
console.log(mixColors(Green, Blue)); // 输出:混合后的颜色字符串
详解:
- colors.ts 和 color-utils.ts 分别导出了颜色常量和颜色混合函数。
- 在 index.ts 文件中,使用 export * from './colors' 和 export * from './color-utils' 重新导出其他模块的所有导出项。这使得 index.ts 成为一个聚合模块,简化了外部文件的导入过程。
- 在 main.ts 文件中,通过导入 index.ts,一次即可获得所需的所有颜色常量和颜色混合函数。
六、枚举(Enums)
TypeScript 中的枚举(Enums)是一种特殊的类型,它允许定义一组命名的常量集合,这些常量通常代表一组固定的、相关的值。枚举提供了类型安全的值检查和易于理解的代码,尤其是在需要表示一组预定义状态或选项的情况下非常有用。以下是 TypeScript 枚举的相关代码示例和详细讲解:
1. 基本枚举定义与使用
// 定义枚举
enum Color {
Red,
Green,
Blue,
}
// 使用枚举
const myFavoriteColor: Color = Color.Green;
console.log(myFavoriteColor); // 输出:1(Green 在枚举中的索引值)
console.log(Color[myFavoriteColor]); // 输出:“Green”(通过索引值获取枚举名)
详解:
- 使用 enum Color 定义了一个名为 Color 的枚举类型,其中包含三个成员:Red、Green 和 Blue。
- 枚举成员的默认值是从 0 开始递增的整数。Red 对应 0,Green 对应 1,Blue 对应 2。
- 声明变量 myFavoriteColor 为 Color 类型,并赋值为 Color.Green。此时,myFavoriteColor 包含枚举成员 Green 的值(即索引值 1)。
- 直接输出 myFavoriteColor 得到其数值表示(索引值)。
- 使用枚举名作为对象的键,通过 Color[myFavoriteColor] 获取对应的枚举成员名(即字符串 “Green”)。
2. 枚举成员自定义值
enum FileAccess {
None = 0,
Read = 1 << 1, // 二进制位运算,等于 2 (10)
Write = 1 << 2, // 二进制位运算,等于 4 (100)
ReadWrite = Read | Write, // 二进制位运算,等于 6 (110)
}
let mode: FileAccess = FileAccess.ReadWrite;
console.log(mode); // 输出:6
console.log(FileAccess[mode]); // 输出:“ReadWrite”
详解:
- 在 FileAccess 枚举中,为部分或全部成员指定了自定义的数值。
- Read 和 Write 成员使用二进制位运算分别指定值为 2 和 4,ReadWrite 成员则通过 | 运算符合并 Read 和 Write 的值,得到 6。
- 声明变量 mode 为 FileAccess 类型,并赋值为 FileAccess.ReadWrite。此时,mode 包含枚举成员 ReadWrite 的值(即自定义值 6)。
- 输出 mode 得到其数值表示(自定义值)。
- 通过 FileAccess[mode] 获取对应的枚举成员名(即字符串 “ReadWrite”)。
3. 枚举反向映射
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
let directionString: string = Direction.Up;
let directionFromValue: Direction = Direction['LEFT'];
console.log(directionString); // 输出:“UP”
console.log(directionFromValue); // 输出:Direction.Left(类型为 Direction)
详解:
- Direction 枚举的成员使用字符串作为值。
- 声明变量 directionString 为字符串类型,并赋值为 Direction.Up。此时,directionString 包含枚举成员 Up 的值(即字符串 “UP”)。
- 通过字符串索引 Direction['LEFT'],从枚举中获取对应的枚举成员(即 Direction.Left)。注意,此处返回的是枚举成员而非字符串值。
- 输出 directionString 得到其字符串值,输出 directionFromValue 显示其枚举成员名(TypeScript 编译后实际为枚举值对应的索引)。
七、三元组(Tuples)
TypeScript 中的三元组(Tuples)是一种特殊类型的数组,它们固定了元素的数量和每个位置上的元素类型。与普通数组不同,三元组严格规定了其长度以及各元素的类型顺序,这使得它们非常适合用来表示具有明确结构的小型数据集合。下面通过代码示例和详细讲解介绍 TypeScript 中三元组的使用:
1. 基本三元组定义与使用
// 定义三元组类型
type RGB = [number, number, number];
// 实例化三元组
const white: RGB = [255, 255, 255];
const black: RGB = [0, 0, 0];
// 访问和操作三元组元素
console.log(white[0], white[1], white[2]); // 输出:255 255 255
console.log(black.join(', ')); // 输出:“0, 0, 0”
// 尝试错误的赋值
const invalidRGB: RGB = [255, 255]; // 错误:缺少第三个元素
const alsoInvalid: RGB = [255, 'red', 0]; // 错误:第二个元素类型不正确
详解:
- 使用类型别名 type RGB 定义了一个三元组类型,它包含三个 number 类型的元素。
- 实例化两个 RGB 类型的变量 white 和 black,分别赋值为表示白色和黑色的 RGB 值。
- 通过索引来访问三元组的各个元素,并打印它们的值。也可以使用数组方法(如 join)操作三元组,因为它本质上仍然是一个数组。
- 尝试错误地赋值给 RGB 类型的变量,由于元素数量或类型与三元组定义不符,编译器会报错,体现了三元组的类型安全性。
2. 可选元素与剩余元素
// 定义带有可选元素和剩余元素的三元组
type Point = [number, number, number?];
type NameAndRest = [string, ...string[]];
// 实例化三元组
const point2D: Point = [1, 2];
const point3D: Point = [1, 2, 3];
const names: NameAndRest = ['Alice', 'Bob', 'Charlie', 'David'];
// 访问和操作三元组元素
console.log(point2D[0], point2D[1]); // 输出:1 2
console.log(names[0], names.slice(1)); // 输出:“Alice” ["Bob", "Charlie", "David"]
详解:
- Point 三元组中最后一个元素标记为 ?,表示它是可选的。因此,可以实例化只有两个元素的 point2D 或者包含三个元素的 point3D。
- NameAndRest 三元组使用 ...string[] 表示剩余元素。第一个元素必须是 string 类型,后面的元素可以是任意数量的 string 类型。names 变量就是一个包含多个字符串的实例。
- 访问和操作这些三元组与常规数组类似,但需要注意它们的类型限制。
八、类型断言
TypeScript 中的类型断言(Type Assertions)允许程序员在编译时强制指定一个值的类型,从而覆盖编译器的类型推断。类型断言主要用于两种场景:一是当编译器无法准确推断出值的确切类型时,帮助编译器理解意图;二是当编译器推断出的类型过于宽泛,而你知道某个表达式的实际类型更为具体时,进行细化类型指定。以下是一些 TypeScript 类型断言的代码示例及详细讲解:
1. 尖括号(as)语法
// 示例1:将 any 类型的值断言为特定类型
const data = JSON.parse(jsonString) as MyDataType;
// 示例2:将联合类型断言为其中一个具体类型
let value: string | number = getSomeValue();
if (typeof value === 'string') {
let stringValue = value as string;
// 在此处,stringValue 确定为 string 类型
}
// 示例3:将 unknown 类型断言为具体类型
function handleInput(input: unknown) {
const userInput = input as string;
// 继续处理已断言为 string 类型的 userInput
}
详解:
- 使用 expression as Type 格式进行类型断言,其中 expression 是待断言的值,Type 是期望断言的目标类型。
- 示例1中,JSON.parse() 返回 any 类型,通过类型断言将其转换为已知的 MyDataType 类型,以便后续使用。
- 示例2中,变量 value 为 string | number 联合类型。在 if 语句中,通过类型断言将确定为 string 类型的 value 赋值给新变量 stringValue,这样后续代码就可以安全地假定 stringValue 是字符串。
- 示例3中,函数 handleInput 接收未知类型 unknown 的输入。在函数内部,通过类型断言将 input 断言为 string 类型,便于后续针对字符串类型的处理。
2. 类型断言的另一种语法:非空断言操作符(!)
let someObject: { foo?: string } = { };
// 不使用非空断言
if (someObject.foo !== undefined) {
console.log(someObject.foo.toLowerCase());
}
// 使用非空断言
console.log((someObject.foo!).toLowerCase());
详解:
- 非空断言操作符(!)可以直接放在可能为 null 或 undefined 的表达式后面,表示你确信该表达式在运行时一定有值,不会是 null 或 undefined。
- 在不使用非空断言的例子中,使用条件判断确保 someObject.foo 存在后再调用 toLowerCase() 方法。
- 使用非空断言的例子中,直接在 someObject.foo 后面加上 !,告诉编译器你已经确认 foo 属性存在,因此可以安全地调用 toLowerCase() 方法,无需额外的条件检查。
九、异步编程
TypeScript 支持多种异步编程模式,其中包括 Promise、async/await 以及 Generators。在这里,我们将重点介绍使用广泛且易于理解的 async/await 方式。async/await 是一种基于 Promises 的简洁语法,用于处理异步操作,使异步代码看起来更接近同步代码,提高了可读性和可维护性。以下是 TypeScript 中使用 async/await 进行异步编程的代码示例及详细讲解:
1. 基本 async/await 示例
async function fetchUser(id: number): Promise<{ name: string; age: number }> {
try {
const response = await fetch(`https://api.example.com/users/${id}`);
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const user = await response.json();
return user;
} catch (error) {
console.error('An error occurred:', error);
throw error;
}
}
fetchUser(1)
.then(user => console.log(user))
.catch(error => console.error('Error fetching user:', error));
详解:
- 使用 async 关键字声明一个异步函数 fetchUser,该函数返回一个 Promise,其 resolve 值为 { name: string; age: number } 类型的对象。
- 在函数内部,await 关键字用于等待 fetch API 的异步操作完成。await 只能在 async 函数内部使用。
- await fetch(...) 会使函数暂停执行,直到 fetch 返回的 Promise 解决。如果 Promise 被解决(即 HTTP 请求成功),await 表达式的结果将是 Response 对象。
- 同样地,await response.json() 暂停执行,等待 json 方法返回的 Promise 解析响应体为 JavaScript 对象。解析完成后,user 变量被赋值为解析结果。
- 如果在 await 表达式中遇到 Promise 被拒绝(如 HTTP 错误),async 函数会抛出一个异常。可以使用 try/catch 块捕获并处理这些异常。
- 外部代码通过 .then/.catch 或 await 语句消费 fetchUser 函数返回的 Promise,处理成功或失败的情况。
2. 链式 async/await 示例
async function getUserInfo(userId: number): Promise<{ name: string; email: string }> {
const user = await fetchUser(userId);
const userEmail = await fetchEmailForUser(user.id);
return {
name: user.name,
email: userEmail,
};
}
async function fetchEmailForUser(userId: number): Promise<string> {
// 假设这是一个异步获取用户邮箱的函数
}
getUserInfo(1)
.then(userInfo => console.log(userInfo))
.catch(error => console.error('Error fetching user info:', error));
详解:
- getUserInfo 函数也是 async 函数,它首先 await 调用 fetchUser 获取用户基本信息,然后 await 调用 fetchEmailForUser 获取用户的电子邮件地址。
- 这两个 await 表达式依次执行,形成异步操作的链式调用。整个过程如同同步代码一样直观易读。
- 最终,getUserInfo 函数返回一个 Promise,其 resolve 值为包含用户姓名和电子邮件的对象。
3. 并行 async/await 示例
async function fetchMultipleUsers(ids: number[]): Promise<{ id: number; name: string; age: number }[]> {
const promises = ids.map(async (id) => {
const user = await fetchUser(id);
return { id, name: user.name, age: user.age };
});
return await Promise.all(promises);
}
const userIds = [1, 2, 3];
fetchMultipleUsers(userIds)
.then(users => console.log(users))
.catch(error => console.error('Error fetching multiple users:', error));
详解:
- fetchMultipleUsers 函数接受一个用户 ID 数组,为每个 ID 创建一个异步任务(使用 Array.prototype.map 和箭头函数),每个任务负责获取单个用户的详细信息。
- 通过 Promise.all 方法并发执行所有异步任务,当所有任务都完成时,Promise.all 返回的 Promise 被解决,其结果是一个包含所有用户信息的对象数组。
- 外部代码同样通过 .then/.catch 或 await 消费返回的 Promise。
十、装饰器(Decorators)
TypeScript 装饰器(Decorators)是一种特殊类型的声明,它可以被附加到类、方法、访问器、属性或参数上,用于在编译时对它们进行元编程,实现诸如添加行为、修改或增强原有功能的目的。装饰器本身是一个函数,接收目标对象作为参数,并返回一个新的对象(或者原始对象的包装版本)。
以下是一些 TypeScript 装饰器的代码示例及其详细讲解:
1. 基础装饰器示例:日志记录装饰器
function log(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
const originalMethod = descriptor.value;
descriptor.value = function (...args: any[]) {
console.log(`Calling "${propertyKey}" with`, args);
const result = originalMethod.apply(this, args);
console.log(`"${propertyKey}" returned`, result);
return result;
};
return descriptor;
}
class MyClass {
@log
doSomething(param1: string, param2: number): boolean {
console.log('Inside doSomething');
return true;
}
}
const instance = new MyClass();
instance.doSomething('test', 42);
详解:
- 定义一个名为 log 的装饰器函数,它接收三个参数:目标类的原型(target)、被装饰的方法名(propertyKey)和该方法的描述符(PropertyDescriptor)。
- 装饰器内部保存原方法的引用,并创建一个新的方法替换原有的 value(即方法体)。新方法在调用原方法前后分别打印日志信息。
- 最后返回修改后的描述符,这样新的方法会被应用于被装饰的类方法。
- 在 MyClass 中,使用 @log 装饰器修饰 doSomething 方法。
- 当创建 MyClass 的实例并调用 doSomething 方法时,装饰器添加的日志功能会被自动触发。
2. 类装饰器示例:添加静态属性
function withStaticProperty(propertyName: string, value: any) {
return function (constructor: Function) {
constructor[propertyName] = value;
};
}
@withStaticProperty('version', '1.0.0')
class MyClass {}
console.log(MyClass.version); // 输出:"1.0.0"
详解:
- 定义一个工厂函数 withStaticProperty,它接收两个参数:要添加的静态属性名和属性值。这个工厂函数返回一个真正的装饰器函数。
- 装饰器函数接收类构造函数作为参数,并在其上直接添加指定的静态属性。
- 使用 @withStaticProperty 装饰器修饰 MyClass 类,传入静态属性名 version 和值 '1.0.0'。
- 类定义完成后,可以直接访问添加的静态属性 MyClass.version。
3. 属性装饰器示例:验证属性值类型
function validateType(type: string) {
return function (target: any, propertyKey: string) {
const originalValue = target[propertyKey];
const getter = function () {
return originalValue;
};
const setter = function (newValue: any) {
if (typeof newValue !== type) {
throw new TypeError(`Expected ${propertyKey} to be of type ${type}, got ${typeof newValue}`);
}
originalValue = newValue;
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter,
enumerable: true,
configurable: true,
});
};
}
class Person {
@validateType('string')
name: string = '';
}
const person = new Person();
person.name = 'John Doe'; // 正确设置
person.name = 123; // 抛出 TypeError
详解:
- 定义一个名为 validateType 的属性装饰器工厂函数,它接收一个期望的类型字符串作为参数,并返回一个装饰器函数。
- 装饰器函数接收目标对象(类的实例)和属性名,保存原属性值,并创建一对 getter/setter 方法替代原有的属性访问。
- 新的 setter 方法在设置属性值时检查新值的类型,若不符合预期则抛出 TypeError。
- 使用 Object.defineProperty 重定义目标对象上的属性,使用新创建的 getter/setter 替换原有的属性访问机制。
- 在 Person 类中,使用 @validateType('string') 装饰器修饰 name 属性。
- 创建 Person 类的实例,并尝试设置 name 属性。合法的赋值会被接受,非法的赋值会导致装饰器抛出错误。
总结
学习TypeScript是一项既有深度又有广度的任务,涉及类型系统、面向对象编程、函数式编程、模块化、工程化等多个方面。遵循上述学习路径,理论与实践相结合,逐步积累经验,您将能够在实际项目中游刃有余地运用TypeScript,提升开发效率与代码质量。同时,持续关注TypeScript的最新发展动态和技术趋势,保持知识