温故而知新——Typescript

23 阅读33分钟

温故而知新——Typescript

TypeScript 的产生是为了应对 JavaScript 在开发大型和复杂应用程序时的不足,满足企业级开发的需求,并推动开发者社区向更高效、更高质量的代码编写方式发展。

什么是TypeScript(What)

TypeScript 是一种由微软开发的开源编程语言,它在 JavaScript 的基础上添加了可选的静态类型检查和其他功能扩展。简而言之,TypeScript 是 JavaScript 的超集,可以编译成纯 JavaScript,以便在任何支持 JavaScript 的环境中运行。

我们为什么要学习TypeScript (Why)

学习 TypeScript 有许多显著的优势,特别是在现代前端开发和大型项目中。以下是一些主要原因:

1. 提高代码质量和可维护性

  • 静态类型检查:TypeScript 的静态类型系统在开发阶段就能捕捉许多类型错误,减少了运行时错误。静态类型检查使代码更可靠、更健壮。
  • 可读性和可维护性:明确的类型声明和接口定义使代码更易于理解和维护,特别是在大型代码库中。

2. 增强开发效率

  • 智能提示和自动补全:TypeScript 提供更好的 IDE 支持,包括代码补全、导航和重构。这些功能可以显著提高开发效率。
  • 错误提前暴露:在编译阶段暴露错误比在运行时发现更高效,能够节省调试时间。

3. 支持现代 JavaScript 特性

  • 提前体验新特性:TypeScript 支持最新的 ECMAScript 标准和未来提案,让你可以提前使用现代 JavaScript 特性。
  • 兼容性:TypeScript 编译器会将新特性编译成兼容的 JavaScript,确保代码能够在所有主流浏览器和环境中运行。

4. 适用于大型项目和团队合作

  • 模块化开发:TypeScript 提供的模块系统和命名空间有助于组织代码,使得大型项目更加结构化。
  • 团队协作:静态类型和接口定义提供了清晰的 API 约定,减少了团队成员之间的误解和沟通成本。

5. 更好的开发工具支持

  • 强大的 IDE 集成:如 Visual Studio Code 等 IDE 对 TypeScript 提供了出色的支持,包括代码导航、错误提示和重构工具。
  • 类型定义文件:许多流行的 JavaScript 库都提供了 TypeScript 类型定义文件(例如 DefinitelyTyped 项目),使得在使用这些库时能够享受类型检查和智能提示。

6. 广泛的社区和生态系统

  • 成熟的社区:TypeScript 拥有活跃的社区和丰富的资源,包括教程、文档和开源项目。
  • 企业级应用:越来越多的企业采用 TypeScript 来构建和维护复杂的应用程序,使其成为一种值得学习的技能。

7. 后端开发的优势

  • 全栈开发:TypeScript 不仅适用于前端开发,在 Node.js 环境中也能很好地工作。这使得开发者可以在前后端使用同一种语言,简化了全栈开发的过程。

我们因该如何学习TypeScript(How)

1. 了解基础概念

  • 官方文档:从 TypeScript 官方文档入手,了解基本概念和语法。官方文档是最权威的资源,涵盖了所有语言特性和使用方法。
    • TypeScript 官方文档
  • 基础教程:学习一些基础教程,掌握 TypeScript 的基本语法和特性。
    • TypeScript 入门教程

2. 设置开发环境

  • 安装 TypeScript:通过 npm 安装 TypeScript 编译器。

    npm install -g typescript
    
  • 选择合适的编辑器:推荐使用 Visual Studio Code,因为它对 TypeScript 提供了很好的支持,包括代码补全、错误提示和调试功能。

3. 编写和运行简单的 TypeScript 程序

  • 创建一个简单项目:

    mkdir my-typescript-project
    cd my-typescript-project
    tsc --init
    
  • 编写 TypeScript 代码:创建一个 index.ts文件,编写一些简单的 TypeScript 代码。

    let message: string = 'Hello, TypeScript!';
    console.log(message);
    
  • 编译并运行:

    tsc
    node index.js
    

4. 学习核心概念

  • 基本类型:学习 TypeScript 的基本类型(如 string、number、boolean、array、tuple、enum 等)。
  • 接口和类型别名:学习如何使用接口和类型别名定义复杂数据结构。
  • 类和继承:了解 TypeScript 中的面向对象编程,包括类、继承和接口实现。
  • 泛型:学习如何使用泛型编写可重用的组件。
  • 模块和命名空间:掌握如何组织和模块化代码。

5. 进阶学习

  • 类型推断和类型守卫:学习 TypeScript 的类型推断机制和类型守卫(如 typeofinstanceof)。
  • 装饰器:了解装饰器的使用方法。
  • 高级类型:学习条件类型、映射类型和其他高级类型特性。

6. 实践项目

  • 小型项目:开始编写一些小型项目来练习 TypeScript,例如简单的 ToDo 应用、计算器等。
  • 与现有项目集成:尝试将 TypeScript 集成到现有的 JavaScript 项目中,逐步将代码库迁移到 TypeScript。

7. 学习和使用相关工具

  • 构建工具:学习如何使用构建工具(如 Webpack、Babel)与 TypeScript 配合工作。
  • Linting 和格式化:使用 ESLint 和 Prettier 来保持代码的一致性和质量。

8. 学习常用库和框架的 TypeScript 使用

  • React:学习如何在 React 项目中使用 TypeScript。
  • Vue3:Vue3使用 TypeScript 作为默认语言,可以通过官方文档深入学习。
  • Node.js:学习如何在 Node.js 环境中使用 TypeScript。

9. 参与社区和开源项目

  • 社区资源:参与 TypeScript 相关的社区活动,如论坛、讨论组和会议,获取最新的资讯和实践经验。
  • 开源贡献:参与开源项目,通过阅读和贡献代码,提升自己的 TypeScript 技能。

10. 持续学习

  • 博客和教程:关注 TypeScript 相关的博客和在线教程,保持对新特性和最佳实践的了解。
  • 在线课程:参加一些在线课程和培训,系统地提升 TypeScript 水平。

Typescript的基础知识

思维导图

TypeScript
├── 基础知识
│   ├── 安装和配置
│   │   ├── 安装 TypeScript
│   │   ├── TypeScript 编译器 (tsc)
│   │   ├── tsconfig.json
│   ├── 基本类型
│   │   ├── boolean
│   │   ├── number
│   │   ├── string
│   │   ├── array
│   │   ├── tuple
│   │   ├── enum
│   │   ├── any
│   │   ├── void
│   │   ├── nullundefined
│   │   ├── never
│   │   ├── unknown
│   ├── 类型断言
│   ├── 类型推断
│   ├── 联合类型和交叉类型
│   ├── 字面量类型
│   ├── 类型别名
├── 高级类型
│   ├── 接口
│   │   ├── 基本接口
│   │   ├── 可选属性
│   │   ├── 只读属性
│   │   ├── 函数类型
│   │   ├── 索引类型
│   │   ├── 接口继承
│   ├── 类
│   │   ├── 类的定义
│   │   ├── 继承
│   │   ├── 公共、私有和受保护的修饰符
│   │   ├── 静态成员
│   │   ├── 抽象类
│   ├── 函数
│   │   ├── 函数类型
│   │   ├── 可选参数和默认参数
│   │   ├── 剩余参数
│   │   ├── this 类型
│   │   ├── 重载
│   ├── 泛型
│   │   ├── 泛型函数
│   │   ├── 泛型接口
│   │   ├── 泛型类
│   │   ├── 泛型约束
│   │   ├── 在泛型中使用类类型
│   ├── 类型兼容性
│   ├── 类型保护
│   │   ├── typeof
│   │   ├── instanceof
│   │   ├── 自定义类型保护
├── 模块化
│   ├── 导入和导出
│   ├── 默认导出
│   ├── 命名空间
├── 装饰器
│   ├── 类装饰器
│   ├── 属性装饰器
│   ├── 方法装饰器
│   ├── 参数装饰器
├── 工具类型
│   ├── Partial
│   ├── Required
│   ├── Readonly
│   ├── Record
│   ├── Pick
│   ├── Omit
│   ├── Exclude
│   ├── Extract
│   ├── NonNullable
│   ├── ReturnType
│   ├── InstanceType
├── 高级主题
│   ├── 条件类型
│   ├── 映射类型
│   ├── 模板字面量类型
│   ├── 名字空间和模块
│   ├── 类型声明文件
│   │   ├── DefinitelyTyped
│   │   ├── 自定义类型声明
├── 配置和工具
│   ├── 构建工具集成
│   │   ├── Webpack
│   │   ├── Babel
│   ├── Linting 和格式化
│   │   ├── ESLint
│   │   ├── Prettier
│   ├── 测试
│   │   ├── Jest
│   │   ├── Mocha
├── 框架和库
│   ├── React
│   ├── Vue3
│   ├── Node.js

安装和配置

TypeScript 的安装和配置相对简单,可以通过以下几个步骤进行:

一、安装 TypeScript

  1. 使用 npm 安装

TypeScript 可以通过 npm(Node Package Manager)全局安装:

npm install -g typescript

安装完成后,可以使用 tsc 命令来查看 TypeScript 编译器是否安装成功:

tsc --version
  1. 在项目中安装

如果希望在项目中使用 TypeScript,可以将 TypeScript 安装为开发依赖项:

npm install --save-dev typescript

二、创建 TypeScript 项目

  1. 初始化项目

首先创建一个新的目录,并初始化 npm 项目:

mkdir my-typescript-project
cd my-typescript-project
npm init -y
  1. 创建 tsconfig.json 文件

tsconfig.json 文件是 TypeScript 项目的配置文件,用于指定编译选项。可以通过运行以下命令生成:

tsc --init

生成的 tsconfig.json 文件可能如下所示:

