探索 TypeScript 技术世界:全面学习指南

引言

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的最新发展动态和技术趋势,保持知识

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

小码快撩

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值