引言
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)
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,提升开发效率与代码质量。同时,持续关注TypeScript的最新发展动态和技术趋势,保持知识
版权声明:
本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。
如若内容造成侵权、违法违规、事实不符,请将相关资料发送至xkadmin@xkablog.com进行投诉反馈,一经查实,立即处理!
转载请注明出处,原文链接:https://www.xkablog.com/typescriptbc/1171.html