{
  "compilerOptions": {
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

三、配置 TypeScript

  1. 主要配置选项
  • target:指定 ECMAScript 目标版本(如 es5es6 等)。
  • module:指定模块系统(如 commonjses6amd 等)。
  • strict:启用所有严格类型检查选项。
  • esModuleInterop:启用对 ES 模块规范的兼容。
  • skipLibCheck:跳过所有 .d.ts 文件的类型检查。
  • forceConsistentCasingInFileNames:确保文件名大小写一致。

可以根据项目需求进一步配置 tsconfig.json 文件。

  1. 组织项目结构

建议将 TypeScript 源代码放在 src 目录中,并将编译后的 JavaScript 文件放在 dist 目录中。可以在 tsconfig.json 文件中配置如下:

{
  "compilerOptions": {
    "outDir": "./dist",  // 输出目录
    "rootDir": "./src",  // 源代码目录
    "target": "es6",
    "module": "commonjs",
    "strict": true,
    "esModuleInterop": true,
    "skipLibCheck": true,
    "forceConsistentCasingInFileNames": true
  },
  "include": ["src"]
}

四、编写和编译 TypeScript 代码

  1. 创建 TypeScript 文件

src 目录中创建一个 index.ts 文件:

// src/index.ts
let message: string = 'Hello, TypeScript!';
console.log(message);
  1. 编译 TypeScript 文件

运行以下命令来编译 TypeScript 文件:

tsc

这将在 dist 目录中生成编译后的 JavaScript 文件:

// dist/index.js
"use strict";
let message = 'Hello, TypeScript!';
console.log(message);
  1. 运行编译后的代码

使用 Node.js 运行编译后的 JavaScript 文件:

node dist/index.js

五、使用构建工具

在实际项目中,通常会使用构建工具(如 Webpack)来自动化构建过程。下面是一个简单的 Webpack 配置示例:

  1. 安装依赖
npm install --save-dev webpack webpack-cli ts-loader
  1. 创建 webpack.config.js 文件
// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.ts$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.ts', '.js'],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};
  1. 修改 package.json 文件

package.json 文件中添加构建脚本:

"scripts": {
  "build": "webpack"
}
  1. 构建项目

运行以下命令来构建项目:

npm run build

构建完成后,生成的 bundle.js 文件会在 dist 目录中。

通过以上步骤,你可以成功地安装和配置 TypeScript,并编写和编译 TypeScript 代码。同时,使用构建工具可以自动化构建过程,提高开发效率。

基本类型

TypeScript 的基本类型知识点主要包括:布尔值、数字、字符串、数组、元组、枚举、any、void、null 和 undefined、never、unknown 等。以下是每个类型的详细介绍及其使用方法:

1. 布尔值 (boolean)

布尔值表示逻辑上的真或假。

let isDone: boolean = false;

2. 数字 (number)

TypeScript 支持所有的数字类型,包括整数和浮点数。

let decimal: number = 6;
let hex: number = 0xf00d;
let binary: number = 0b1010;
let octal: number = 0o744;

3. 字符串 (string)

字符串用于表示文本数据,可以使用单引号、双引号或模板字符串。

let color: string = "blue";
let fullName: string = `John Doe`;
let age: number = 37;
let sentence: string = `Hello, my name is ${fullName}. I'll be ${age + 1} years old next month.`;

4. 数组 (Array)

数组可以包含相同类型的一组元素。定义数组有两种方式:

let list: number[] = [1, 2, 3];
let list: Array<number> = [1, 2, 3];

5. 元组 (Tuple)

元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。

let x: [string, number];
x = ["hello", 10]; // OK
// x = [10, "hello"]; // Error

6. 枚举 (enum)

枚举用于定义数值集合,便于定义和使用具有名字的常量集合。

enum Color {Red, Green, Blue}
let c: Color = Color.Green;

enum Color {Red = 1, Green, Blue}
let c: Color = Color.Green;

enum Color {Red = 1, Green = 2, Blue = 4}
let c: Color = Color.Green;

7. 任意值 (any)

any 类型用于表示任意类型的值,适用于不确定变量类型或允许多种类型时。

let notSure: any = 4;
notSure = "maybe a string instead";
notSure = false; // OK

8. 空值 (void)

void 类型通常用于函数没有返回值的情况。

function warnUser(): void {
  console.log("This is my warning message");
}

let unusable: void = undefined;

9. Null 和 Undefined

nullundefined 分别有自己的类型,通常用于初始化变量或处理可选参数。

let u: undefined = undefined;
let n: null = null;

10. Never

never 类型表示那些永不存在的值的类型,通常用于函数永远抛出错误或无限循环的情况。

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

function fail() {
  return error("Something failed");
}

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

11. 未知值 (unknown)

unknown 类型表示未知类型的值,适用于不确定类型的变量,需要进一步检查其类型。

let notSure: unknown = 4;
notSure = "maybe a string instead";

if (typeof notSure === "string") {
  console.log(notSure.toUpperCase()); // 确定是 string 类型后才可以调用 string 方法
}

类型断言

类型断言是 TypeScript 中的一个重要特性,它允许开发者在编译时明确指定一个值的类型,从而避免类型检查错误。类型断言可以提高代码的类型安全性和可维护性,但也需谨慎使用,以避免潜在的运行时错误。以下是对 TypeScript 类型断言的详细知识点介绍:

  1. 基本概念

类型断言的目的是告诉 TypeScript 编译器某个值的具体类型,而不进行实际的类型检查或数据转换。它只在编译时起作用,不影响运行时行为。

语法

  • 尖括号语法(不推荐用于 JSX)

    let someValue: any = "this is a string";
    let strLength: number = (<string>someValue).length;
    
  • as 语法(推荐)

    let someValue: any = "this is a string";
    let strLength: number = (someValue as string).length;
    
  1. 使用场景

2.1. 处理 any 类型

当值的类型被指定为 any 时,TypeScript 不会对其进行类型检查。可以使用类型断言将其转为更具体的类型。

let notSure: any = "Hello, world!";
let strLength: number = (notSure as string).length;

2.2. 从 DOM 获取元素

在处理 DOM 元素时,通常会用到类型断言来告诉 TypeScript 编译器获取的元素的具体类型。

let myCanvas = document.getElementById("main_canvas") as HTMLCanvasElement;
myCanvas.width = 500;
myCanvas.height = 500;

2.3. 将值从联合类型断言为具体类型

当一个值可以是多种类型之一时,类型断言可以帮助确定它的实际类型。

type StrOrNum = string | number;

function getLength(input: StrOrNum): number {
  if ((input as string).length !== undefined) {
    return (input as string).length;
  } else {
    return input.toString().length;
  }
}

console.log(getLength("Hello")); // 5
console.log(getLength(12345));   // 5
  1. 双重断言

双重断言(即先断言为 any,然后再断言为目标类型)可以绕过 TypeScript 的类型检查,但不推荐使用,因为这可能会掩盖潜在的错误。

let someValue: any = "this is a string";
let strLength: number = ((someValue as any) as string).length;
  1. 类型断言的限制
  • 不进行运行时检查:类型断言不会改变数据的实际类型,也不会进行运行时类型检查,只在编译阶段生效。
  • 可能隐藏错误:不正确的类型断言可能会导致运行时错误,因为编译器不会检测断言的正确性。
  • 慎用:应避免过度使用类型断言,尤其是当可以通过其他方式解决问题时,比如使用类型守卫或类型保护。
  1. 常见示例

示例 1:基本类型断言

let someValue: any = "Hello, TypeScript!";
let strLength: number = (someValue as string).length;
console.log(strLength); // 输出: 18

示例 2:从联合类型断言为具体类型

function getStringLength(value: string | number): number {
  if ((value as string).length !== undefined) {
    return (value as string).length;
  } else {
    return value.toString().length;
  }
}

console.log(getStringLength("Hello")); // 输出: 5
console.log(getStringLength(12345));   // 输出: 5

示例 3:DOM 元素类型断言

let inputElement = document.getElementById("username") as HTMLInputElement;
console.log(inputElement.value); // 获取 input 元素的值

示例 4:断言函数返回值

function getUserData(): any {
  return { name: "Alice", age: 25 };
}

let userData = getUserData() as { name: string; age: number };
console.log(userData.name); // Alice
console.log(userData.age);  // 25
  1. 类型守卫与类型保护

在一些情况下,类型断言不是最好的解决方案。类型守卫和类型保护可以在运行时检查类型,从而提供更安全的类型检查方式。

function isString(value: any): value is string {
  return typeof value === 'string';
}

let someValue: any = "This is a string";

if (isString(someValue)) {
  console.log(someValue.length); // TypeScript 知道 someValue 是 string
} else {
  console.log(someValue.toString()); // someValue 是其他类型
}

类型断言是 TypeScript 提供的一种灵活的工具,用于在编译时明确指定值的类型。虽然它能提高代码的灵活性和简洁性,但滥用可能导致潜在的错误,因此在使用时应谨慎,并尽量利用其他类型检查机制来确保类型安全。

类型推断

在 TypeScript 中,类型推断是一种强大的特性,能够自动推断变量、函数返回值和表达式的类型。理解类型推断的工作原理可以帮助你编写更简洁的代码,同时保持类型安全性。以下是关于 TypeScript 类型推断的详细知识点。

  1. 基本概念

类型推断指的是 TypeScript 编译器自动推断出某个值的类型,而无需显式地声明类型。TypeScript 在大多数情况下都能正确地推断出变量的类型。

  1. 变量类型推断

当你声明一个变量并初始化时,TypeScript 会根据初始化值的类型推断出该变量的类型。

let x = 10; // x 被推断为 number 类型
let y = "Hello"; // y 被推断为 string 类型

如果你没有给变量初始化,TypeScript 会将其推断为 any 类型。

let z; // z 被推断为 any 类型
z = 10;
z = "Hello";
  1. 函数返回值类型推断

当你编写函数时,如果函数有返回值而你没有显式地声明返回类型,TypeScript 会根据函数的返回语句自动推断返回值的类型。

function getNumber() {
  return 10; // 返回值被推断为 number 类型
}

function getMessage() {
  return "Hello, TypeScript"; // 返回值被推断为 string 类型
}
  1. 上下文类型

上下文类型是类型推断的另一种形式,发生在表达式的类型由上下文决定时。比如,事件处理函数参数的类型可以从事件绑定的上下文中推断出来。

window.onmousedown = function (event) {
  console.log(event.button); // event 被推断为 MouseEvent
};
  1. 最佳通用类型

当 TypeScript 需要从多个类型中推断出一个通用类型时,它会使用最佳通用类型算法。例如,当你创建一个包含多个不同类型元素的数组时,TypeScript 会尝试推断出一个最通用的类型。

let mixedArray = [1, "Hello", true]; // mixedArray 被推断为 (string | number | boolean)[]
  1. 上下文敏感函数

在某些情况下,TypeScript 会根据上下文推断出函数参数的类型。这在回调函数中非常常见。

let numbers = [1, 2, 3];
numbers.forEach((num) => {
  console.log(num); // num 被推断为 number 类型
});
  1. 对象字面量

在对象字面量中,TypeScript 会根据每个属性的值推断出对象的类型。

let person = {
  name: "Alice",
  age: 30
}; // person 被推断为 { name: string; age: number }
  1. 类型推断中的最佳实践
  • 尽量利用类型推断:在大多数情况下,TypeScript 的类型推断足够智能,可以推断出变量和函数的类型。显式声明类型有时是多余的。
  • 在复杂的情况下显式声明类型:如果类型推断不够明确或你希望提高代码的可读性和维护性,可以显式声明类型。
  • 利用接口和类型别名:当你的代码涉及复杂的对象或函数时,使用接口和类型别名可以帮助 TypeScript 更好地推断类型,并使代码更具可读性。
  1. 示例

示例 1:变量类型推断

let count = 10; // count 被推断为 number 类型
let greeting = "Hello, TypeScript"; // greeting 被推断为 string 类型

示例 2:函数返回值类型推断

function add(a: number, b: number) {
  return a + b; // 返回值被推断为 number 类型
}

function createUser(name: string, age: number) {
  return {
    name,
    age
  }; // 返回值被推断为 { name: string; age: number }
}

示例 3:上下文类型推断

document.addEventListener("click", (event) => {
  console.log(event.target); // event 被推断为 MouseEvent
});

示例 4:最佳通用类型推断

let mixedList = [1, "two", false]; // mixedList 被推断为 (string | number | boolean)[]

示例 5:对象字面量类型推断

let car = {
  brand: "Toyota",
  model: "Corolla",
  year: 2020
}; // car 被推断为 { brand: string; model: string; year: number }

类型推断是 TypeScript 的一个重要特性,它使代码更简洁,同时保持类型安全性。理解类型推断的工作原理,可以帮助你编写更清晰和可维护的代码。在需要时,可以显式声明类型以提高代码的可读性和明确性。

联合类型和交叉类型

在 TypeScript 中,联合类型和交叉类型是用于组合类型的两个强大工具。理解它们的使用场景和特性可以帮助你编写更灵活和类型安全的代码。以下是关于 TypeScript 联合类型和交叉类型的详细知识点。

联合类型(Union Types)

联合类型用于表示一个值可以是几种类型之一。使用 | 符号将多个类型连接起来。

  1. 基本语法
let value: string | number;
value = "Hello"; // OK
value = 42;      // OK
value = true;    // Error
  1. 函数参数和返回值

联合类型常用于函数参数和返回值,表示函数可以接受多种类型的参数或返回多种类型的值。

function format(input: string | number): string {
  if (typeof input === "string") {
    return `String: ${input}`;
  } else {
    return `Number: ${input}`;
  }
}

console.log(format("Hello")); // 输出: String: Hello
console.log(format(12345));   // 输出: Number: 12345
  1. 类型守卫(Type Guards)

为了安全地处理联合类型中的不同类型,TypeScript 提供了类型守卫,确保在特定的代码块中变量的类型是特定的。

function printId(id: string | number) {
  if (typeof id === "string") {
    console.log(`ID: ${id.toUpperCase()}`);
  } else {
    console.log(`ID: ${id.toFixed(2)}`);
  }
}
  1. 联合类型数组

联合类型也可以用于数组,表示数组中的元素可以是多种类型之一。

let mixedArray: (string | number)[] = ["Hello", 42, "World", 100];
交叉类型(Intersection Types)

交叉类型用于将多个类型合并为一个类型。使用 & 符号将多个类型连接起来。

  1. 基本语法
interface Person {
  name: string;
}

interface Employee {
  employeeId: number;
}

type EmployeePerson = Person & Employee;

let emp: EmployeePerson = {
  name: "John",
  employeeId: 1234
};
  1. 组合多个对象类型

交叉类型通常用于将多个对象类型合并为一个类型,以确保对象具有所有需要的属性。

interface A {
  a: string;
}

interface B {
  b: number;
}

type C = A & B;

let obj: C = { a: "Hello", b: 42 };
  1. 在类和接口中的应用

交叉类型可以用于类和接口,允许对象同时拥有多个类型的特性。

class Mixin1 {
  method1() {
    console.log("Method 1");
  }
}

class Mixin2 {
  method2() {
    console.log("Method 2");
  }
}

type Mixed = Mixin1 & Mixin2;

let mixedObject: Mixed = {
  method1: () => console.log("Method 1"),
  method2: () => console.log("Method 2"),
};

mixedObject.method1(); // 输出: Method 1
mixedObject.method2(); // 输出: Method 2
联合类型与交叉类型的比较
  • 联合类型表示一个值可以是多种类型中的一种。
  • 交叉类型表示一个值同时具有多种类型的所有特性。

联合类型和交叉类型的示例

示例 1:联合类型函数参数

function getLength(value: string | number): number {
  if (typeof value === "string") {
    return value.length;
  } else {
    return value.toString().length;
  }
}

console.log(getLength("Hello")); // 输出: 5
console.log(getLength(12345));   // 输出: 5

示例 2:交叉类型对象

interface Drivable {
  drive(): void;
}

interface Flyable {
  fly(): void;
}

type Vehicle = Drivable & Flyable;

let carPlane: Vehicle = {
  drive: () => console.log("Driving..."),
  fly: () => console.log("Flying...")
};

carPlane.drive(); // 输出: Driving...
carPlane.fly();   // 输出: Flying...

示例 3:联合类型数组

let arr: (string | number)[] = [1, "Hello", 2, "World"];
arr.push(3);
arr.push("TypeScript");
console.log(arr); // 输出: [1, "Hello", 2, "World", 3, "TypeScript"]

示例 4:类型守卫中的联合类型

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function getSmallPet(): Bird | Fish {
  return Math.random() > 0.5 ? { fly: () => console.log("Flying"), layEggs: () => console.log("Laying eggs") } : { swim: () =>     console.log("Swimming"), layEggs: () => console.log("Laying eggs") };
}

let pet = getSmallPet();

if ("fly" in pet) {
  pet.fly(); // Bird 类型
} else {
  pet.swim(); // Fish 类型
}
  • 联合类型|)用于表示一个值可以是多种类型之一,适合处理可能具有多种类型的变量。
  • 交叉类型&)用于将多个类型合并为一个类型,适合组合多个类型的特性以创建更复杂的类型。

理解并熟练使用联合类型和交叉类型,可以编写出更灵活、可扩展且类型安全的 TypeScript 代码。

字面量类型

字面量类型是 TypeScript 中的一种特性,它允许你将变量限制为特定的值。这些值可以是字符串、数字或布尔类型。通过字面量类型,可以定义非常具体和严格的类型,使得代码更具可读性和可维护性。以下是 TypeScript 字面量类型的详细知识点。

  1. 基本概念

字面量类型表示具体的值,而不仅仅是一个类型。你可以使用字符串、数字或布尔值来创建字面量类型。

字符串字面量类型

let direction: "north" | "south" | "east" | "west";
direction = "north"; // OK
direction = "south"; // OK
direction = "up";    // Error: Type '"up"' is not assignable to type '"north" | "south" | "east" | "west"'.

数字字面量类型

let oneOrTwo: 1 | 2;
oneOrTwo = 1; // OK
oneOrTwo = 2; // OK
oneOrTwo = 3; // Error: Type '3' is not assignable to type '1 | 2'.

布尔字面量类型

let yesOrNo: true | false;
yesOrNo = true;  // OK
yesOrNo = false; // OK
yesOrNo = "true"; // Error: Type '"true"' is not assignable to type 'true | false'.
  1. 字面量类型和联合类型

字面量类型通常与联合类型结合使用,定义变量可以接受的一组具体值。

type TrafficLight = "red" | "yellow" | "green";
let light: TrafficLight;

light = "red";    // OK
light = "yellow"; // OK
light = "blue";   // Error: Type '"blue"' is not assignable to type 'TrafficLight'.
  1. 字面量类型和函数参数

字面量类型可以用于函数参数,限制函数可以接受的值。

function move(direction: "up" | "down" | "left" | "right") {
  // Function implementation
}

move("up");    // OK
move("left");  // OK
move("forward"); // Error: Argument of type '"forward"' is not assignable to parameter of type '"up" | "down" | "left" | "right"'.
  1. 字面量类型和类型别名

类型别名可以用于定义字面量类型的组合,从而使代码更具可读性。

type CardinalDirection = "north" | "south" | "east" | "west";

function navigate(direction: CardinalDirection) {
  // Function implementation
}

navigate("north"); // OK
navigate("west");  // OK
navigate("up");    // Error: Argument of type '"up"' is not assignable to parameter of type 'CardinalDirection'.
  1. 字面量类型和枚举

字面量类型可以与枚举结合使用,以定义一组具体的常量值。

enum Color {
  Red = "red",
  Green = "green",
  Blue = "blue"
}

let color: Color;
color = Color.Red;   // OK
color = Color.Green; // OK
color = "yellow";    // Error: Type '"yellow"' is not assignable to type 'Color'.
  1. 字面量推断

当你将一个具体的值赋给一个变量时,TypeScript 可以自动推断该值的字面量类型。

let direction = "north"; // TypeScript 推断 direction 的类型为 "north"
direction = "south"; // Error: Type '"south"' is not assignable to type '"north"'.

要允许 direction 变量接受多个字面量类型,可以显式声明类型:

let direction: "north" | "south" | "east" | "west" = "north";
direction = "south"; // OK
  1. 常量上下文

使用 as const 断言可以将对象字面量的属性推断为字面量类型。

typescript复制代码const config = {
  width: 100,
  height: 200
} as const;

// config 的属性类型被推断为字面量类型
let width: 100 = config.width; // OK
let height: 200 = config.height; // OK
  1. 字面量类型的实际应用

示例 1:状态管理

type Status = "idle" | "loading" | "success" | "error";

function handleStatus(status: Status) {
  switch (status) {
    case "idle":
      // Handle idle state
      break;
    case "loading":
      // Handle loading state
      break;
    case "success":
      // Handle success state
      break;
    case "error":
      // Handle error state
      break;
  }
}

let currentStatus: Status = "loading";
handleStatus(currentStatus); // OK

示例 2:函数重载

function format(value: string): string;
function format(value: number): string;
function format(value: string | number): string {
  if (typeof value === "string") {
    return `String: ${value}`;
  } else {
    return `Number: ${value}`;
  }
}

console.log(format("Hello")); // 输出: String: Hello
console.log(format(123));     // 输出: Number: 123

示例 3:API 参数验证

type HTTPMethod = "GET" | "POST" | "PUT" | "DELETE";

function sendRequest(method: HTTPMethod, url: string) {
  // Function implementation
}

sendRequest("GET", "/api/data"); // OK
sendRequest("POST", "/api/data"); // OK
sendRequest("PATCH", "/api/data"); // Error: Argument of type '"PATCH"' is not assignable to parameter of type 'HTTPMethod'.
  • 字面量类型允许你将变量限制为特定的值,提高代码的类型安全性和可读性。
  • 联合类型类型别名可以结合字面量类型,定义更加灵活和具体的类型。
  • 常量上下文as const)可以将对象字面量的属性推断为字面量类型,确保类型安全。

通过理解和使用字面量类型,可以编写出更加严谨和可维护的 TypeScript 代码。

类型别名

类型别名(Type Aliases)是 TypeScript 中的一种机制,用于为类型创建新的名字。类型别名可以用来给任何类型起一个新的名字,包括基本类型、联合类型、交叉类型、对象类型、函数类型、元组类型等等。通过使用类型别名,可以提高代码的可读性和可维护性。以下是 TypeScript 类型别名的详细知识点。

  1. 基本语法

使用 type 关键字定义类型别名。

type Name = string;
type Age = number;
type Person = {
  name: Name;
  age: Age;
};
  1. 类型别名与基本类型

类型别名可以用来给基本类型(如字符串、数字、布尔值)起一个新的名字。

type Username = string;
type UserAge = number;

let username: Username = "Alice";
let userage: UserAge = 25;
  1. 类型别名与对象类型

类型别名可以用来定义对象类型,使得对象类型更具语义化和可读性。

type Address = {
  street: string;
  city: string;
  country: string;
};

type User = {
  name: string;
  age: number;
  address: Address;
};

let user: User = {
  name: "Bob",
  age: 30,
  address: {
    street: "123 Main St",
    city: "Springfield",
    country: "USA"
  }
};
  1. 类型别名与联合类型

类型别名可以用来定义联合类型,使得代码更简洁。

type ID = string | number;

let userId: ID;
userId = "abc123"; // OK
userId = 123;     // OK
userId = true;    // Error: Type 'boolean' is not assignable to type 'ID'.
  1. 类型别名与交叉类型

类型别名可以用来定义交叉类型,组合多个类型的特性。

type Person = {
  name: string;
};

type Employee = {
  employeeId: number;
};

type EmployeePerson = Person & Employee;

let employee: EmployeePerson = {
  name: "Alice",
  employeeId: 123
};
  1. 类型别名与函数类型

类型别名可以用来定义函数类型,使得函数类型的定义更简洁。

type Greet = (name: string) => string;

const greet: Greet = (name) => {
  return `Hello, ${name}!`;
};

console.log(greet("Alice")); // 输出: Hello, Alice!
  1. 类型别名与元组类型

类型别名可以用来定义元组类型,提高代码的可读性。

type Point = [number, number];

let point: Point = [10, 20];
  1. 泛型中的类型别名

类型别名可以与泛型结合使用,创建更加灵活的类型。

type Wrapper<T> = {
  value: T;
};

let numberWrapper: Wrapper<number> = { value: 123 };
let stringWrapper: Wrapper<string> = { value: "Hello" };
  1. 递归类型别名

类型别名可以递归地引用自身,定义递归数据结构,如树或链表。

type Tree<T> = {
  value: T;
  left?: Tree<T>;
  right?: Tree<T>;
};

let tree: Tree<number> = {
  value: 1,
  left: {
    value: 2
  },
  right: {
    value: 3,
    left: {
      value: 4
    },
    right: {
      value: 5
    }
  }
};
  1. 类型别名的局限性
  • 类型别名不能像接口一样被扩展。
  • 类型别名在复杂类型的定义中可能会导致类型检查错误更加难以理解。

示例和应用

示例 1:定义复杂对象类型

type ContactInfo = {
  email: string;
  phone: string;
};

type Employee = {
  name: string;
  age: number;
  contact: ContactInfo;
};

let employee: Employee = {
  name: "John Doe",
  age: 35,
  contact: {
    email: "john.doe@example.com",
    phone: "123-456-7890"
  }
};

示例 2:联合类型和类型别名

type Status = "success" | "error" | "loading";

function printStatus(status: Status) {
  console.log(status);
}

printStatus("success"); // OK
printStatus("failure"); // Error: Argument of type '"failure"' is not assignable to parameter of type 'Status'.

示例 3:类型别名与泛型

type Response<T> = {
  status: number;
  data: T;
};

let response: Response<string> = {
  status: 200,
  data: "OK"
};

let errorResponse: Response<{ message: string }> = {
  status: 500,
  data: {
    message: "Internal Server Error"
  }
};

示例 4:递归类型别名

type ListNode<T> = {
  value: T;
  next?: ListNode<T>;
};

let list: ListNode<number> = {
  value: 1,
  next: {
    value: 2,
    next: {
      value: 3
    }
  }
};
  • 类型别名通过使用 type 关键字,可以为任意类型起一个新的名字,提高代码的可读性和可维护性。
  • 基本类型对象类型联合类型交叉类型函数类型元组类型泛型递归类型都可以通过类型别名来定义。
  • 类型别名的局限性在于它们不能像接口一样被扩展,但它们在复杂类型的定义和代码简洁性方面具有很大优势。

理解并熟练使用类型别名,可以使你的 TypeScript 代码更加简洁、清晰和具备良好的类型安全性。

Typescript的高级类型

接口

接口(Interfaces)是 TypeScript 中用于定义数据结构的一种方式,它可以描述对象的形状、类的结构、函数类型等。接口在 TypeScript 中非常强大,提供了灵活的类型检查机制。以下是 TypeScript 接口的详细知识点。

  1. 基本接口定义

接口用于定义对象的形状。

interface Person {
  name: string;
  age: number;
}

let person: Person = {
  name: "Alice",
  age: 25
};
  1. 可选属性

接口中的属性可以标记为可选,用问号 ? 表示。

interface Person {
  name: string;
  age?: number;
}

let person1: Person = { name: "Alice" };
let person2: Person = { name: "Bob", age: 30 };
  1. 只读属性

接口中的属性可以标记为只读,用 readonly 关键字表示。

interface Person {
  readonly id: number;
  name: string;
}

let person: Person = { id: 1, name: "Alice" };
person.name = "Bob"; // OK
person.id = 2; // Error: Cannot assign to 'id' because it is a read-only property.
  1. 函数类型

接口可以定义函数类型,包括参数和返回值。

interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
  return source.search(subString) !== -1;
};
  1. 索引签名

索引签名允许定义具有动态键的对象,键名可以是 stringnumber 类型。

interface StringArray {
  [index: number]: string;
}

let myArray: StringArray = ["Alice", "Bob"];
interface Dictionary {
  [key: string]: string;
}

let myDict: Dictionary = { name: "Alice", age: "25" };
  1. 类类型

接口可以用于描述类的结构,包括属性和方法。

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();
  
  setTime(d: Date) {
    this.currentTime = d;
  }
}
  1. 扩展接口

接口可以通过 extends 关键字扩展其他接口,继承其属性和方法。

interface Shape {
  color: string;
}

interface Square extends Shape {
  sideLength: number;
}

let square: Square = { color: "blue", sideLength: 10 };
  1. 混合类型

接口可以描述一个对象既可以作为函数使用,也可以作为对象使用。

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = (function (start: number) {}) as Counter;
  counter.interval = 123;
  counter.reset = function () {};
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
  1. 接口继承类

接口可以继承类类型,这意味着接口将类的所有成员都抽象出来,但不包括其实现。

class Control {
  private state: any;
}

interface SelectableControl extends Control {
  select(): void;
}

class Button extends Control implements SelectableControl {
  select() {}
}

class TextBox extends Control implements SelectableControl {
  select() {}
}
  1. 泛型接口

接口可以使用泛型,以使其更加灵活和可重用。

interface Box<T> {
  contents: T;
}

let stringBox: Box<string> = { contents: "Hello" };
let numberBox: Box<number> = { contents: 123 };
  1. 接口与类型别名的对比

类型别名 type 也可以用于定义对象类型,但接口 interface 提供了更多的功能,如继承和实现。

type PersonType = {
  name: string;
  age: number;
};

interface PersonInterface {
  name: string;
  age: number;
}

let person1: PersonType = { name: "Alice", age: 25 };
let person2: PersonInterface = { name: "Bob", age: 30 };
  1. 示例和应用

示例 1:复杂对象类型

interface Address {
  street: string;
  city: string;
  country: string;
}

interface Person {
  name: string;
  address: Address;
}

let person: Person = {
  name: "Alice",
  address: {
    street: "123 Main St",
    city: "Springfield",
    country: "USA"
  }
};

示例 2:函数与对象类型结合

interface MathOperation {
  (a: number, b: number): number;
}

interface Calculator {
  add: MathOperation;
  subtract: MathOperation;
  multiply: MathOperation;
  divide: MathOperation;
}

let calculator: Calculator = {
  add: (a, b) => a + b,
  subtract: (a, b) => a - b,
  multiply: (a, b) => a * b,
  divide: (a, b) => a / b
};

console.log(calculator.add(1, 2)); // 输出: 3
console.log(calculator.divide(4, 2)); // 输出: 2
  • 基本接口定义 用于定义对象的形状。
  • 可选属性只读属性 提供了更灵活和安全的属性定义方式。
  • 函数类型 用于定义函数的类型。
  • 索引签名 允许定义动态键的对象。
  • 类类型 描述类的结构。
  • 扩展接口接口继承类 提供了接口继承的机制。
  • 混合类型 描述既可以作为函数使用,也可以作为对象使用的类型。
  • 泛型接口 提供了更灵活和可重用的接口定义。

理解并熟练应用这些接口特性,可以让你的 TypeScript 代码更加灵活、可读和类型安全。

在 TypeScript 中,类是面向对象编程的核心结构。类提供了创建对象的蓝图,并允许我们使用继承、多态等面向对象编程特性。以下是 TypeScript 中类的详细知识点。

  1. 基本类定义

使用 class 关键字定义类。类可以包含属性和方法。

class Person {
  name: string;
  age: number;

  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }

  greet() {
    console.log(`Hello, my name is ${this.name} and I am ${this.age} years old.`);
  }
}

let person = new Person("Alice", 25);
person.greet(); // Hello, my name is Alice and I am 25 years old.
  1. 继承

使用 extends 关键字继承另一个类,子类可以访问父类的属性和方法。

class Employee extends Person {
  employeeId: number;

  constructor(name: string, age: number, employeeId: number) {
    super(name, age); // 调用父类的构造函数
    this.employeeId = employeeId;
  }

  displayEmployeeInfo() {
    console.log(`Employee ID: ${this.employeeId}`);
  }
}

let employee = new Employee("Bob", 30, 123);
employee.greet(); // Hello, my name is Bob and I am 30 years old.
employee.displayEmployeeInfo(); // Employee ID: 123
  1. 访问修饰符

TypeScript 提供了 publicprivateprotected 三种访问修饰符来控制类成员的可见性。

  • public:默认修饰符,成员在任何地方可见。
  • private:成员仅在类内部可见。
  • protected:成员在类内部及其子类中可见。
class Person {
  public name: string;
  private age: number;
  protected address: string;

  constructor(name: string, age: number, address: string) {
    this.name = name;
    this.age = age;
    this.address = address;
  }

  public getAge() {
    return this.age;
  }

  protected getAddress() {
    return this.address;
  }
}

class Employee extends Person {
  constructor(name: string, age: number, address: string, public employeeId: number) {
    super(name, age, address);
  }

  displayEmployeeInfo() {
    console.log(`Employee ID: ${this.employeeId}, Address: ${this.getAddress()}`);
  }
}

let employee = new Employee("Alice", 25, "123 Main St", 123);
console.log(employee.name); // Alice
console.log(employee.getAge()); // 25
// console.log(employee.address); // Error: 'address' is protected
  1. 静态成员

使用 static 关键字定义静态成员,静态成员属于类而不是类的实例。

class Circle {
  static pi: number = 3.14;

  static calculateArea(radius: number) {
    return this.pi * radius * radius;
  }
}

console.log(Circle.pi); // 3.14
console.log(Circle.calculateArea(5)); // 78.5
  1. 抽象类

使用 abstract 关键字定义抽象类,抽象类不能被实例化,只能被继承。抽象类可以包含抽象方法,抽象方法在子类中必须实现。

abstract class Animal {
  abstract makeSound(): void;

  move() {
    console.log("Moving...");
  }
}

class Dog extends Animal {
  makeSound() {
    console.log("Woof! Woof!");
  }
}

let dog = new Dog();
dog.makeSound(); // Woof! Woof!
dog.move(); // Moving...
  1. 接口实现

类可以使用 implements 关键字实现一个或多个接口,必须实现接口中的所有属性和方法。

interface Drivable {
  start(): void;
  stop(): void;
}

class Car implements Drivable {
  start() {
    console.log("Car started.");
  }

  stop() {
    console.log("Car stopped.");
  }
}

let car = new Car();
car.start(); // Car started.
car.stop(); // Car stopped.
  1. 类的属性和方法的默认值

类的属性可以有默认值,方法可以有默认参数。

class Person {
  name: string = "Unknown";
  age: number = 0;

  constructor(name?: string, age?: number) {
    if (name) this.name = name;
    if (age) this.age = age;
  }

  greet(greeting: string = "Hello") {
    console.log(`${greeting}, my name is ${this.name}`);
  }
}

let person1 = new Person();
let person2 = new Person("Alice", 25);

person1.greet(); // Hello, my name is Unknown
person2.greet("Hi"); // Hi, my name is Alice
  1. 参数属性

参数属性简化了构造函数中的属性定义和初始化。

class Person {
  constructor(public name: string, private age: number) {}
}

let person = new Person("Alice", 25);
console.log(person.name); // Alice
// console.log(person.age); // Error: 'age' is private
  1. Getters 和 Setters

使用 getset 关键字定义访问器,控制属性的访问和修改。

class Person {
  private _name: string = "Unknown";

  get name(): string {
    return this._name;
  }

  set name(newName: string) {
    if (newName.length > 0) {
      this._name = newName;
    } else {
      console.log("Name cannot be empty");
    }
  }
}

let person = new Person();
console.log(person.name); // Unknown
person.name = "Alice";
console.log(person.name); // Alice
person.name = ""; // Name cannot be empty
  1. 类类型

接口可以描述类的类型,包括其构造函数和实例方法。

interface ClockInterface {
  currentTime: Date;
  setTime(d: Date): void;
}

class Clock implements ClockInterface {
  currentTime: Date = new Date();

  setTime(d: Date) {
    this.currentTime = d;
  }

  constructor(h: number, m: number) {}
}

let clock = new Clock(12, 30);
clock.setTime(new Date());
console.log(clock.currentTime);
  • 基本类定义:使用 class 关键字定义类。
  • 继承:使用 extends 关键字继承类。
  • 访问修饰符publicprivateprotected 用于控制成员的可见性。
  • 静态成员:使用 static 关键字定义静态属性和方法。
  • 抽象类:使用 abstract 关键字定义不能被实例化的类。
  • 接口实现:类可以实现接口,必须实现接口中的所有属性和方法。
  • 类的属性和方法的默认值:属性可以有默认值,方法可以有默认参数。
  • 参数属性:简化构造函数中的属性定义和初始化。
  • Getters 和 Setters:使用 getset 关键字定义访问器。
  • 类类型:接口可以描述类的类型,包括其构造函数和实例方法。

通过理解和掌握这些知识点,你可以编写更强大和灵活的 TypeScript 代码,充分利用面向对象编程的优势。

函数

在 TypeScript 中,函数是一个重要的组成部分。TypeScript 提供了对函数的强类型支持,使得函数的定义和使用更加安全和高效。以下是 TypeScript 函数的详细知识点:

  1. 函数定义

函数可以通过函数声明或函数表达式来定义。

函数声明:

function add(x: number, y: number): number {
  return x + y;
}

let result = add(2, 3); // 5

函数表达式:

let add = function (x: number, y: number): number {
  return x + y;
};

let result = add(2, 3); // 5
  1. 可选参数和默认参数

函数参数可以是可选的,也可以有默认值。

可选参数:

function buildName(firstName: string, lastName?: string): string {
  if (lastName) {
    return firstName + " " + lastName;
  } else {
    return firstName;
  }
}

let result1 = buildName("Bob"); // Bob
let result2 = buildName("Bob", "Adams"); // Bob Adams

默认参数:

function buildName(firstName: string, lastName = "Smith"): string {
  return firstName + " " + lastName;
}

let result1 = buildName("Bob"); // Bob Smith
let result2 = buildName("Bob", "Adams"); // Bob Adams
  1. 剩余参数

使用 ... 语法定义接受不定数量参数的函数。

function buildName(firstName: string, ...restOfName: string[]): string {
  return firstName + " " + restOfName.join(" ");
}

let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie"); // Joseph Samuel Lucas MacKinzie
  1. this 参数

TypeScript 允许指定 this 参数的类型,从而避免错误地使用 this

interface Card {
  suit: string;
  card: number;
}

interface Deck {
  suits: string[];
  cards: number[];
  createCardPicker(this: Deck): () => Card;
}

let deck: Deck = {
  suits: ["hearts", "spades", "clubs", "diamonds"],
  cards: Array(52),
  createCardPicker: function (this: Deck) {
    return () => {
      let pickedCard = Math.floor(Math.random() * 52);
      let pickedSuit = Math.floor(pickedCard / 13);

      return { suit: this.suits[pickedSuit], card: pickedCard % 13 };
    };
  }
};

let cardPicker = deck.createCardPicker();
let pickedCard = cardPicker();
console.log("card: " + pickedCard.card + " of " + pickedCard.suit);
  1. 重载

TypeScript 支持函数重载,即同一个函数名可以有多个不同的调用签名。

function pickCard(x: { suit: string; card: number }[]): number;
function pickCard(x: number): { suit: string; card: number };
function pickCard(x: any): any {
  if (typeof x === "object") {
    return Math.floor(Math.random() * x.length);
  } else if (typeof x === "number") {
    let suit = Math.floor(x / 13);
    return { suit: ["hearts", "spades", "clubs", "diamonds"][suit], card: x % 13 };
  }
}

let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];
let pickedCard1 = myDeck[pickCard(myDeck)];
console.log("card: " + pickedCard1.card + " of " + pickedCard1.suit);

let pickedCard2 = pickCard(15);
console.log("card: " + pickedCard2.card + " of " + pickedCard2.suit);
  1. 箭头函数

箭头函数使用 => 语法,并且不绑定 this

let add = (x: number, y: number): number => {
  return x + y;
};

console.log(add(2, 3)); // 5
  1. 函数类型

可以通过接口或类型别名定义函数类型。

// 使用接口定义函数类型
interface SearchFunc {
  (source: string, subString: string): boolean;
}

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
  return source.search(subString) !== -1;
};

// 使用类型别名定义函数类型
type SearchFunc = (source: string, subString: string) => boolean;

let mySearch: SearchFunc;
mySearch = function (source: string, subString: string) {
  return source.search(subString) !== -1;
};
  1. 调用签名

接口可以拥有调用签名,用来定义函数类型。

interface StringArray {
  [index: number]: string;
}

interface FuncWithCallSignature {
  (arg: number): string;
}

let myFunc: FuncWithCallSignature;
myFunc = function (arg: number): string {
  return arg.toString();
};

console.log(myFunc(123)); // "123"
  1. 可调用类型

可以通过接口定义既可调用又有属性的对象类型。

interface Counter {
  (start: number): string;
  interval: number;
  reset(): void;
}

function getCounter(): Counter {
  let counter = (function (start: number) {} as Counter);
  counter.interval = 123;
  counter.reset = function () {};
  return counter;
}

let c = getCounter();
c(10);
c.reset();
c.interval = 5.0;
  1. 函数泛型

函数可以使用泛型,使其更加灵活和可重用。

function identity<T>(arg: T): T {
  return arg;
}

let output1 = identity<string>("myString"); // 手动指定类型
let output2 = identity<number>(123); // 手动指定类型
let output3 = identity("myString"); // 类型推断
let output4 = identity(123); // 类型推断
  • 函数定义:通过函数声明和函数表达式定义函数。
  • 可选参数和默认参数:支持定义可选参数和默认参数。
  • 剩余参数:使用 ... 语法定义接受不定数量参数的函数。
  • this 参数:指定 this 参数类型,避免错误地使用 this
  • 重载:支持函数重载,即同一个函数名可以有多个不同的调用签名。
  • 箭头函数:使用 => 语法定义不绑定 this 的函数。
  • 函数类型:通过接口或类型别名定义函数类型。
  • 调用签名:接口可以拥有调用签名,用来定义函数类型。
  • 可调用类型:通过接口定义既可调用又有属性的对象类型。
  • 函数泛型:函数可以使用泛型,使其更加灵活和可重用。

通过理解和掌握这些知识点,你可以编写出类型安全、高效且灵活的 TypeScript 函数。

泛型

泛型是 TypeScript 提供的一种用于创建可重用组件的特性。它使得组件不仅能够支持当前的数据类型,还能够支持未来的数据类型,从而提高代码的灵活性和可复用性。以下是 TypeScript 泛型的详细知识点。

  1. 基本使用

泛型通过在函数名、接口名或类名后面加上尖括号 <T> 来声明。T 是泛型变量,可以代表任何类型。

function identity<T>(arg: T): T {
  return arg;
}

let output1 = identity<string>("myString"); // 指定类型
let output2 = identity<number>(123); // 指定类型
let output3 = identity("myString"); // 类型推断
let output4 = identity(123); // 类型推断
  1. 泛型变量

泛型允许使用多个类型变量。

function merge<T, U>(arg1: T, arg2: U): T & U {
  return { ...arg1, ...arg2 };
}

let merged = merge({ name: "Alice" }, { age: 25 });
console.log(merged.name); // Alice
console.log(merged.age); // 25
  1. 泛型接口

接口可以使用泛型来定义。

interface GenericIdentityFn<T> {
  (arg: T): T;
}

function identity<T>(arg: T): T {
  return arg;
}

let myIdentity: GenericIdentityFn<number> = identity;
console.log(myIdentity(123)); // 123
  1. 泛型类

类可以使用泛型来定义。

class GenericNumber<T> {
  zeroValue: T;
  add: (x: T, y: T) => T;
}

let myGenericNumber = new GenericNumber<number>();
myGenericNumber.zeroValue = 0;
myGenericNumber.add = (x, y) => x + y;
console.log(myGenericNumber.add(1, 2)); // 3
  1. 泛型约束

可以使用 extends 关键字对泛型进行约束,限制其类型。

interface Lengthwise {
  length: number;
}

function loggingIdentity<T extends Lengthwise>(arg: T): T {
  console.log(arg.length); // 确保存在 length 属性
  return arg;
}

loggingIdentity({ length: 10, value: "Hello" }); // OK
loggingIdentity(3); // Error: Argument of type '3' is not assignable to parameter of type 'Lengthwise'.
  1. 在泛型约束中使用类型参数

在泛型约束中,可以使用另一个类型参数。

function getProperty<T, K extends keyof T>(obj: T, key: K) {
  return obj[key];
}

let person = { name: "Alice", age: 25 };
console.log(getProperty(person, "name")); // Alice
console.log(getProperty(person, "age")); // 25
  1. 泛型默认类型

在 TypeScript 2.3 及更高版本中,泛型可以有默认类型。

function createArray<T = string>(length: number, value: T): T[] {
  return Array(length).fill(value);
}

let stringArray = createArray(3, "Hello");
let numberArray = createArray<number>(3, 42);
  1. 泛型工具类型

TypeScript 提供了一些实用的泛型工具类型,用于对类型进行变换和操作。

Partial

将类型的所有属性变为可选。

interface Person {
  name: string;
  age: number;
}

let partialPerson: Partial<Person> = {};
partialPerson.name = "Alice";

Readonly

将类型的所有属性变为只读。

let readonlyPerson: Readonly<Person> = { name: "Alice", age: 25 };
// readonlyPerson.age = 30; // Error: Cannot assign to 'age' because it is a read-only property.

Record

创建一个新的类型,将属性键 K 映射到属性值 T。

type StringNumberPair = Record<string, number>;
let pair: StringNumberPair = { key1: 10, key2: 20 };

Pick

从类型 T 中选择一组属性 K。

type PersonName = Pick<Person, "name">;
let personName: PersonName = { name: "Alice" };

Omit

从类型 T 中排除一组属性 K。

type PersonWithoutAge = Omit<Person, "age">;
let personWithoutAge: PersonWithoutAge = { name: "Alice" };
  1. 泛型与类型推断

TypeScript 的类型推断可以在使用泛型时自动推断出类型。

function identity<T>(arg: T): T {
  return arg;
}

let output = identity("myString"); // TypeScript 自动推断 T 为 string
  1. 泛型参数的默认值

TypeScript 3.4 引入了泛型参数的默认值。

interface Box<T = string> {
  contents: T;
}

let box: Box = { contents: "Hello" }; // 默认类型为 string
  1. 泛型工厂函数

可以使用泛型创建工厂函数。

function createInstance<T>(ctor: new () => T): T {
  return new ctor();
}

class Person {
  name: string = "Alice";
}

let person = createInstance(Person);
console.log(person.name); // Alice
  • 基本使用:通过在函数名、接口名或类名后面加上尖括号 <T> 声明泛型。
  • 泛型变量:允许使用多个类型变量。
  • 泛型接口:接口可以使用泛型来定义。
  • 泛型类:类可以使用泛型来定义。
  • 泛型约束:通过 extends 关键字对泛型进行约束。
  • 在泛型约束中使用类型参数:在约束中使用另一个类型参数。
  • 泛型默认类型:为泛型提供默认类型。
  • 泛型工具类型:如 PartialReadonlyRecordPickOmit 等。
  • 泛型与类型推断:TypeScript 自动推断泛型类型。
  • 泛型参数的默认值:提供泛型参数的默认值。
  • 泛型工厂函数:使用泛型创建工厂函数。

掌握这些泛型的知识点,可以让你编写出更灵活和可复用的 TypeScript 代码。

类型兼容性

TypeScript 的类型兼容性是一个重要的特性,帮助我们理解不同类型之间如何互相兼容。类型兼容性系统在 TypeScript 中扮演了关键角色,使得代码能够更好地符合预期的行为并保持类型安全。以下是 TypeScript 类型兼容性的详细知识点:

  1. 结构性子类型

TypeScript 的类型系统是基于结构的,也就是说,一个类型的兼容性主要取决于它的结构(即它包含了哪些属性和方法)。这和传统的名义类型(类型兼容性取决于类型名或标识符)不同。只要两个类型的结构兼容,它们就可以互相赋值。

interface Person {
  name: string;
}

interface Employee {
  name: string;
  employeeId: number;
}

let person: Person = { name: "Alice" };
let employee: Employee = { name: "Bob", employeeId: 123 };

person = employee; // OK,因为 Employee 的结构包含了 Person 的结构
  1. 函数兼容性

函数类型的兼容性取决于参数列表和返回类型。对于函数类型,TypeScript 使用的是协变(covariant)和逆变(contravariant)概念。

  • 协变(返回值的兼容性):返回类型可以更具体。
  • 逆变(参数的兼容性):参数类型可以更宽泛。
function greet(name: string): void {
  console.log("Hello " + name);
}

let greetPerson: (name: string) => void = greet; // OK,因为 greetPerson 的签名与 greet 一致

// 参数兼容性
let greetAny: (name: any) => void = greet; // OK,因为 greet 的参数比 greetAny 更具体

// 返回值兼容性
function greetPerson(name: string): string {
  return "Hello " + name;
}

let greetVoid: (name: string) => void = greetPerson; // OK,因为 greetPerson 的返回值更具体(void 是所有返回值的子类型)
  1. 类的兼容性

在类的兼容性中,主要考虑的是实例的类型兼容性,即类的实例是否可以互相赋值。类的兼容性与类的结构(包括属性和方法)有关。

class Animal {
  name: string;
}

class Dog extends Animal {
  bark() {}
}

let animal: Animal = new Dog(); // OK,因为 Dog 继承了 Animal
// let dog: Dog = new Animal(); // Error: Animal is not assignable to Dog
  1. 接口的兼容性

接口之间的兼容性取决于它们的结构。如果一个接口包含了另一个接口的所有属性和方法,那么它们是兼容的。

interface A {
  x: number;
}

interface B {
  x: number;
  y: number;
}

let a: A = { x: 1 };
let b: B = { x: 1, y: 2 };

a = b; // OK,因为 B 包含了 A 的所有属性
  1. 泛型的兼容性

泛型的兼容性遵循特定的规则,主要基于实际类型参数的兼容性。

interface Box<T> {
  value: T;
}

let box1: Box<number> = { value: 1 };
let box2: Box<any> = box1; // OK,因为 Box<number> 是 Box<any> 的子类型
  1. 类型别名的兼容性

类型别名和接口在大多数情况下是互换的,类型别名也支持联合类型和交叉类型等复杂的结构。

type A = { x: number };
type B = { x: number; y: number };

let a: A = { x: 1 };
let b: B = { x: 1, y: 2 };

a = b; // OK,因为 B 包含了 A 的所有属性
  1. 字面量类型的兼容性

字面量类型的兼容性是比较严格的。子类型字面量类型可以赋值给父类型,但反之则不行。

type Color = "red" | "green" | "blue";
let color: Color = "red"; // OK
color = "green"; // OK
// color = "yellow"; // Error: Type '"yellow"' is not assignable to type 'Color'.
  1. 联合类型和交叉类型的兼容性
  • 联合类型:联合类型的变量可以是联合中的任意一个类型。只有当所有可能的类型都可以接受时,才能进行赋值。
type A = { x: number };
type B = { y: number };

let a: A | B = { x: 1 };
a = { y: 2 }; // OK
  • 交叉类型:交叉类型合并了多个类型的属性,兼容性是基于交叉类型的所有属性。
type A = { x: number };
type B = { y: number };

let ab: A & B = { x: 1, y: 2 };
  1. 类型推断与兼容性

类型推断会影响类型兼容性。TypeScript 在许多情况下会自动推断类型,并在类型赋值时使用这些推断的类型进行兼容性检查。

let a = { x: 1, y: 2 };
let b: { x: number } = a; // OK,因为 a 的结构包含了 b 的结构
  1. 类型守卫与兼容性

类型守卫(如 typeofinstanceof)帮助 TypeScript 确定在运行时的类型,从而使类型兼容性检查更加准确。

function process(value: string | number) {
  if (typeof value === "string") {
    // `value` 是 string
  } else {
    // `value` 是 number
  }
}
  • 结构性子类型:类型兼容性主要基于类型的结构。
  • 函数兼容性:基于参数和返回值的兼容性。
  • 类的兼容性:类实例的类型兼容性基于类的结构。
  • 接口的兼容性:接口之间的兼容性基于属性和方法的结构。
  • 泛型的兼容性:基于实际类型参数的兼容性。
  • 类型别名的兼容性:与接口类似,基于结构。
  • 字面量类型的兼容性:子类型可以赋值给父类型,但反之不行。
  • 联合类型和交叉类型的兼容性:联合类型中的任意类型可以接受,交叉类型合并多个类型的属性。
  • 类型推断与兼容性:自动推断的类型影响类型兼容性。
  • 类型守卫与兼容性:运行时的类型守卫帮助确定类型兼容性。

掌握这些类型兼容性的知识点,能够帮助你更好地理解 TypeScript 类型系统,编写更加健壮和类型安全的代码。

类型保护

在 TypeScript 中,类型保护(Type Guards)是一种确保变量在某个代码块中具有特定类型的技术。通过类型保护,可以安全地访问变量的属性和方法,避免类型错误。以下是 TypeScript 类型保护的详细知识点:

  1. 类型保护的基本概念

类型保护用于缩小变量的类型范围,使 TypeScript 编译器能够更准确地推断变量的类型。常见的类型保护包括 typeofinstanceof、自定义类型保护函数等。

  1. typeof 类型保护

typeof 运算符用于检查一个变量的基本类型,如 stringnumberbooleansymbolundefinedobject

function padLeft(value: string, padding: string | number) {
  if (typeof padding === "number") {
    return Array(padding + 1).join(" ") + value;
  }
  if (typeof padding === "string") {
    return padding + value;
  }
  throw new Error(`Expected string or number, got ${typeof padding}.`);
}
  1. instanceof 类型保护

instanceof 运算符用于检查一个对象是否是另一个对象的实例,通常用于类的实例。

class Bird {
  fly() {
    console.log("Flying");
  }
}

class Fish {
  swim() {
    console.log("Swimming");
  }
}

function move(animal: Bird | Fish) {
  if (animal instanceof Bird) {
    animal.fly(); // 确定是 Bird 类型
  } else if (animal instanceof Fish) {
    animal.swim(); // 确定是 Fish 类型
  }
}
  1. in 操作符

in 操作符用于检查对象上是否存在某个属性。

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function getAnimal(): Bird | Fish {
  // ...
}

let pet = getAnimal();

if ("fly" in pet) {
  pet.fly();
} else {
  pet.swim();
}
  1. 用户自定义类型保护

用户自定义类型保护使用自定义函数来细化变量的类型。这些函数返回一个类型谓词,即 paramName is Type

interface Bird {
  fly(): void;
  layEggs(): void;
}

interface Fish {
  swim(): void;
  layEggs(): void;
}

function isFish(pet: Bird | Fish): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function move(animal: Bird | Fish) {
  if (isFish(animal)) {
    animal.swim(); // 确定是 Fish 类型
  } else {
    animal.fly(); // 确定是 Bird 类型
  }
}
  1. 联合类型和类型保护

联合类型表示一个变量可以是多种类型之一。通过类型保护,可以在运行时确定变量的具体类型。

type NetworkState = 
  | { state: "loading" }
  | { state: "failed"; code: number }
  | { state: "success"; response: string };

function handleState(state: NetworkState) {
  if (state.state === "loading") {
    console.log("Loading...");
  } else if (state.state === "failed") {
    console.log(`Error code: ${state.code}`);
  } else if (state.state === "success") {
    console.log(`Response: ${state.response}`);
  }
}
  1. 交叉类型和类型保护

交叉类型表示一个变量同时具有多种类型的属性。类型保护有助于确保访问属性时的类型安全。

type ErrorHandling = { success: boolean; error?: { message: string } };
type ArtworksData = { artworks: { title: string }[] };

type ArtworksResponse = ArtworksData & ErrorHandling;

function handleResponse(response: ArtworksResponse) {
  if (response.success) {
    console.log(response.artworks);
  } else {
    console.log(response.error?.message);
  }
}
  1. 类型保护与类型别名

类型别名可以使类型定义更加简洁。使用类型别名时,类型保护同样适用。

type Pet = Bird | Fish;

function isBird(pet: Pet): pet is Bird {
  return (pet as Bird).fly !== undefined;
}

function isFish(pet: Pet): pet is Fish {
  return (pet as Fish).swim !== undefined;
}

function handlePet(pet: Pet) {
  if (isBird(pet)) {
    pet.fly();
  } else if (isFish(pet)) {
    pet.swim();
  }
}
  1. 类型保护与类型断言

类型断言用于显式地告诉编译器变量的具体类型。类型保护可以减少类型断言的需求,提升代码的类型安全性。

function getLength(input: string | number): number {
  if (typeof input === "string") {
    return input.length;
  } else {
    return input.toString().length;
  }
}
  1. 类型保护与类型推断

类型保护可以帮助 TypeScript 更好地推断变量的类型,使得代码更加精确和安全。

type Shape = Circle | Square;

interface Circle {
  kind: "circle";
  radius: number;
}

interface Square {
  kind: "square";
  sideLength: number;
}

function getArea(shape: Shape): number {
  if (shape.kind === "circle") {
    return Math.PI * shape.radius ** 2;
  } else {
    return shape.sideLength ** 2;
  }
}
  • typeof 类型保护:用于检查基本类型。
  • instanceof 类型保护:用于检查对象是否是某个类的实例。
  • in 操作符:用于检查对象上是否存在某个属性。
  • 用户自定义类型保护:通过自定义函数实现类型保护。
  • 联合类型和类型保护:确定联合类型的具体类型。
  • 交叉类型和类型保护:确保访问交叉类型属性时的类型安全。
  • 类型保护与类型别名:简化类型定义,提高类型保护的可读性。
  • 类型保护与类型断言:减少类型断言的需求,提升类型安全性。
  • 类型保护与类型推断:帮助 TypeScript 更好地推断变量的类型。

通过理解和使用这些类型保护技术,可以在 TypeScript 中编写更加类型安全和健壮的代码。

Typescript的模块化

TypeScript 的模块化系统使得代码更容易组织、维护和重用。模块是将代码封装在独立的文件或单元中,并通过导入和导出机制来管理依赖关系。以下是 TypeScript 模块化的详细知识点:

  1. 模块的基本概念

模块是一个封闭的代码块,其中包含定义、逻辑和接口。模块通过导出(export)和导入(import)机制与外界进行通信。

  1. 导出与导入

导出

可以导出变量、函数、类、接口、类型等。

// 导出变量
export const pi = 3.14;

// 导出函数
export function add(x: number, y: number): number {
  return x + y;
}

// 导出类
export class Circle {
  constructor(public radius: number) {}
}

// 导出接口
export interface Shape {
  area(): number;
}

// 导出类型
export type Point = { x: number; y: number };

导入

可以使用 import 关键字从其他模块中导入内容。

import { pi, add, Circle, Shape, Point } from './module';

// 使用导入的内容
console.log(pi); // 3.14
console.log(add(2, 3)); // 5
const circle = new Circle(5);
console.log(circle.radius); // 5
  1. 默认导出与导入

模块可以有一个默认导出,使用 export default 语法。

// module.ts
export default function subtract(x: number, y: number): number {
  return x - y;
}

// main.ts
import subtract from './module';
console.log(subtract(5, 3)); // 2
  1. 重命名导出与导入

可以使用 as 关键字来重命名导出的内容。

// module.ts
export const pi = 3.14;
export function add(x: number, y: number): number {
  return x + y;
}

// main.ts
import { pi as PI, add as addition } from './module';
console.log(PI); // 3.14
console.log(addition(2, 3)); // 5
  1. 导入所有内容

可以使用 import * as 语法导入模块中的所有导出内容。

// module.ts
export const pi = 3.14;
export function add(x: number, y: number): number {
  return x + y;
}

// main.ts
import * as math from './module';
console.log(math.pi); // 3.14
console.log(math.add(2, 3)); // 5
  1. 命名空间导入与导出

命名空间用于将一组相关的代码组织在一起,避免命名冲突。

// module.ts
export namespace Geometry {
  export class Circle {
    constructor(public radius: number) {}
  }

  export class Square {
    constructor(public side: number) {}
  }
}

// main.ts
import { Geometry } from './module';
const circle = new Geometry.Circle(5);
const square = new Geometry.Square(10);
console.log(circle.radius); // 5
console.log(square.side); // 10
  1. 混合导出

模块可以同时使用命名导出和默认导出。

// module.ts
export const pi = 3.14;
export function add(x: number, y: number): number {
  return x + y;
}
export default class Calculator {
  add(x: number, y: number): number {
    return x + y;
  }
}

// main.ts
import Calculator, { pi, add } from './module';
console.log(pi); // 3.14
console.log(add(2, 3)); // 5
const calculator = new Calculator();
console.log(calculator.add(2, 3)); // 5
  1. 模块解析

TypeScript 有两种模块解析策略:Node 和 Classic。默认使用 Node 策略,解析模块时按照 Node.js 的方式查找模块。

模块解析配置

tsconfig.json 文件中,可以通过 moduleResolution 配置模块解析策略。

{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}
  1. ES 模块与 CommonJS 模块

TypeScript 支持 ES 模块(ECMAScript Modules)和 CommonJS 模块。

ES 模块

使用 importexport 语法,适用于前端和现代 Node.js 项目。

// ES 模块示例
export const pi = 3.14;
import { pi } from './module';

CommonJS 模块

使用 requiremodule.exports 语法,主要用于传统的 Node.js 项目。

// CommonJS 模块示例
module.exports = { pi: 3.14 };
const { pi } = require('./module');
  1. 动态导入

动态导入(Dynamic Import)允许在运行时按需加载模块。

// 动态导入示例
async function loadModule() {
  const module = await import('./module');
  console.log(module.pi); // 3.14
}
loadModule();
  1. 模块的声明文件

模块的声明文件(Declaration File)用于为 JavaScript 库或模块提供类型定义。

创建模块声明文件

创建一个 .d.ts 文件,定义模块的类型。

// module.d.ts
declare module 'some-library' {
  export function someFunction(): void;
}

// main.ts
import { someFunction } from 'some-library';
someFunction();

引用声明文件

tsconfig.json 文件中,通过 typeRootstypes 配置引用声明文件。

{
  "compilerOptions": {
    "typeRoots": ["./node_modules/@types", "./typings"]
  }
}
  • 导出与导入:使用 export 导出模块内容,使用 import 导入其他模块内容。
  • 默认导出与导入:使用 export default 导出默认内容,使用不加花括号的 import 导入。
  • 重命名导出与导入:使用 as 关键字重命名导出或导入的内容。
  • 导入所有内容:使用 import * as 导入模块的所有导出内容。
  • 命名空间导入与导出:使用 namespace 组织相关代码,避免命名冲突。
  • 混合导出:模块可以同时使用命名导出和默认导出。
  • 模块解析:TypeScript 支持 Node 和 Classic 模块解析策略。
  • ES 模块与 CommonJS 模块:支持现代和传统的模块系统。
  • 动态导入:按需加载模块,提升性能。
  • 模块的声明文件:为 JavaScript 库或模块提供类型定义。

掌握这些模块化的知识点,能够帮助你在 TypeScript 中更好地组织代码,提升代码的可维护性和可重用性。

Typescript的装饰器

TypeScript 的装饰器(Decorators)是一种特殊的声明,它能够附加到类、方法、访问器、属性或参数上。装饰器是一种用于修改类及其成员行为的语法糖。以下是 TypeScript 装饰器的详细知识点:

  1. 装饰器的基本概念

装饰器是一种可以附加到类声明、方法、访问器、属性或参数上并可以修改其行为的特殊类型的声明。装饰器使用 @expression 语法。

  1. 启用装饰器

tsconfig.json 中需要启用 experimentalDecorators 选项:

{
  "compilerOptions": {
    "experimentalDecorators": true
  }
}
  1. 装饰器类型

TypeScript 支持以下几种装饰器:

  • 类装饰器
  • 方法装饰器
  • 访问器装饰器
  • 属性装饰器
  • 参数装饰器
  1. 类装饰器

类装饰器应用于类构造函数,可以用来监视、修改或替换类定义。类装饰器接受一个参数,即被装饰的类。

function sealed(constructor: Function) {
  Object.seal(constructor);
  Object.seal(constructor.prototype);
}

@sealed
class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return `Hello, ${this.greeting}`;
  }
}
  1. 方法装饰器

方法装饰器应用于方法,可以用来监视、修改或替换方法定义。方法装饰器接受三个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 方法的名字。
  • 方法的属性描述符。
function enumerable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.enumerable = value;
  };
}

class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }

  @enumerable(false)
  greet() {
    return `Hello, ${this.greeting}`;
  }
}
  1. 访问器装饰器

访问器装饰器应用于访问器(getter 或 setter),可以用来监视、修改或替换访问器的定义。访问器装饰器接受三个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 访问器的名字。
  • 访问器的属性描述符。
function configurable(value: boolean) {
  return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
    descriptor.configurable = value;
  };
}

class Point {
  private _x: number;
  private _y: number;

  constructor(x: number, y: number) {
    this._x = x;
    this._y = y;
  }

  @configurable(false)
  get x() {
    return this._x;
  }

  @configurable(false)
  get y() {
    return this._y;
  }
}
  1. 属性装饰器

属性装饰器应用于类的属性,可以用来监视或修改属性的定义。属性装饰器接受两个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 属性的名字。
function format(target: any, propertyKey: string) {
  let _val = target[propertyKey];

  const getter = () => _val;
  const setter = (newVal) => {
    _val = newVal;
  };

  Object.defineProperty(target, propertyKey, {
    get: getter,
    set: setter,
    enumerable: true,
    configurable: true,
  });
}

class Person {
  @format
  name: string;

  constructor(name: string) {
    this.name = name;
  }
}
  1. 参数装饰器

参数装饰器应用于方法的参数,可以用来监视或修改参数的定义。参数装饰器接受三个参数:

  • 对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
  • 方法的名字。
  • 参数在函数参数列表中的索引。
function logParameter(target: any, propertyKey: string, parameterIndex: number) {
  const existingParameters = Reflect.getOwnMetadata("logParameters", target, propertyKey) || [];
  existingParameters.push(parameterIndex);
  Reflect.defineMetadata("logParameters", existingParameters, target, propertyKey);
}

class Person {
  greet(@logParameter message: string) {
    console.log(message);
  }
}
  1. 元数据反射 API

TypeScript 提供了一些反射 API,用于在运行时获取装饰器相关信息。需要在 tsconfig.json 中启用 emitDecoratorMetadata 选项:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true
  }
}

使用 reflect-metadata 库:

import 'reflect-metadata';

function logType(target: any, key: string) {
  const type = Reflect.getMetadata("design:type", target, key);
  console.log(`${key} type: ${type.name}`);
}

class Demo {
  @logType
  public attr: string;
}
  1. 装饰器工厂

装饰器工厂是一个返回装饰器函数的高阶函数,可以接受参数并返回一个装饰器。

function logClass(target: any) {
  console.log(`Class: ${target.name}`);
}

function logMethod(target: any, key: string, descriptor: PropertyDescriptor) {
  const originalMethod = descriptor.value;

  descriptor.value = function (...args: any[]) {
    console.log(`Method: ${key}`);
    console.log(`Arguments: ${JSON.stringify(args)}`);
    return originalMethod.apply(this, args);
  };
  return descriptor;
}

function logParameter(target: any, key: string, index: number) {
  const metadataKey = `log_${key}_parameters`;
  if (Array.isArray(target[metadataKey])) {
    target[metadataKey].push(index);
  } else {
    target[metadataKey] = [index];
  }
}

function logClassFactory(value: string) {
  return function (constructor: Function) {
    console.log(`Factory: ${value}`);
    console.log(`Class: ${constructor.name}`);
  };
}

@logClassFactory('DemoClass')
class Demo {
  @logMethod
  sayHello(@logParameter message: string) {
    console.log(message);
  }
}
  • 类装饰器:用于类构造函数,可以监视、修改或替换类定义。
  • 方法装饰器:用于方法,可以监视、修改或替换方法定义。
  • 访问器装饰器:用于访问器,可以监视、修改或替换访问器定义。
  • 属性装饰器:用于类的属性,可以监视或修改属性定义。
  • 参数装饰器:用于方法的参数,可以监视或修改参数定义。
  • 装饰器工厂:返回装饰器函数的高阶函数,允许传递参数。
  • 元数据反射 API:使用 reflect-metadata 库在运行时获取装饰器相关信息。

通过理解和使用这些装饰器知识点,能够帮助你在 TypeScript 中更好地组织代码,提升代码的可维护性和可读性。

Typescript的工具类型

TypeScript 中的工具类型(Utility Types)是用于操作和变换其他类型的高级类型,极大地提高了类型系统的灵活性和表达能力。以下是 TypeScript 工具类型的详细知识点:

  1. Partial

Partial<T> 将所有属性变为可选属性。可以用于创建类型的可选版本。

interface Person {
  name: string;
  age: number;
}

const partialPerson: Partial<Person> = {
  name: "Alice"
};
  1. Required

Required<T> 将所有属性变为必选属性。可以用于确保所有属性都被定义。

interface Person {
  name?: string;
  age?: number;
}

const requiredPerson: Required<Person> = {
  name: "Alice",
  age: 30
};
  1. Readonly

Readonly<T> 将所有属性变为只读属性。可以用于防止修改对象的属性。

interface Person {
  name: string;
  age: number;
}

const readonlyPerson: Readonly<Person> = {
  name: "Alice",
  age: 30
};

// readonlyPerson.age = 31; // Error: Cannot assign to 'age' because it is a read-only property.
  1. Record<K, T>

Record<K, T> 构造一个类型,其属性名类型为 K,属性值类型为 T。通常用于创建键值对的对象类型。

type Page = "home" | "about" | "contact";

const pageInfo: Record<Page, string> = {
  home: "/home",
  about: "/about",
  contact: "/contact"
};
  1. Pick<T, K>

Pick<T, K> 从类型 T 中选取一组属性 K 组成新的类型。可以用于创建类型的子集。

interface Person {
  name: string;
  age: number;
  address: string;
}

const personName: Pick<Person, "name"> = {
  name: "Alice"
};
  1. Omit<T, K>

Omit<T, K> 从类型 T 中排除一组属性 K,生成新的类型。可以用于创建去除某些属性的类型。

interface Person {
  name: string;
  age: number;
  address: string;
}

const personWithoutAddress: Omit<Person, "address"> = {
  name: "Alice",
  age: 30
};
  1. Exclude<T, U>

Exclude<T, U> 从类型 T 中排除类型 U 的子类型。可以用于过滤联合类型。

type T = "a" | "b" | "c";
type U = "a";

type Excluded = Exclude<T, U>; // "b" | "c"
  1. Extract<T, U>

Extract<T, U> 从类型 T 中提取类型 U 的子类型。可以用于过滤联合类型中的特定类型。

type T = "a" | "b" | "c";
type U = "a" | "d";

type Extracted = Extract<T, U>; // "a"
  1. NonNullable

NonNullable<T> 排除类型 T 中的 nullundefined。可以用于去除可空类型。

type T = string | number | null | undefined;

type NonNullableType = NonNullable<T>; // string | number
  1. ReturnType

ReturnType<T> 获取函数类型 T 的返回类型。可以用于提取函数的返回值类型。

function getUser() {
  return { name: "Alice", age: 30 };
}

type User = ReturnType<typeof getUser>; // { name: string; age: number }
  1. InstanceType

InstanceType<T> 获取构造函数类型 T 的实例类型。可以用于提取类实例的类型。

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonInstance = InstanceType<typeof Person>; // Person
  1. Parameters

Parameters<T> 获取函数类型 T 的参数类型组成的元组。可以用于提取函数的参数类型。

function setUser(name: string, age: number) {}

type SetUserParams = Parameters<typeof setUser>; // [string, number]
  1. ConstructorParameters

ConstructorParameters<T> 获取构造函数类型 T 的参数类型组成的元组。可以用于提取类构造函数的参数类型。

class Person {
  constructor(public name: string, public age: number) {}
}

type PersonConstructorParams = ConstructorParameters<typeof Person>; // [string, number]
  1. ThisParameterType

ThisParameterType<T> 获取函数类型 Tthis 参数类型。如果函数没有 this 参数,则返回 unknown

function sayHello(this: Person) {
  console.log(`Hello, ${this.name}`);
}

type ThisType = ThisParameterType<typeof sayHello>; // Person
  1. OmitThisParameter

OmitThisParameter<T> 从函数类型 T 中移除 this 参数类型。如果函数没有 this 参数,则返回 T

function sayHello(this: Person) {
  console.log(`Hello, ${this.name}`);
}

type NoThisSayHello = OmitThisParameter<typeof sayHello>;
  1. ThisType

ThisType<T> 用于指定上下文对象的类型。这在对象字面量中和 noImplicitThis 配合使用时非常有用。

type ObjectDescriptor<D, M> = {
  data?: D;
  methods?: M & ThisType<D & M>; // Type of 'this' in methods is D & M
};

function makeObject<D, M>(desc: ObjectDescriptor<D, M>): D & M {
  let data: object = desc.data || {};
  let methods: object = desc.methods || {};
  return { ...data, ...methods } as D & M;
}

let obj = makeObject({
  data: { x: 0, y: 0 },
  methods: {
    moveBy(dx: number, dy: number) {
      this.x += dx; // 'this' is { x: number, y: number, moveBy: (dx: number, dy: number) => void }
      this.y += dy;
    }
  }
});

obj.x = 10;
obj.y = 20;
obj.moveBy(5, 5);

TypeScript 的工具类型提供了强大的类型变换和操作能力,可以极大地提高类型系统的表达能力和灵活性。通过熟练掌握这些工具类型,可以编写更加健壮和灵活的 TypeScript 代码。

Typescript的高级主题

条件类型

条件类型是 TypeScript 中强大的功能,用于在类型层级进行逻辑运算。以下是 TypeScript 条件类型的详细知识点:

  1. 基本语法

条件类型的基本语法类似于三元运算符(?:):

T extends U ? X : Y

如果类型 T 能赋值给类型 U,那么返回类型 X,否则返回类型 Y

  1. 示例
type IsString<T> = T extends string ? "yes" : "no";

type A = IsString<string>;  // "yes"
type B = IsString<number>;  // "no"
  1. 分布式条件类型

当条件类型作用于联合类型时,它会分布在联合类型的每一个成员上:

type ToArray<T> = T extends any ? T[] : never;

type StrArr = ToArray<string>;  // string[]
type NumArr = ToArray<number>;  // number[]
type UnionArr = ToArray<string | number>;  // string[] | number[]

在上面的例子中,ToArray<string | number> 被分发为 ToArray<string> | ToArray<number>,即 string[] | number[]

  1. 内置条件类型

TypeScript 提供了一些内置的条件类型,用于常见的类型操作。

Exclude<T, U>

T 中排除可以赋值给 U 的类型:

type T1 = Exclude<"a" | "b" | "c", "a">;  // "b" | "c"
type T2 = Exclude<string | number | (() => void), Function>;  // string | number

Extract<T, U>

T 中提取可以赋值给 U 的类型:

type T3 = Extract<"a" | "b" | "c", "a" | "f">;  // "a"
type T4 = Extract<string | number | (() => void), Function>;  // () => void

NonNullable

T 中排除 nullundefined

type T5 = NonNullable<string | number | undefined>;  // string | number
type T6 = NonNullable<string[] | null | undefined>;  // string[]

ReturnType

获取函数类型 T 的返回类型:

function getUser() {
  return { name: "Alice", age: 30 };
}

type T7 = ReturnType<typeof getUser>;  // { name: string; age: number }

InstanceType

获取构造函数类型 T 的实例类型:

class Person {
  name: string;
  age: number;
  constructor(name: string, age: number) {
    this.name = name;
    this.age = age;
  }
}

type T8 = InstanceType<typeof Person>;  // Person
  1. 复杂条件类型示例

结合条件类型和泛型,可以创建复杂的类型操作:

深度可选类型(DeepPartial)

将对象的所有属性递归地设为可选:

type DeepPartial<T> = {
  [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P];
};

interface User {
  id: number;
  info: {
    name: string;
    age: number;
  };
}

type PartialUser = DeepPartial<User>;

// Equivalent to:
// type PartialUser = {
//   id?: number;
//   info?: {
//     name?: string;
//     age?: number;
//   };
// }

联合类型转交叉类型(UnionToIntersection)

将联合类型转换为交叉类型:

type UnionToIntersection<U> = 
  (U extends any ? (x: U) => void : never) extends (x: infer R) => void ? R : never;

type T9 = UnionToIntersection<{ a: string } | { b: number }>;  // { a: string } & { b: number }
  1. 分布式条件类型的实际应用

分布式条件类型非常适合用于联合类型的操作:

获取联合类型中的函数类型

type FunctionType<T> = T extends (...args: any[]) => any ? T : never;

type Mixed = string | number | (() => void);

type OnlyFunctions = FunctionType<Mixed>;  // () => void
  1. 使用条件类型的实用技巧

根据条件类型实现重载

可以使用条件类型来实现函数重载:

type FuncType<T> = T extends string ? (s: string) => string : (n: number) => number;

function processValue<T>(value: T): FuncType<T> {
  if (typeof value === 'string') {
    return ((s: string) => s.toUpperCase()) as FuncType<T>;
  } else {
    return ((n: number) => n * 2) as FuncType<T>;
  }
}

const result1 = processValue("hello");  // (s: string) => string
const result2 = processValue(42);       // (n: number) => number
  1. 条件类型的递归

条件类型可以递归使用,来处理复杂的嵌套结构:

type Flatten<T> = T extends any[] ? T[number] : T;

type NestedArray = number[][][];
type Flattened = Flatten<NestedArray>;  // number

深度展开(DeepFlatten)

将嵌套数组展开为单层数组:

type DeepFlatten<T> = T extends (infer U)[] ? DeepFlatten<U> : T;

type NestedArray2 = number[][][];
type DeepFlattened = DeepFlatten<NestedArray2>;  // number

条件类型是 TypeScript 中的一个强大工具,可以实现各种复杂的类型逻辑和操作。通过条件类型,可以:

  • 基于类型进行逻辑判断。
  • 创建动态和灵活的类型。
  • 实现复杂类型转换和操作。
  • 与泛型结合,增强代码的类型安全和可读性。

熟练掌握条件类型及其应用,可以极大提高 TypeScript 代码的灵活性和健壮性。

映射类型

TypeScript 的映射类型(Mapped Types)提供了一种从现有类型生成新类型的方法。这些新类型可以对现有类型的所有属性进行变换,从而实现更灵活和强大的类型定义。

映射类型的基本语法

映射类型的基本语法使用 in 关键字来遍历类型的键,并使用 keyof 操作符来获取类型的所有键:

type MappedType<T> = {
  [P in keyof T]: T[P];
};

内置映射类型

TypeScript 提供了多种内置映射类型,来满足不同的类型变换需求:

Partial<T>

Partial<T> 将所有属性变为可选:

interface Person {
  name: string;
  age: number;
}

type PartialPerson = Partial<Person>;
// Equivalent to: { name?: string; age?: number }

Required<T>

Required<T> 将所有属性变为必选:

interface Person {
  name?: string;
  age?: number;
}

type RequiredPerson = Required<Person>;
// Equivalent to: { name: string; age: number }

Readonly<T>

Readonly<T> 将所有属性变为只读:

interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;
// Equivalent to: { readonly name: string; readonly age: number }

Record<K, T>

Record<K, T> 创建一个类型,其属性名类型为 K,属性值类型为 T

type Page = "home" | "about" | "contact";
type PageInfo = Record<Page, { title: string }>;

const pageInfo: PageInfo = {
  home: { title: "Home" },
  about: { title: "About" },
  contact: { title: "Contact" },
};

Pick<T, K>

Pick<T, K> 从类型 T 中选取一组属性 K

interface Person {
  name: string;
  age: number;
  location: string;
}

type PersonNameAndAge = Pick<Person, "name" and "age">;
// Equivalent to: { name: string; age: number }

Omit<T, K>

Omit<T, K> 从类型 T 中排除一组属性 K

interface Person {
  name: string;
  age: number;
  location: string;
}

type PersonWithoutLocation = Omit<Person, "location">;
// Equivalent to: { name: string; age: number }

创建自定义映射类型

除了内置映射类型,你也可以创建自己的映射类型。

Mutable<T>

将所有属性变为可变(非只读):

type Mutable<T> = {
  -readonly [P in keyof T]: T[P];
};

interface ReadonlyPerson {
  readonly name: string;
  readonly age: number;
}

type MutablePerson = Mutable<ReadonlyPerson>;
// Equivalent to: { name: string; age: number }

Nullable<T>

将所有属性变为可以为 null

type Nullable<T> = {
  [P in keyof T]: T[P] | null;
};

interface Person {
  name: string;
  age: number;
}

type NullablePerson = Nullable<Person>;
// Equivalent to: { name: string | null; age: number | null }

DeepReadonly<T>

递归地将所有属性变为只读:

type DeepReadonly<T> = {
  readonly [P in keyof T]: DeepReadonly<T[P]>;
};

interface Person {
  name: string;
  address: {
    street: string;
    city: string;
  };
}

type ReadonlyPerson = DeepReadonly<Person>;
// Equivalent to: { readonly name: string; readonly address: { readonly street: string; readonly city: string } }

映射类型中的修饰符

在映射类型中,可以使用 +- 修饰符来添加或移除属性的修饰符:

移除可选属性

type Required<T> = {
  [P in keyof T]-?: T[P];
};

interface Person {
  name?: string;
  age?: number;
}

type RequiredPerson = Required<Person>;
// Equivalent to: { name: string; age: number }

添加只读属性

type Readonly<T> = {
  readonly [P in keyof T]: T[P];
};

interface Person {
  name: string;
  age: number;
}

type ReadonlyPerson = Readonly<Person>;
// Equivalent to: { readonly name: string; readonly age: number }

映射类型的应用场景

转换 API 响应

当从 API 获取数据时,可以使用映射类型来转换响应数据的类型:

interface ApiResponse {
  userId: number;
  id: number;
  title: string;
  completed: boolean;
}

type ClientResponse = {
  [P in keyof ApiResponse as `api_${P}`]: ApiResponse[P];
};

// Equivalent to: { api_userId: number; api_id: number; api_title: string; api_completed: boolean }

数据库实体类型

在数据库应用中,通常会有两种类型:输入类型和输出类型。可以使用映射类型来定义这些类型:

interface Entity {
  id: number;
  createdAt: Date;
  updatedAt: Date;
}

type CreateEntityInput<T> = {
  [P in keyof T as Exclude<P, "id" | "createdAt" | "updatedAt">]: T[P];
};

type UpdateEntityInput<T> = {
  [P in keyof T as Exclude<P, "createdAt" | "updatedAt">]?: T[P];
};

interface User extends Entity {
  name: string;
  email: string;
}

type CreateUserInput = CreateEntityInput<User>;
// Equivalent to: { name: string; email: string }

type UpdateUserInput = UpdateEntityInput<User>;
// Equivalent to: { id?: number; name?: string; email?: string }

映射类型的类型推导

映射类型在类型推导中也起着重要作用:

interface Options {
  width: number;
  height: number;
  color: string;
}

function configure(options: Partial<Options>) {
  // implementation
}

configure({ width: 100, color: "red" });  // valid

映射类型是 TypeScript 中强大的工具,用于在现有类型上进行变换和操作。通过映射类型,可以:

  • 创建基于现有类型的新类型。
  • 将属性变为可选、只读或其他变换。
  • 定义复杂的数据结构和类型转换逻辑。
  • 提高代码的灵活性和可维护性。

掌握映射类型及其应用,可以极大地提升 TypeScript 代码的灵活性和类型安全性。

模板字面量类型

TypeScript 的模板字面量类型(Template Literal Types)允许你使用字符串模板字面量的语法来构造新的字符串字面量类型。这使得字符串类型的操作更加灵活和强大。以下是模板字面量类型的详细知识点:

基本语法

模板字面量类型使用反引号(`)和占位符(${})来定义:

type Greeting = `Hello, ${string}`;

组合字符串字面量类型

可以通过模板字面量类型将多个字符串字面量类型组合在一起:

type HelloWorld = `Hello, ${"world" | "TypeScript"}`;
// Equivalent to: "Hello, world" | "Hello, TypeScript"

实际示例

假设我们有一组预定义的字符串类型,我们可以使用模板字面量类型来组合它们:

type Language = "JavaScript" | "TypeScript";
type Greeting = `Hello, ${Language}`;
// Equivalent to: "Hello, JavaScript" | "Hello, TypeScript"

使用内置字符串操作类型

TypeScript 提供了一些内置的字符串操作类型,可以与模板字面量类型结合使用:

Uppercase<S>

将字符串类型 S 转换为大写:

type UpperCaseGreeting = Uppercase<"hello">;
// Equivalent to: "HELLO"

Lowercase<S>

将字符串类型 S 转换为小写:

type LowerCaseGreeting = Lowercase<"HELLO">;
// Equivalent to: "hello"

Capitalize<S>

将字符串类型 S 的首字母转换为大写:

type CapitalizedGreeting = Capitalize<"hello">;
// Equivalent to: "Hello"

Uncapitalize<S>

将字符串类型 S 的首字母转换为小写:

type UncapitalizedGreeting = Uncapitalize<"Hello">;
// Equivalent to: "hello"

实际应用

模板字面量类型在许多实际应用中非常有用,特别是在构建字符串相关的类型系统时。

动态路由

假设我们有一个动态路由系统,可以使用模板字面量类型来定义路由参数:

type Route = `/users/${string}`;
const route: Route = "/users/123";  // valid
// const invalidRoute: Route = "/user/123";  // Error

提取子字符串类型

可以使用条件类型和模板字面量类型来提取字符串的一部分:

type ExtractUserId<Route> = Route extends `/users/${infer UserId}` ? UserId : never;
type UserId = ExtractUserId<"/users/123">;
// Equivalent to: "123"

复杂模板字面量类型

模板字面量类型可以与联合类型、条件类型、映射类型等结合使用,形成更复杂的类型定义。

动态生成属性名

可以使用模板字面量类型动态生成对象的属性名:

type PersonProperties = "name" | "age";
type Person = {
  [P in PersonProperties as `person_${P}`]: string;
};
// Equivalent to: { person_name: string; person_age: string }

状态机

可以使用模板字面量类型来定义状态机的状态和事件:

type State = "pending" | "success" | "error";
type Event = `on${Capitalize<State>}`;
type EventHandler = {
  [E in Event]: () => void;
};
// Equivalent to: { onPending: () => void; onSuccess: () => void; onError: () => void }

模板字面量类型为 TypeScript 提供了更灵活和强大的字符串类型操作功能。通过掌握模板字面量类型及其应用,可以极大地增强代码的类型安全性和可维护性。主要知识点包括:

  1. 基本语法:使用反引号和占位符定义模板字面量类型。
  2. 组合字符串字面量类型:组合多个字符串字面量类型。
  3. 内置字符串操作类型:使用 UppercaseLowercaseCapitalizeUncapitalize 等类型进行字符串操作。
  4. 实际应用:如动态路由、提取子字符串类型等。
  5. 复杂模板字面量类型:结合条件类型、映射类型等,进行更复杂的类型定义。

通过这些知识点的学习和应用,可以更好地利用 TypeScript 的类型系统来构建健壮和类型安全的代码。

名字空间及模块

在 TypeScript 中,名字空间(Namespaces)和模块(Modules)是两种用于组织和管理代码的机制。它们分别适用于不同的场景,但都能有效地帮助开发者管理大型代码库。以下是它们的详细知识点。

名字空间(Namespaces)

名字空间用于组织在一个全局作用域内的代码,以避免命名冲突。它们常用于组织大规模的应用程序中的代码。

定义名字空间

使用 namespace 关键字来定义名字空间:

namespace MyNamespace {
  export const myConstant = 42;
  export function myFunction() {
    console.log("Hello from MyNamespace");
  }
}

使用名字空间

通过 namespace 的名称访问其内部成员:

MyNamespace.myFunction(); // Hello from MyNamespace
console.log(MyNamespace.myConstant); // 42

嵌套名字空间

名字空间可以嵌套,以进一步组织代码:

namespace MyNamespace {
  export namespace NestedNamespace {
    export function nestedFunction() {
      console.log("Hello from NestedNamespace");
    }
  }
}

MyNamespace.NestedNamespace.nestedFunction(); // Hello from NestedNamespace

合并名字空间

名字空间可以跨文件合并,只要名字空间的名称相同即可:

// file1.ts
namespace MyNamespace {
  export function function1() {
    console.log("Function1");
  }
}

// file2.ts
namespace MyNamespace {
  export function function2() {
    console.log("Function2");
  }
}

// Usage
MyNamespace.function1(); // Function1
MyNamespace.function2(); // Function2

模块(Modules)

模块是 TypeScript 组织代码的主要方式,遵循 ES6 模块规范。模块有助于创建独立、可重用的代码块。

定义模块

每个文件默认都是一个模块。可以使用 exportimport 关键字来导出和导入模块成员。

// utils.ts
export const add = (a: number, b: number) => a + b;
export const subtract = (a: number, b: number) => a - b;

// main.ts
import { add, subtract } from './utils';

console.log(add(5, 3)); // 8
console.log(subtract(5, 3)); // 2

默认导出

可以使用 export default 导出一个默认成员:

// utils.ts
const multiply = (a: number, b: number) => a * b;
export default multiply;

// main.ts
import multiply from './utils';

console.log(multiply(5, 3)); // 15

导入所有成员

可以使用 * as 语法导入模块的所有成员:

// utils.ts
export const add = (a: number, b: number) => a + b;
export const subtract = (a: number, b: number) => a - b;

// main.ts
import * as Utils from './utils';

console.log(Utils.add(5, 3)); // 8
console.log(Utils.subtract(5, 3)); // 2

重新导出

可以使用 export ... from 语法重新导出一个模块的成员:

// moreUtils.ts
export { add, subtract } from './utils';

模块解析

TypeScript 通过配置文件 tsconfig.json 中的 moduleResolution 选项来解析模块。常见的值有 nodeclassic

{
  "compilerOptions": {
    "moduleResolution": "node"
  }
}

区别与应用场景

  • 名字空间
    • 常用于组织在同一个全局作用域中的代码。
    • 适用于需要将代码分隔但不希望创建多个独立模块的场景。
    • 主要用于老旧代码或需要在浏览器环境中直接使用的代码。
  • 模块
    • 遵循 ES6 模块规范,是现代 JavaScript 和 TypeScript 的标准。
    • 适用于几乎所有场景,特别是前端和后端开发。
    • 支持更好的代码分离和依赖管理,是推荐的代码组织方式。

名字空间与模块的结合

在过渡阶段,可以将名字空间与模块结合使用:

// shapes.ts
export namespace Shapes {
  export class Circle {
    constructor(public radius: number) {}
  }
}

// main.ts
import { Shapes } from './shapes';

const circle = new Shapes.Circle(10);
console.log(circle.radius); // 10

名字空间和模块在 TypeScript 中提供了两种有效的代码组织方式:

  • 名字空间:适用于避免命名冲突和组织大规模代码,但不适合模块化开发。
  • 模块:是现代代码组织和依赖管理的标准,适用于大多数开发场景。

掌握它们的使用方法和应用场景,可以显著提高代码的可维护性和可扩展性。

类型声明文件

TypeScript 的类型声明文件(Declaration Files)允许你为 JavaScript 代码库提供类型信息,从而在 TypeScript 中使用这些库时获得类型检查和智能提示。类型声明文件的扩展名为 .d.ts,通常用于描述第三方库的类型,但也可以用于描述你自己的代码。

以下是 TypeScript 类型声明文件的详细知识点:

基本概念

什么是类型声明文件

类型声明文件是一个仅包含类型声明的 TypeScript 文件,它不包含具体的实现代码。其主要目的是为 JavaScript 库提供静态类型检查支持。

// example.d.ts
declare function greet(name: string): void;

安装和使用类型声明文件

对于常用的 JavaScript 库,类型声明文件通常可以通过 DefinitelyTyped 项目获取。你可以使用 npm 或 yarn 安装这些类型:

npm install @types/lodash

创建类型声明文件

全局库的类型声明

对于在全局作用域中使用的库,可以直接在类型声明文件中使用 declare 关键字。

// globals.d.ts
declare const myGlobalVar: string;

declare function myGlobalFunction(x: number): void;

declare namespace myGlobalNamespace {
  function nestedFunction(): void;
}

模块库的类型声明

对于使用模块系统(如 CommonJS 或 ES6 模块)的库,类型声明文件应使用 export 关键字导出类型。

// module.d.ts
declare module "my-module" {
  export function myModuleFunction(x: number): void;
  export const myModuleConstant: string;
}

声明合并

TypeScript 允许在多个地方对同一个实体进行声明,这些声明会被合并。这在为 JavaScript 库编写类型声明时非常有用。

// declarations.d.ts
interface Widget {
  height: number;
  width: number;
}

// additionalDeclarations.d.ts
interface Widget {
  color: string;
}

// Resulting type of Widget
// interface Widget {
//   height: number;
//   width: number;
//   color: string;
// }

类型声明的高级特性

声明命名空间

命名空间用于将相关的类型和接口组织在一起,防止命名冲突。

// myLib.d.ts
declare namespace MyLib {
  function doSomething(): void;

  interface Options {
    width: number;
    height: number;
  }
}

声明类

声明类及其构造函数和方法。

// myClass.d.ts
declare class MyClass {
  constructor(name: string);

  getName(): string;
}

声明接口

接口用于描述对象的形状,并且可以扩展其他接口。

// myInterface.d.ts
interface MyInterface {
  name: string;
  age: number;
}

interface ExtendedInterface extends MyInterface {
  address: string;
}

声明类型别名

类型别名用于给类型起一个新名字,尤其适用于联合类型和交叉类型。

// myTypes.d.ts
type StringOrNumber = string | number;
type Callback = (err: Error, data: any) => void;

模块扩展

你可以为已存在的模块添加新的类型声明,这在扩展第三方库时非常有用。

// lodashExtensions.d.ts
import * as _ from "lodash";

declare module "lodash" {
  interface LoDashStatic {
    customFunction(): void;
  }
}

// usage.ts
_.customFunction();

特殊文件

@types

通常第三方库的类型声明文件会被发布到 @types 组织下,你可以通过 npmyarn 安装这些包。例如:

npm install @types/react

tsconfig.json 配置

tsconfig.json 中配置 typeRootstypes 属性,以指定类型声明文件的路径和包含的类型包。

{
  "compilerOptions": {
    "typeRoots": ["./types", "./node_modules/@types"],
    "types": ["node", "lodash"]
  }
}

常见的类型声明文件场景

为 JavaScript 库编写类型声明

// mathLib.d.ts
declare module "mathLib" {
  export function add(a: number, b: number): number;
  export function subtract(a: number, b: number): number;
}

为全局对象编写类型声明

// global.d.ts
declare const VERSION: string;
declare function initialize(config: object): void;

类型声明文件在 TypeScript 中起到了连接 JavaScript 代码和 TypeScript 类型系统的桥梁作用,主要知识点包括:

  1. 基本概念:理解什么是类型声明文件,以及如何使用它们。
  2. 创建类型声明文件:为全局库和模块库编写类型声明。
  3. 声明合并:多处声明同一实体时的合并规则。
  4. 高级特性:命名空间、类、接口、类型别名、模块扩展等。
  5. 特殊文件@types 包和 tsconfig.json 配置。
  6. 常见场景:为 JavaScript 库和全局对象编写类型声明。

掌握这些知识点,可以让你在 TypeScript 项目中更好地利用类型声明文件,提升代码的类型安全性和开发效率。

Typescript的配置和工具

TypeScript 提供了丰富的配置选项和工具支持,以满足各种项目需求。以下是 TypeScript 配置和工具的详细知识点。

TypeScript 配置

tsconfig.json

tsconfig.json 是 TypeScript 项目的配置文件,用于指定编译器选项和项目文件。

基本结构

{
  "compilerOptions": {
    // 编译选项
  },
  "include": [
    // 包含的文件或目录
  ],
  "exclude": [
    // 排除的文件或目录
  ],
  "files": [
    // 显式包含的文件列表
  ],
  "references": [
    // 项目引用
  ]
}

常用编译选项

  • target: 指定 ECMAScript 目标版本(如 ES5ES6/ES2015)。
  • module: 指定模块系统(如 commonjsamdesnext)。
  • lib: 指定编译时包含的库文件(如 ES6DOM)。
  • outDir: 指定输出目录。
  • rootDir: 指定输入文件的根目录。
  • strict: 启用所有严格类型检查选项。
  • esModuleInterop: 启用 ECMAScript 模块互操作性。
  • sourceMap: 生成对应的 .map 文件。
  • declaration: 生成对应的 .d.ts 文件。
{
  "compilerOptions": {
    "target": "ES6",
    "module": "commonjs",
    "lib": ["ES6", "DOM"],
    "outDir": "./dist",
    "rootDir": "./src",
    "strict": true,
    "esModuleInterop": true,
    "sourceMap": true,
    "declaration": true
  },
  "include": ["src/**/*"],
  "exclude": ["node_modules", "dist"]
}

严格类型检查选项

  • strict: 启用所有严格类型检查。
  • noImplicitAny: 禁止隐式的 any 类型。
  • strictNullChecks: 启用严格的 null 检查。
  • strictFunctionTypes: 启用严格的函数类型检查。
  • strictPropertyInitialization: 启用严格的属性初始化检查。
{
  "compilerOptions": {
    "strict": true
  }
}

TypeScript 工具

TSLint 和 ESLint

TSLint 曾经是 TypeScript 的主要代码检查工具,但现已被弃用。现在推荐使用 ESLint。

安装 ESLint

npm install --save-dev eslint @typescript-eslint/parser @typescript-eslint/eslint-plugin

配置 ESLint

创建 .eslintrc.json 文件:

{
  "parser": "@typescript-eslint/parser",
  "extends": [
    "plugin:@typescript-eslint/recommended"
  ],
  "plugins": ["@typescript-eslint"],
  "rules": {
    // 自定义规则
  }
}

运行 ESLint

npx eslint 'src/**/*.ts'

Prettier

Prettier 是一个代码格式化工具,可以与 ESLint 集成。

安装 Prettier

npm install --save-dev prettier eslint-config-prettier eslint-plugin-prettier

配置 Prettier

创建 .prettierrc 文件:

{
  "singleQuote": true,
  "semi": false
}

更新 .eslintrc.json 文件:

{
  "extends": [
    "plugin:@typescript-eslint/recommended",
    "plugin:prettier/recommended"
  ],
  "plugins": ["@typescript-eslint", "prettier"],
  "rules": {
    "prettier/prettier": "error"
  }
}

运行 Prettier

npx prettier --write 'src/**/*.ts'

编译与构建工具

Webpack

使用 ts-loaderbabel-loader 来集成 TypeScript。

安装依赖

npm install --save-dev webpack webpack-cli ts-loader typescript

配置 Webpack

// webpack.config.js
const path = require('path')

module.exports = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
}

Gulp

使用 gulp-typescript 集成 TypeScript。

安装依赖

npm install --save-dev gulp gulp-typescript typescript

配置 Gulp

// gulpfile.js
const gulp = require('gulp')
const ts = require('gulp-typescript')

const tsProject = ts.createProject('tsconfig.json')

gulp.task('scripts', function () {
  return tsProject.src().pipe(tsProject()).pipe(gulp.dest('dist'))
})

gulp.task('default', gulp.series('scripts'))

Rollup

使用 @rollup/plugin-typescript 集成 TypeScript。

安装依赖

npm install --save-dev rollup @rollup/plugin-typescript typescript

配置 Rollup

// rollup.config.js
import typescript from '@rollup/plugin-typescript'

export default {
  input: 'src/index.ts',
  output: {
    file: 'dist/bundle.js',
    format: 'cjs',
  },
  plugins: [typescript()],
}

Babel

Babel 是一个 JavaScript 编译器,可以通过 @babel/preset-typescript 来集成 TypeScript。Babel 可以与 Webpack 一起使用。

安装依赖

npm install --save-dev @babel/core @babel/preset-env @babel/preset-typescript babel-loader typescript

配置 Babel 和 Webpack

// babel.config.json
{
  "presets": ["@babel/preset-env", "@babel/preset-typescript"]
}
// webpack.config.js
const path = require('path');

module.exports = {
  entry: './src/index.ts',
  module: {
    rules: [
      {
        test: /\.tsx?$/,
        use: 'babel-loader',
        exclude: /node_modules/,
      },
    ],
  },
  resolve: {
    extensions: ['.tsx', '.ts', '.js'],
  },
  output: {
    filename: 'bundle.js',
    path: path.resolve(__dirname, 'dist'),
  },
};

编译和打包

npx webpack

测试工具

Jest

使用 ts-jest 集成 TypeScript。

安装依赖

npm install --save-dev jest ts-jest @types/jest

配置 Jest

// jest.config.js
module.exports = {
  preset: 'ts-jest',
  testEnvironment: 'node',
  testMatch: ['**/__tests__/**/*.ts', '**/?(*.)+(spec|test).ts'],
}

创建测试文件

// __tests__/sum.test.ts
import { sum } from '../src/sum'

test('adds 1 + 2 to equal 3', () => {
  expect(sum(1, 2)).toBe(3)
})

运行测试

npx jest

Mocha

使用 ts-node 集成 TypeScript。

安装依赖

npm install --save-dev mocha ts-node @types/mocha

配置 Mocha

// package.json
{
  "scripts": {
    "test": "mocha -r ts-node/register 'test/**/*.ts'"
  }
}

创建测试文件

// test/sum.test.ts
import { sum } from '../src/sum'
import { expect } from 'chai'

describe('sum', () => {
  it('should add 1 + 2 to equal 3', () => {
    expect(sum(1, 2)).to.equal(3)
  })
})

运行测试

npm test

掌握 TypeScript 的配置和工具知识点,可以极大提升开发效率和项目管理能力:

  1. 配置 tsconfig.json:设置编译选项和项目文件路径。
  2. 代码检查和格式化:
    • ESLint:使用 @typescript-eslint 检查 TypeScript 代码。
    • Prettier:格式化 TypeScript 代码,并与 ESLint 集成。
  3. 构建工具集成:
    • Webpack:使用 ts-loaderbabel-loader 集成 TypeScript。
    • Gulp:使用 gulp-typescript 集成 TypeScript。
    • Rollup:使用 @rollup/plugin-typescript 集成 TypeScript。
    • Babel:使用 @babel/preset-typescript 集成 TypeScript。
  4. 测试工具集成:
    • Jest:使用 ts-jest 测试 TypeScript 代码。
    • Mocha:使用 ts-node 测试 TypeScript 代码。

通过合理配置和使用这些工具,可以让 TypeScript 项目更加高效、规范和易于维护。