跳至主要內容

模块

樱桃茶大约 59 分钟

模块

模块是 TypeScript 的一个核心概念,用于组织和封装代码。每个模块都有自己的作用域,并且只有明确导出的部分才能被其他模块访问。

  • 模块导出与导入: 在 TypeScript 中,可以使用 export 关键字来导出模块中的变量、函数、类或接口等,以便其他模块可以使用 import 关键字来导入这些功能。

    // math.ts
    export function add(x: number, y: number): number {
      return x + y;
    }
    
    // 使用模块的文件
    import { add } from "./math";
    console.log(add(1, 2)); // 输出:3
    
  • 默认导出: 如果模块只导出单一事物,可以使用默认导出。这样在导入时不需要知道具体的命名。

    // calculator.ts
    export default class Calculator {
      add(x: number, y: number): number {
        return x + y;
      }
    }
    
    // 使用模块的文件
    import Calculator from "./calculator";
    const calc = new Calculator();
    console.log(calc.add(2, 3)); // 输出:5
    
  • 重新导出: 可以使用 export 关键字将另一个模块的导出再次导出,使其可从当前模块导入。

    // utilities.ts
    export * from "./math"; // 重新导出 math 模块的所有导出
    export * as helper from "./strings"; // 将 strings 模块的所有导出作为 helper 对象
    
  • 模块解析: TypeScript 解析模块时会查找 .ts.tsx.d.ts 文件。它首先尝试找到对应的 .ts 文件,如果没有找到,则会寻找 .d.ts 类型声明文件。

  • 路径映射: 在 tsconfig.json 配置文件中可以设置路径映射,简化模块导入时的路径。

    {
      "compilerOptions": {
        "baseUrl": ".", // 这里设置基准路径
        "paths": {
          "*": ["types/*"], // 所有模块都可以从"types"目录下搜索类型定义
          "utils/*": ["src/utils/*"] // 设置 utils 路径别名
        }
      }
    }
    
  • 命名空间与模块: 命名空间在早期用于组织代码和避免全局作用域污染,但现在,模块已经成为更好的选择,因为它们提供了相同的封装性,并且可以通过工具支持进行静态分析。

理解 TypeScript 模块对于构建大型应用程序非常重要,因为它们帮助你组织代码,并确保各部分之间清晰的依赖关系。随着你的项目增长,合理使用模块可以大大提高代码的可维护性和可扩展性。

导出

模块中的“导出”(Export)是 TypeScript 中用于指定哪些部分可以从一个文件被访问或者在另外一个文件中使用的功能。当你创建了一个模块,即一个包含代码和数据的文件时,你可能希望其他模块能够重用这些代码或数据,这就是导出发挥作用的地方。以下是一些基本概念和实例:

  • 基本导出: 你可以使用export关键字来导出变量、函数、类、接口等。
// 示例:导出一个变量
export const pi = 3.14;

// 示例:导出一个函数
export function multiply(x: number, y: number): number {
  return x * y;
}

// 示例:导出一个类
export class Greeter {
  greeting: string;

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

  greet() {
    return "Hello, " + this.greeting;
  }
}
  • 重命名导出: 如果你想避免命名冲突或者你想对外提供一个不同的名字,你可以在导出时重命名。
// 示例:导出时重命名
function listLength(list: any[]): number {
  return list.length;
}

export { listLength as count };
  • 导出语句: 除了在声明时直接导出,你还可以在模块的末尾统一导出。
// 示例:单独的导出语句
const someVariable = 42;

function someFunction() {}

class SomeClass {}

export { someVariable, someFunction, SomeClass };
  • 默认导出: 每个模块可以有一个默认导出,默认导出使用default关键字标记。导入时通常更简单。
// 示例:默认导出
export default class {
  greet() {
    return "Hello World!";
  }
}

// 导入默认导出时不需要大括号
import Greeting from "./GreetingModule";
  • 汇总导出: 如果你想在单一位置重新导出多个模块,可以使用汇总导出。
// 示例:从其他模块汇总导出
export { pi as PI } from "./MathModule";
export { multiply as mult } from "./MathModule";
  • 导出类型: TypeScript 特有的功能,也可以导出类型定义,如接口和类型别名。
// 示例:导出接口
export interface Serializable {
  serialize(): string;
}

通过上述方式,TypeScript 模块可以各自维护自己的代码和类型,并且可以很容易地将这些代码和类型暴露给其他模块使用,保持了代码的可维护性和可重用性。

导出声明

在 TypeScript 中,导出声明是指将代码中的特定部分(比如变量、函数、类、接口等)标记为可以被其他模块导入使用的语法结构。

  • 使用export关键字可以将模块内的成员(变量、函数、类等)暴露给其他文件使用。
  • 模块的消费者可以通过import关键字引入这些导出的成员。

举例说明:

// example.ts

// 导出变量
export const name = "Alice";

// 导出函数
export function sayHello(user: string) {
  console.log(`Hello, ${user}!`);
}

// 导出接口
export interface User {
  name: string;
  age: number;
}

// 导出类
export class Greeter {
  greeting: string;
  constructor(message: string) {
    this.greeting = message;
  }
  greet() {
    return `Hello, ${this.greeting}`;
  }
}

在另一个文件中,你可以这样导入它们:

// consumer.ts

import { name, sayHello, User, Greeter } from "./example";

console.log(name); // 输出:'Alice'
sayHello("Bob"); // 输出:'Hello, Bob!'

let user: User = {
  name: "Charlie",
  age: 30,
};

let greeter = new Greeter("world");
console.log(greeter.greet()); // 输出:'Hello, world'
  • 你还可以用 export 直接导出表达式。

例如:

// 变量
export const pi = 3.14;

// 函数
export function multiply(a: number, b: number): number {
  return a * b;
}

// 类
export class Animal {
  private name: string;
  constructor(name: string) {
    this.name = name;
  }
  move(distanceInMeters: number = 0) {
    console.log(`${this.name} moved ${distanceInMeters}m.`);
  }
}
  • 如果你想一次性导出多个成员,也可以使用一个单独的export语句。

例如:

const someVariable = 42;
function someFunction() {}
class SomeClass {}

export { someVariable, someFunction, SomeClass };
  • 还可以在导出时重命名导出的成员:

例如:

class MyClass {}
export { MyClass as RenamedClass };

以上就是 TypeScript 中导出声明的基本概念。通过合理地使用导出,你可以创建可维护和可重用的代码模块。

导出语句

在 TypeScript 中,模块是自己定义作用域的文件。每个模块里面的变量、函数、类等默认是私有的,如果你想让外部文件可以使用它们,就需要明确地导出它们。这就是导出语句的作用。

  • 导出声明: 你可以在任何声明(比如变量、函数、类、接口等)前加上export关键字来导出它们。
// example.ts
export const pi = 3.14;
export function calculateCircumference(diameter: number) {
  return diameter * pi;
}
export class Circle {
  static calculateArea(radius: number) {
    return pi * radius * radius;
  }
}
  • 导出语句: 如果你想在一个地方集中导出多个声明,或者重新导出模块中的内容,可以使用导出语句。
// utilities.ts
const sqrt = Math.sqrt;
function square(x: number) {
  return x * x;
}
function diag(x: number, y: number) {
  return sqrt(square(x) + square(y));
}

// Re-export
export { sqrt as mySqrt, square, diag };
  • 重命名导出: 当你导出某个模块成员时,可以同时给它重命名,这样消费者在导入时会使用你指定的新名称。
// 在导出时重命名
export { square as squareFunction, diag as diagonalLength };
  • 默认导出: 每个模块可以拥有一个default导出,当使用默认导入时不需要花括号。
// math.ts
export default function add(x: number, y: number) {
  return x + y;
}

// 使用默认导出意味着在其他文件中这样导入:
// import add from "./math";
  • 汇总导出: 你可以通过一条单独的导出语句来重新导出其他模块的导出,这对于创建“包”的公共 API 特别有用。
// index.ts
export * from "./utilities"; // 导出utilities.ts中所有导出
export * from "./math"; // 导出math.ts中所有导出

记住,导出的本质目的是控制模块成员的可见性并标识出哪些是可以被其他模块使用的。适当利用导出可以帮助你组织代码并建立清晰的依赖关系。

重新导出

在 TypeScript 中,"重新导出"是一个允许你将一个模块的导出变成另一个模块的导出的特性。这样做的好处是可以创建一个集中导出的模块,或者对外提供一个经过筛选或封装的 API 接口。下面通过例子来解释如何使用重新导出:

  • 假设我们有两个模块moduleA.tsmoduleB.ts,每个模块都有一些导出:
// moduleA.ts
export const a1 = "Hello";
export const a2 = "World";

// moduleB.ts
export const b1 = 42;
export const b2 = true;
  • 如果我们想在另一个模块(比如index.ts)中集中导出moduleAmoduleB中的所有或部分内容,我们可以使用重新导出:
// index.ts
export * from "./moduleA";
export * from "./moduleB";

// 或者只重新导出部分内容
export { a1, a2 } from "./moduleA";
export { b1 } from "./moduleB";
  • 当其他模块需要使用moduleAmoduleB中的变量时,可以直接从index.ts导入:
import { a1, b1 } from "./index";

console.log(a1); // 输出: Hello
console.log(b1); // 输出: 42
  • 这种方式有几个好处:
    • 可维护性:通过集中管理和导出,可以更容易地控制哪些功能对外公开,以及在未来需要修改导出内容时,只需修改一个地方。
    • 名称空间清晰:有助于避免不同模块之间的命名冲突,因为你可以选择性地重新导出并重命名特定的导出项。
    • 促进模块化:鼓励开发者按照功能组织代码,而不是将所有的函数或变量放在一个大文件中。

重新导出非常适用于创建库或工具集,允许库的使用者通过单一入口导入所需的所有功能,同时也保持了内部结构的封装性和灵活性。

导入

TypeScript 中的导入语句用于从其他模块引入绑定(变量、函数、类等)。这些语句可以让你使用别的文件或模块中定义的代码。在 JavaScript ES6 标准中引入了importexport来处理模块,TypeScript 作为 JavaScript 的超集,也支持这个功能,并且增加了类型检查和更多的功能。

  • 基本导入:

    • 使用import { name } from 'module-name';来导入特定的成员。
    import { Component } from "@angular/core";
    
  • 整体导入:

    • 使用import * as alias from 'module-name';来导入全部成员并赋值给一个别名。
    import * as fs from "fs";
    
  • 默认导入:

    • 如果模块导出时使用了默认导出,你可以不用花括号来导入。
    import React from "react";
    
  • 带副作用的导入:

    • 仅当你需要模块执行但不导入任何绑定时,使用。
    import "rxjs/add/operator/map";
    
  • 导入类型:

    • TypeScript 专有的导入方式,仅用于导入类型而不是值。
    import type { User } from "./user.model";
    
  • 混合导入:

    • 可以同时导入类型和值。
    import React, { FunctionComponent } from "react";
    

使用这些导入语句时,TypeScript 会进行类型检查和自动补全,这帮助开发者在编写代码时避免错误,并提升效率。需要注意的是,要正确导入其他模块,必须清楚地理解被导入模块的导出结构,比如哪些是默认导出,哪些是命名导出等。

导入一个模块中的某个导出内容

在 TypeScript 中,模块是一种将代码分割成可重用单元的方式。每个模块都可以包含变量、函数、类等,这些可以被其他模块导入并使用。当你想要导入一个模块中的某个导出内容时,你基本上是希望使用该模块提供的特定功能或数据类型。

  • 基本导入

    • 假设有一个模块 mathUtils.ts,它导出了一个函数 add

      // mathUtils.ts
      export function add(x: number, y: number): number {
        return x + y;
      }
      
    • 要在另一个文件中使用这个 add 函数,你需要导入它:

      // main.ts
      import { add } from "./mathUtils";
      
      console.log(add(1, 2)); // 输出:3
      
  • 导入整个模块为一个别名

    • 如果你想一次性导入 mathUtils.ts 模块中的所有导出,可以使用 * as 语法,并给它一个别名:

      // main.ts
      import * as MathUtils from "./mathUtils";
      
      console.log(MathUtils.add(1, 2)); // 输出:3
      
  • 导入时重命名

    • 你也可以在导入时重命名某个导出的内容,这在避免命名冲突或者对导入的内容进行更明确命名时很有用:

      // main.ts
      import { add as addNumbers } from "./mathUtils";
      
      console.log(addNumbers(1, 2)); // 输出:3
      
  • 默认导出和导入

    • 如果一个模块只导出一个主要的功能,你可以使用默认导出:

      // calculator.ts
      export default class Calculator {
        add(x: number, y: number) {
          return x + y;
        }
        // 更多方法...
      }
      
    • 导入默认导出时,不需要使用大括号:

      // main.ts
      import Calculator from "./calculator";
      
      const calc = new Calculator();
      console.log(calc.add(1, 2)); // 输出:3
      

通过正确地导入模块中的内容,你可以有效地组织和管理你的代码库,同时确保各部分之间的高度解耦和复用性。

将整个模块导入到一个变量,并通过它来访问模块的导出部分

在 TypeScript 中,模块是一种将代码分割成可复用的单元的方式。当你想要使用模块中定义的变量、函数、类等时,你需要导入它们。

将整个模块导入到一个变量中,然后通过这个变量来访问模块的导出部分,可以使用以下语法:

import * as myVariable from "path/to/module";

这里的myVariable是你自己定义的名称,path/to/module是模块文件的路径。

例子:

假设有一个数学工具库math.ts,它导出了一些基础的数学函数:

// 文件:math.ts
export const pi = 3.1415926;
export function add(x: number, y: number): number {
  return x + y;
}
export function multiply(x: number, y: number): number {
  return x * y;
}

如果你想在另一个文件中导入并使用这些函数,可以这样做:

// 文件:app.ts
import * as math from "./math";

console.log(math.pi); // 输出:3.1415926
console.log(math.add(2, 3)); // 输出:5
console.log(math.multiply(4, 5)); // 输出:20

在这个例子中,我们没有单独导入piaddmultiply,而是导入了整个math模块,并且通过前缀math.来访问它导出的成员。这种方式非常适合于你想要导入大量内容,同时希望保持命名空间清晰的场景。

具有副作用的导入模块

具有副作用的导入模块,是指在导入模块时,并不是为了导入特定的接口、类型或者变量等,而是为了模块中的代码能被执行,通常这种代码会影响全局状态或者注册某些功能。

  • 在 TypeScript 中,你可能会遇到一些模块,当它们被导入时即便你没有直接使用其中的任何导出,它们仍然需要执行。例如:

    • 某模块可能会向全局对象添加属性。
    • 某模块可能会改变其他模块的行为,如添加方法扩展。
    • 某模块可能仅仅是希望运行其中的代码,而不需要返回任何结果。
  • 若要导入一个具有副作用的模块,可以简单地使用import "module-name";语句,省略了从模块中导入特定绑定(exports)的部分。

举例来说:

// 假设存在一个模块 'my-module',
// 它在全局对象上注册了一个新函数

// 不导入任何绑定,仅为了确保 'my-module' 中的代码得以执行
import "my-module";

// 现在可以安全地假设全局对象上的新函数已经可用
  • 这种导入方式通常在应用初始化阶段较为常见,你可能需要设置环境或配置某些全局插件。

  • 需要注意的是,这种具有副作用的导入如果过多且不加以管理,可能会导致代码难以追踪和调试,因为它们会产生全局效应,但却不像正常的模块导入那样容易理解它们的来源和目的。

  • TypeScript 会对这些具有副作用的导入进行处理,确保它们的代码被执行,就像在 JavaScript 中一样。尽管如此,在设计模块系统时,还是推荐尽可能避免全局副作用,以保持代码的清晰和可维护性。

默认导出

默认导出(Default Exports)是 TypeScript 中一种特殊的导出方式,允许你为每个模块指定一个默认导出。以下是关于默认导出的重要点和实例:

  • **单个值导出:**每个模块可以有一个default导出。这意味着当你导入时不需要知道导出的具体名字,这通常用于模块只导出一个主要功能的情况。

  • **无需花括号:**默认导出在导入时不使用花括号{}。这与命名导出形成对比,后者需要使用花括号。

  • **重命名简便:**导入默认导出时可以直接给它命名而不需要使用as语法。

  • **每个模块一个默认导出:**尽管可以有多个命名导出,但只能有一个默认导出。

以下是一些使用默认导出和导入的例子:

// mathUtils.ts
function add(x: number, y: number): number {
  return x + y;
}

export default add; // 将add函数作为默认导出

// main.ts
import addFunction from "./mathUtils"; // 导入默认导出并命名为addFunction

console.log(addFunction(1, 2)); // 输出:3

在这个例子中,mathUtils.ts文件定义了一个add函数,并且将它作为默认导出。然后,在main.ts文件中我们导入了这个默认导出,并且可以随意命名它(在这里命名为addFunction)。当我们调用addFunction(1, 2)时,它实际上就是在调用mathUtils.ts中定义的add函数。

默认导出非常适合那些输出单一对象、类库或工具函数的场景。但是当你的模块提供多个功能时,通常推荐使用命名导出来保持清晰的导出结构。

export = 和 import = require()

TypeScript 模块中的export =import = require()是遵循 CommonJS 模块系统的特殊语法。在 CommonJS 模块系统中,可以使用module.exports来导出一个模块的公开 API,并通过require()函数来导入其他模块。当你从 JavaScript 转向 TypeScript 时,你可能会遇到这种类型的模块语法。

  • export =
    • 这个语法用于在 TypeScript 模块中导出单个对象、类、函数或变量。
    • 它是 CommonJS 模块的等价于module.exports
    • 只能使用一次,因为它指定了模块的全部导出内容。
// calculator.ts
class Calculator {
  add(a: number, b: number) {
    return a + b;
  }
}

export = Calculator; // 导出Calculator类
  • import = require()
    • 这个语法用于导入使用export =导出的模块。
    • 相当于 CommonJS 中的const module = require('module')
    • 使用这种方式导入时,不能使用 TypeScript 的 ES6 风格导入(例如import { Module } from 'module')。
// app.ts
import Calculator = require("./calculator");

const calc = new Calculator();
console.log(calc.add(2, 3)); // 输出:5

需要注意的是,虽然 TypeScript 支持 CommonJS 的模块语法,但推荐在新项目中使用 ES6 的模块语法(import/export),因为它提供了更丰富的语法和编译时检查。只有当你需要和已有的 CommonJS 模块集成或者有特殊需求时,才考虑使用export =import = require()

生成模块代码

在 TypeScript 中,"生成模块代码"通常指的是 TypeScript 编译器如何将你写的模块代码转换成 JavaScript 代码,以便浏览器或 Node.js 可以执行。不同的运行环境可能支持不同类型的模块系统,比如 CommonJS、AMD、UMD、ES2015 等。在 TypeScript 中,你可以通过配置文件tsconfig.json来指定要使用的模块系统。

下面是几个关于“生成模块代码”的关键点:

  • 模块系统

    • CommonJS:主要用于 Node.js,使用requiremodule.exports
    • AMD:异步模块定义,用于前端,需要 RequireJS 库来加载模块。
    • UMD:通用模块定义,既可以用于前端也可以用于 Node.js。
    • ES2015 (ES6):JavaScript 的官方模块系统,使用importexport
  • 配置: 在tsconfig.json中的compilerOptions部分,使用"module"字段来设置目标模块系统,例如:

    {
      "compilerOptions": {
        "module": "commonjs"
      }
    }
    
  • 例子: 假如你有一个模块math.ts

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

    根据配置的不同,生成的 JavaScript 代码会有所不同:

    • 如果tsconfig.json设置为使用 CommonJS,输出可能会是:

      function add(x, y) {
        return x + y;
      }
      exports.add = add;
      
    • 如果设置为使用 ES2015 模块,则输出可能类似于:

      export function add(x, y) {
        return x + y;
      }
      
  • 使用模块: 对其他文件来说,它们会根据你设置的模块系统来导入这些函数。例如,如果你正在使用 ES2015 模块:

    import { add } from "./math";
    
    console.log(add(1, 2)); // 输出3
    

了解“生成模块代码”对于编写可在不同环境运行的 TypeScript 代码非常重要。正确地配置模块类型能确保你的代码能够无缝工作在你的目标平台上。

简单示例

模块是 TypeScript 中组织和重用代码的一种方式。在 JavaScript ES6 中引入了模块概念,TypeScript 也采用了这个标准。一个模块就是一个包含有导出和导入的文件。导出(export)是为了让模块的某部分可以被其他模块使用,而导入(import)则是为了在一个模块中使用另一个模块导出的部分。

  • 简单示例:

    • 假设我们有一个名为 validation.ts 的模块,它导出了一个字符串验证功能。

      // validation.ts
      export function isValid(str: string): boolean {
        return str.length > 0;
      }
      

      这里 isValid 函数前面的 export 关键字表示该函数可以被其他模块导入并使用。

    • 另外一个文件,例如 main.ts,可以使用 import 语句来导入 validation.ts 模块中的 isValid 函数:

      // main.ts
      import { isValid } from "./validation";
      
      console.log(isValid("Hello")); // 输出 true
      console.log(isValid("")); // 输出 false
      

      main.ts 中,通过从 validation.ts 导入 isValid 函数,可以使用这个函数校验字符串。

    • 请注意,当你导入模块时,应该使用相对路径或者绝对路径,并且不需要添加文件扩展名 .ts

模块化编程有助于维护大型代码库,因为它能让你将代码分割成可重用的单元,并且更容易管理依赖关系。在 TypeScript 中理解和使用模块对构建现代 Web 应用程序是非常重要的。

可选的模块加载和其它高级加载场景

模块中的"可选的模块加载和其它高级加载场景"是关于你如何在 TypeScript 中按需(条件性地)或使用特殊方式来加载模块。这对于大型应用程序或有特定环境要求的应用程序很有用,比如当你只想在某些条件成立时才加载一个模块,或者你需要动态地加载模块。

  • 使用顶层 import 语句进行静态加载 静态加载是模块导入最常见的形式。这意味着当你在文件的顶部使用import语句时,被导入的模块将在程序开始时就被加载进来。

    import { myFunction } from "./myModule";
    
  • 动态加载模块 TypeScript 支持通过 ES2015 的import()表达式来动态加载模块。与顶层import不同,import()可以在函数体内、if 语句中或任何你需要的地方调用。它返回一个 Promise,让你能够异步加载模块。

    if (condition) {
      import("./myModule").then((module) => {
        module.myFunction();
      });
    }
    
  • 使用 require 条件性加载模块 在 CommonJS 模块系统(比如 node.js 环境)中,你可以使用require函数来条件性地加载模块。

    if (condition) {
      const myModule = require("./myModule");
      myModule.myFunction();
    }
    
  • Reflective API 和命名空间导入 在一些高级场合,你可能还会需要使用反射 API(Reflective APIs)如System.import来加载模块。这通常用于某些框架或库的模块加载机制。

每种加载方式都适用于不同的场景,了解它们的区别和适用情况有助于构建更加灵活和健壮的应用程序。

使用其它的 JavaScript 库

当你在使用 TypeScript 编写代码时,很可能需要依赖一些已存在的 JavaScript 库。尽管这些库可能并没有用 TypeScript 编写,但你仍然可以在 TypeScript 项目中安全地使用它们。为了做到这一点,你需要理解如何在 TypeScript 中使用其他 JavaScript 库。

  • 类型声明文件: TypeScript 使用.d.ts文件为已存在的 JavaScript 代码提供类型信息。这个文件不包含任何逻辑代码,只有类型声明。例如,如果你想要在 TypeScript 中使用 jQuery,你需要 jQuery 的类型声明文件。

    // jQuery的类型声明示例
    // jquery.d.ts
    interface JQuery {
      text(content: string): this;
    }
    
    declare var $: JQuery;
    
  • @types 组织: 许多流行的 JavaScript 库的类型声明文件都可以在 npm 上的@types组织中找到。通过安装这些类型声明包,TypeScript 编译器能够知道库的 API 结构。例如,要使用 lodash 库,你可以通过 npm 安装对应的类型声明包:

    npm install --save @types/lodash
    

    安装后,就可以在 TypeScript 项目中导入并使用 lodash,同时享受类型检查和自动补全等功能。

  • 手动添加或编辑类型声明: 如果你正在使用一个较小的库,或者出于某种原因,它没有可用的类型声明文件,你可以自己创建一个简单的声明文件,或者向现有的.d.ts文件添加必要的类型信息。

    // 创建一个简单的.d.ts文件
    // my-library.d.ts
    declare module "my-library" {
      export function myMethod(val: string): string;
    }
    

    这样,当你导入并使用'my-library'时,TypeScript 将会知道myMethod的存在以及它的类型信息。

  • 模块插件或 UMD 模块: 当第三方库是以模块插件形式或是 UMD(Universal Module Definition)模块提供时,其类型声明文件也应该相应地提供正确的模块格式声明。

    // UMD模块声明示例
    // my-umd-library.d.ts
    declare namespace MyUmdLibrary {
      function doSomething(): void;
    }
    
    export as namespace MyUmdLibrary;
    export = MyUmdLibrary;
    

    这使得该库既可以作为模块导入使用,也可以作为全局变量访问。

了解如何与其他 JavaScript 库配合使用,能帮助你更好地集成 TypeScript 到现有项目中,同时利用 TypeScript 提供的类型系统优势。随着社区的发展和类型声明文件的丰富,使用 TypeScript 与 JavaScript 库共存变得越来越容易。

外部模块

在 TypeScript 中,当你想要使用其它的 JavaScript 库时,你可能会遇到一些并非用 TypeScript 编写的代码。因此,为了能够在 TypeScript 项目中顺利地使用这些 JavaScript 库,你需要理解如何与这些"外部模块"进行交互。

  • 外部模块指的是那些由第三方提供的、可以被导入到你自己的项目中的代码模块。在 JavaScript 中,这通常是通过包管理器如 npm 来安装和管理的。

  • 为了在 TypeScript 中使用这些外部模块,你需要有一个对应的类型声明文件(.d.ts文件),这样 TypeScript 编译器就能理解模块暴露出来的 API 的类型信息。

  • 如果库的维护者没有提供类型声明文件,社区可能已经在 DefinitelyTyped 上为其提供了。DefinitelyTyped 是一个大型的 GitHub 仓库,里面包含了许多流行 JavaScript 库的类型声明文件。

  • 使用@types命名空间来从 npm 安装对应的类型声明文件。例如,如果你想使用 lodash 这个库,你可以运行 npm install --save @types/lodash 来安装 lodash 的类型声明。

举例:

// 假设已经安装了lodash及其类型声明文件
import _ from "lodash";

// 现在你可以在TypeScript中安全地使用lodash,并且享受到类型检查
let shuffledArray = _.shuffle([1, 2, 3, 4, 5]);
  • 在某些情况下,如果你无法找到所需库的类型声明文件,或者你正在使用一个私有库,你可以创建一个自定义的类型声明文件。在你的项目中创建一个.d.ts文件,然后在其中添加类型声明:
// example.d.ts
declare module "my-own-library" {
  export function myMethod(val: string): string;
}
  • 然后,你就可以在其他 TypeScript 文件中引用你的库,同时获得类型检查的好处:
import { myMethod } from "my-own-library";

let result = myMethod("string"); // 正确的调用

记住,TypeScript 的目标之一是确保你可以在不牺牲现有 JavaScript 生态系统的前提下使用 TypeScript。适当使用类型声明文件能够帮助你更平滑地实现这一点。

外部模块简写

外部模块简写是在 TypeScript 中处理并使用那些没有 TypeScript 类型定义的 JavaScript 库时的一种方法。当你从 JavaScript 迁移到 TypeScript 时,可能会遇到一些没有提供.d.ts类型声明文件的第三方库。为了让这些库能够在 TypeScript 项目中使用而不引发编译错误,你可以使用外部模块简写来告诉 TypeScript 忽视类型检查。

  • 使用外部模块简写的情况主要包括:

    • 当你使用的第三方 JavaScript 库没有提供 TypeScript 的类型定义。
    • 当你处于迁移旧项目到 TypeScript 的过程中,且暂时无法为每个模块提供完整的类型定义。
  • 实现方式:

    • 创建一个声明文件(通常以.d.ts结尾),例如declarations.d.ts
    • 在该文件中,你可以为缺失类型定义的模块使用declare module语句,这样 TypeScript 就会将它们视为任意类型。
  • 示例: 假设你正在使用一个名为awesome-js-library的 JavaScript 库,但这个库没有相应的 TypeScript 类型定义。你可以如下创建一个简单的声明文件来避免类型错误:

// declarations.d.ts
declare module "awesome-js-library";
  • 使用后的效果:

    • 在你的 TypeScript 文件中,当你导入并使用awesome-js-library时,TypeScript 编译器将不会对其进行类型检查,因为你已经通过外部模块简写告诉了 TypeScript,这个模块的类型是任意的。
    • 这允许你在没有正式类型定义的情况下使用 JavaScript 库,同时也意味着你失去了 TypeScript 提供的类型安全性保障。
  • 注意事项:

    • 尽管这种方法可以快速解决缺少类型定义的问题,但推荐尽可能寻找或贡献库的类型定义。可以在DefinitelyTypedopen in new window上查找是否有现成的类型定义。
    • 如果你对库非常熟悉,考虑自己编写更详细的类型定义,以便能够享受到 TypeScript 提供的类型检查和代码智能提示等功能。

通过使用外部模块简写,你可以在不牺牲项目进度的情况下,逐步将 JavaScript 项目迁移至 TypeScript,同时也为将来可能的全面类型化打下基础。

模块声明通配符

模块声明通配符是 TypeScript 中一种特殊的语法,主要用于当你在使用外部 JavaScript 库时,这些库可能没有提供自己的类型定义文件(.d.ts)。为了能够在 TypeScript 项目中顺利使用这些库而不引发编译器错误,你可以使用模块声明通配符来告诉 TypeScript 如何处理这些导入的模块。

  • 使用模块声明通配符时,你可以创建一个.d.ts文件,在这个文件中,你可以如下使用declare module关键字后跟一个字符串模式(使用*作为通配符)来匹配那些没有类型定义的模块名。

    // typings.d.ts
    declare module "*.jpg" {
      const content: string;
      export default content;
    }
    
  • 上面的例子中,任何以.jpg结尾的导入都会被赋予string类型的默认导出。这对于一些构建工具允许你导入图片或其他非代码资源并需要类型支持的情况非常有用。

  • 另一个常见的用例是,当使用某些全局插件或老旧的 JavaScript 库没有提供.d.ts文件时,你也可以用这种方式来“欺骗”TypeScript 编译器,使其认为这个模块是存在的,并且可以是任何你声明的类型。

    // global-plugin.d.ts
    declare module "ancient-global-plugin" {
      export function doSomethingGlobal(): void;
    }
    
  • 在这个例子中,任何尝试导入ancient-global-plugin的操作都会被认为是有效的,且doSomethingGlobal函数会被认为是该模块的一部分,返回void类型。

总结来说,模块声明通配符是 TypeScript 提供的一种灵活机制,以便在没有现成类型定义的情况下,仍然可以在 TypeScript 项目中安全地使用外部 JavaScript 库。它允许开发者通过简单的声明来桥接类型系统和现实世界代码库之间的差异。

UMD 模块

UMD (Universal Module Definition) 模块是一种支持多种模块系统的设计模式,旨在让同一个代码库能同时在客户端(如浏览器)和服务器端(如 Node.js)环境中无缝工作。它通过检测当前环境支持的模块定义方式(比如 AMD、CommonJS 或全局变量),并据此采用相应的方式来导出模块。

  • 使用场景: 当你想要编写既可以在前端也可以在后端运行的 JavaScript 库时,UMD 模式非常有用。这样的库在被引入到不同的环境中时,会自动适应该环境支持的模块系统。

  • 基本结构:

    (function (root, factory) {
      if (typeof define === "function" && define.amd) {
        // AMD. Register as an anonymous module.
        define(["dependency"], factory);
      } else if (typeof module === "object" && module.exports) {
        // Node. Does not work with strict CommonJS,
        // but only CommonJS-like environments that support module.exports,
        // like Node.
        module.exports = factory(require("dependency"));
      } else {
        // Browser globals (root is window)
        root.returnExports = factory(root.dependency);
      }
    })(this, function (dependency) {
      // Actual module code goes here
      return {};
    });
    

    这段代码首先检查环境是否支持 AMD 模块定义方式(define函数存在),如果是,就使用 AMD 的方式定义模块。如果不是,再检查是否处于 CommonJS 环境(如 Node.js,通过module.exports判断),如果也不是,则假定它运行在一个没有模块系统的全局环境中,比如直接在浏览器里通过<script>标签引入。

  • 类型声明:当使用 TypeScript 开发或使用 UMD 模块时,可能需要为该模块提供类型声明文件(.d.ts),以便 TypeScript 知道如何正确地类型检查使用该模块的代码。例如,如果有一个名为myLibrary的 UMD 库,其类型声明文件可能如下所示:

    // myLibrary.d.ts
    export as namespace myLibrary;
    
    export function myFunction(param: string): void;
    

    此类型声明文件表明myLibrary模块具有一个名为myFunction的函数,该函数接受一个字符串参数且不返回任何内容。通过这种方式,UMD 库可以在 TypeScript 项目中被平滑地集成和使用,同时保持跨环境的灵活性。

记得,在使用任何第三方 UMD 库时,首先查看该库是否已经提供了 TypeScript 类型声明文件,或者在DefinitelyTypedopen in new window上查找是否有社区贡献的声明文件。这样可以大大简化集成过程。

创建模块结构指导

模块是 TypeScript 中用于组织和封装代码的一种结构。在 TypeScript 中,任何包含顶层 importexport 的文件都被视为一个模块。下面解释的是如何创建模块结构。

  • 导出:当你想在多个文件之间共享代码时,可以将它们导出。使用 export 关键字可以将变量、函数、类、接口等导出,使得其他模块能够通过 import 进行访问。

    • 示例:

      // mathUtils.ts
      export function add(x: number, y: number): number {
        return x + y;
      }
      
      export function multiply(x: number, y: number): number {
        return x * y;
      }
      
  • 导入:如果你需要使用其他模块提供的功能,可以使用 import 关键字来导入所需的部分。

    • 示例:

      // calculator.ts
      import { add, multiply } from "./mathUtils";
      
      console.log(add(2, 3)); // 输出:5
      console.log(multiply(2, 3)); // 输出:6
      
  • 默认导出:每个模块可以有一个 'default' 导出。默认导出使用 default 关键字标记,并且在导入时可以指定任意名称。

    • 示例:

      // StringUtils.ts
      export default function capitalize(str: string): string {
        return str.charAt(0).toUpperCase() + str.slice(1);
      }
      
      // usage.ts
      import makeCapital from "./StringUtils";
      
      console.log(makeCapital("hello")); // 输出:"Hello"
      
  • 模块结构:建议把相关的功能放在同一个模块中;无关紧要的功能则分散在不同模块中。这样做可以帮助保持代码的清晰和可维护性。

  • 避免使用命名空间:在 TypeScript 中,由于模块系统的存在,通常不需要使用命名空间(以前称作“内部模块”),应优先使用模块导出和导入。

  • 类型导出:TypeScript 允许导出声明,包括类型别名和接口,这样可以在其他模块中复用类型信息。

    • 示例:

      // types.ts
      export interface User {
        name: string;
        age: number;
      }
      
      // userUtils.ts
      import { User } from "./types";
      
      export function greet(user: User) {
        console.log(
          `Hello, my name is ${user.name} and I am ${user.age} years old.`
        );
      }
      
  • 文件与目录结构:文件结构应该直观反映模块的结构。例如,相关的模块可以放在同一个目录下。

  • 模块解析:TypeScript 编译器需要知道如何查找模块文件,这涉及到配置 tsconfig.json 文件中的 "moduleResolution" 和 "baseUrl" 等选项。

通过理解这些基本概念,你就能创建清晰、结构化的模块,有助于管理大型项目中的代码。

尽可能地在顶层导出

尽可能地在顶层导出”的原则是指,在使用 TypeScript 编写模块时,应该尽量保持导出的内容位于文件或模块的顶部层级,而不是嵌套在其他结构或函数内部。这样做有几个好处:

  • 可读性和可维护性:更容易理解模块暴露了哪些功能,同时也便于其他开发者查找和使用这些导出的功能。
  • 重构友好:如果将来需要移动功能到其他文件或者重命名某个导出,如果它们都在顶层,这个过程会简单很多。
  • Tree-shaking:现代工具(如 Webpack)可以通过静态分析去除未使用的代码(也就是 tree-shaking)。顶层导出使得这些工具更容易识别和优化。

以下是一些示例,说明如何遵循这个原则:

// 坏的实践:导出嵌套在函数内部
function createLibrary() {
  function doSomething() {}

  return {
    doSomething,
  };
}

// 好的实践:直接在顶层导出
export function doSomething() {}

// 另外一个选择是使用命名空间,但仍然在顶层
namespace MyLibrary {
  export function doSomething() {}
}

确保导出语句不被嵌套在其他逻辑中,可以显著改善模块的清晰度和可用性。

如果仅导出单个 class 或 function,使用 export default

在 TypeScript 中,模块是自己的作用域中包含代码的文件。这意味着在一个模块文件里定义的变量、函数、类等等,默认情况下在其他文件中是不可见的,除非你明确地导出它们。同时,为了使用其他模块定义的这些元素,则需要通过import语句将其导入。

  • 尽可能地在顶层导出

    当你创建一个模块时,建议尽可能在文件的顶层进行导出,而不是在嵌套的作用域内部导出。

    • 实例: 假设你有一个工具函数,你希望在多个文件中使用它。

      // utils.ts
      export function usefulFunction() {
        // ... 函数内容 ...
      }
      

      在另一个文件中,你可以直接导入并使用usefulFunction

      // anotherModule.ts
      import { usefulFunction } from "./utils";
      
      usefulFunction();
      
  • 如果仅导出单个 class 或 function,使用 export default

    如果你的模块中只有一个主要的类或者函数要导出,那么应该使用export default

    • 实例: 假设你有一个特定的类MyClass,你打算在整个应用程序中频繁地使用它。

      // MyClass.ts
      export default class MyClass {
        // ... 类的内容 ...
      }
      

      这样,在其他文件中导入MyClass的时候,你可以这样做:

      // someOtherModule.ts
      import MyClass from "./MyClass";
      
      const myClassInstance = new MyClass();
      

      使用export default可以使得导入语句更加简洁,当然,每个模块只能有一个默认导出。

以上就是如何导出模块成员以及何时使用默认导出的基本指南。记住,清晰的模块结构和合理的导出方式能够让你的代码更易于理解和维护。

MyClass.ts
  • TypeScript 中的模块是自己的作用域,不是全局作用域。当你在一个文件里写下一个变量、函数、类等时,它们在这个文件或模块内部,默认是不可见的,除非你明确地使用 export 来导出它们。

  • 使用 export default 可以导出一个模块默认的输出。这是推荐的做法,当一个文件或模块只导出一个单独的对象,比如一个类或函数时。

  • 当其他文件想要使用这个类或者函数时,可以通过 import ... from '...' 语句来导入。

例子:

MyClass.ts

// 这里定义了一个类 MyClass,并且希望它能被其他模块引用
export default class MyClass {
  constructor(public message: string) {}

  printMessage() {
    console.log(this.message);
  }
}

AnotherModule.ts

// 这里显示了如何导入 MyClass 类,并创建其实例
import MyClass from "./MyClass";

const myClassInstance = new MyClass("Hello TypeScript!");
myClassInstance.printMessage();
  • AnotherModule.ts 中,我们没有使用花括号来包围 MyClass,因为它是默认导出的。如果 MyClass 是非默认导出,我们需要使用花括号来导入。

  • 默认导出的好处是,导入时可以自由命名导入的内容,而不必严格匹配导出的名字。

  • 每个模块只能有一个默认导出,这限制了默认导出的使用场景。如果模块中有多个类或函数需要导出,那么应该使用命名导出。

MyFunc.ts

在 TypeScript 中,export default用于导出模块中的主要内容,使得其他模块能够更容易地导入。当你有一个模块只导出一个类或者函数时,使用export default会很方便。下面是使用export default的一些指南和例子:

  • 当你的模块(MyFunc.ts)只包含一个主要的功能或实体时,比如一个函数或类,你应该使用export default
  • 这样做可以让其他开发者在导入你的模块时写起来更简单,因为他们不需要使用花括号{}
  • 默认导出的模块在被导入时,可以用任何名称来接收,不需要知道原始的名称。

示例:

假设我们有一个函数MyFunc,它是MyFunc.ts文件中唯一的导出:

// MyFunc.ts
export default function MyFunc() {
  // 函数的具体操作
}

在另一个文件中导入MyFunc.ts的默认导出时:

// OtherFile.ts
import AnyNameYouLike from "./MyFunc";

AnyNameYouLike(); // 调用刚才导入的函数

注意,在上面的例子中,导入时我没有使用花括号,并且我可以自由选择一个名字(AnyNameYouLike)来引用这个函数。它不需要与MyFunc.ts中定义的名字相同,因为它是一个默认导出。

Consumer.ts
  • TypeScript 中的export default用于导出模块中的主要内容,这可以是一个类、接口、函数或变量。
  • 当使用export default时,导入该模块的其他文件可以不用花括号直接导入默认导出项,并给它取任何名字。

假设有一个模块MyClass.ts,它定义了一个类并希望被作为主要内容导出:

// MyClass.ts
export default class MyClass {
  constructor(public message: string) {}
  printMessage() {
    console.log(this.message);
  }
}
  • 当其他模块需要使用MyClass时,可以如下导入:
// Consumer.ts
import AnyNameYouWant from "./MyClass";

const instance = new AnyNameYouWant("Hello, World!");
instance.printMessage(); // 输出:Hello, World!
  • 在上面的例子中,AnyNameYouWant是导入时给MyClass起的名字,因为它是默认导出,所以你不需要知道原来的类名就可以导入并使用它。
  • 只有一个东西可以被标记为export default,如果尝试对多个项使用export default,TypeScript 会报错。
  • 默认导出的好处是简化了导入过程,特别是当模块只有一个输出值时。但如果模块需要导出多个值,则建议使用具名导出,在导入时就要使用相应的名称和大括号了。

如果要导出多个对象,把它们放在顶层里导出

当我们在 TypeScript 中使用模块时,推荐的做法是尽可能地在顶层导出我们需要共享的功能或数据结构。这样做有几个好处:

  • 提高可读性:当其他开发者查看你的模块时,他们可以很容易地找到所有可用的导出项,因为所有东西都在顶部。
  • 易于维护:随着你的项目或库的增长,将导出项放在顶层使得维护起来更加方便,因为你不需要深入到文件的内部去寻找或修改导出的内容。
  • 方便重构:如果你决定更改某个功能的实现但不改变其导出方式,你可以自由地在模块内部进行调整,而不会影响到使用该模块的代码。

下面通过一些例子来说明如何实践这个指导原则:

假设我们有两个函数和一个类需要从一个模块中导出。

  • 错误示范:
function helper() {
  // 实现细节
}

export class MyClass {
  constructor() {
    console.log("MyClass 实例化了");
  }
  // 使用 helper 函数
  useHelper() {
    helper();
  }
}

// 在类定义之后导出 helper 函数
export { helper };

在上述代码中,helper 函数的导出被分散了,它并没有和MyClass类一起在顶部被导出,这可能会让人忽略掉helper函数的存在或者导致导入时的不便。

  • 正确示范:
// 在顶层导出所有公共接口
export function helper() {
  // 实现细节
}

export class MyClass {
  constructor() {
    console.log("MyClass 实例化了");
  }
  // 使用 helper 函数
  useHelper() {
    helper();
  }
}

在这个示例中,所有要导出的项(helper函数和MyClass类)都紧靠文件的顶部导出。这种方式使得别的开发者打开这个文件时,能够立即看到所有公开的 API,增强了模块的可用性与清晰度。

简而言之,尽可能在顶层导出有助于提升模块的清晰度和易用性,并使得维护和重构工作变得更加轻松。

MyThings.ts

在 TypeScript 中,模块是一个包含代码和声明的文件,它们可以相互导入和导出功能、类型、接口等。遵循一些最佳实践可以让你创建更清晰和可维护的模块结构。

  • 尽可能地在顶层导出: 当你有多个函数、类或接口要导出时,建议直接在文件的顶层进行导出,而不是首先将它们封装进一个对象,再导出那个对象。这样做可以提高模块的可用性并简化导入路径。
// 推荐:在顶层导出每个项
export class MyClass {...}
export function myFunction() {...}
export interface MyInterface {...}

而不是:

// 不推荐:将所有导出封装在一个对象中
class MyClass {...}
function myFunction() {...}
interface MyInterface {...}

export { MyClass, myFunction, MyInterface };
  • 如果要导出多个对象,把它们放在顶层里导出:有时候你可能需要从一个模块中导出多个对象。同样,应该在文件顶层进行导出,而不是创建一个默认导出的对象包含所有这些对象。
// 推荐:独立导出每个对象
export class AnotherClass {...}
export function anotherFunction() {...}
export interface AnotherInterface {...}

与之相反的情况会使得导入时的语法更加复杂,因为使用者需要知道具体要从对象中解构哪些属性。

// 不推荐:使用单个对象导出所有
const MyThings = {
  AnotherClass: class {...},
  anotherFunction: function () {...},
  AnotherInterface: interface {...}
};

export default MyThings;

使用上面推荐的方法,其他文件可以通过明确的导入语句来获取需要的类、函数或接口:

// 导入单个项
import { MyClass, myFunction } from "./MyThings";

// 使用导入的类和函数
let instance = new MyClass();
myFunction();

这种方法比起默认导出整个对象,可以带来更好的模块化和树摇(tree-shaking)效果,后者是指构建工具在打包时移除未使用的代码的过程。

明确地列出导入的名字

  • 尽可能地在顶层导出

    • 这个原则鼓励开发者在模块的最外层,也就是文件的顶部进行功能、类别或对象的导出。这样做的好处是使模块的结构更清晰,便于维护和使用。

    • 示例:

      • 假设你有一个工具函数集合,在utils.ts文件中,而不是在文件内部定义函数后再单独导出,应直接在顶级作用域导出每个函数。
      // 推荐方式
      export function add(a: number, b: number): number {
        return a + b;
      }
      
      export function subtract(a: number, b: number): number {
        return a - b;
      }
      
      • 相对地,避免在文件内部先定义后再统一导出,尽管这也是可行的,但不符合"尽可能地在顶层导出"的指导原则。
      // 不推荐方式
      function add(a: number, b: number): number {
        return a + b;
      }
      
      function subtract(a: number, b: number): number {
        return a - b;
      }
      
      export { add, subtract };
      
  • 明确地列出导入的名字

    • 当使用其他模块的导出时,应该明确地列出你需要导入的名字,而不是使用星号(*)导入整个模块。这有利于提高代码的可读性和维护性,并且有时候可以帮助减少打包大小因为工具可以更容易地进行“树摇”(tree-shaking,即去除未使用代码)。

    • 示例:

      • 如果你需要使用utils.ts文件中的addsubtract函数,正确的导入方式如下:
      // 推荐方式
      import { add, subtract } from "./utils";
      
      const sum = add(1, 2);
      const difference = subtract(3, 1);
      
      • 避免使用通配符(*)导入所有导出,因为这会使得不清楚当前文件究竟使用了模块的哪些功能,减少代码的可读性。
      // 不推荐方式
      import * as Utils from "./utils";
      
      const sum = Utils.add(1, 2);
      const difference = Utils.subtract(3, 1);
      
Consumer.ts

在 TypeScript 中,了解如何正确导入和使用模块是非常关键的部分。这个知识点主要强调了在编写 TypeScript 模块(或文件)时应该遵循的最佳实践,尤其是针对如何导入其他模块或库中的函数、类等。以下是一些核心建议及示例:

  • 明确地列出导入的名字:这意味着当你在一个文件中需要使用另一个模块提供的功能时,应该明确指定你想要导入什么。这样做有助于代码的可读性和可维护性。

    • 错误示范:使用通配符导入一个模块中的所有内容,这可能会导致命名冲突,且使得代码的依赖关系不够明确。
      // 不推荐
      import * as Utils from "./utils";
      const result = Utils.compute();
      
    • 正确示范:只导入需要的部分。
      // 推荐
      import { compute } from "./utils";
      const result = compute();
      

通过具体指定要导入的名称,你可以清楚地了解当前文件依赖哪些外部功能,并且减少潜在的命名空间污染问题。

这个理念背后的思考是,TypeScript(以及 JavaScript)的模块化能够帮助我们构建更加结构化和可维护的代码库。通过显式地声明我们所依赖的部分,我们不仅能够让代码更加清晰、易于理解,同时也便于未来的维护和优化。例如,在进行代码重构时,如果我们发现某个模块不再需要某个外部依赖,只需移除相应的导入语句即可,而不必担心未使用的代码残留在项目中造成混乱。此外,现代 JavaScript 打包工具(如 Webpack 或 Rollup)在打包时,能够利用这种明确的导入声明来进行“树摇”(tree-shaking),即移除未被使用的代码,从而减小最终打包文件的大小,提高应用的加载速度和性能。

总之,明确地列出导入的名字是一个简单但非常有效的方式,能够帮助你写出更清晰、更高效的 TypeScript 代码。

使用命名空间导入模式当你要导出大量内容的时候

  • TypeScript 的模块是一种将代码分割成小块来进行组织和封装的方法,每个模块都有自己的作用域。
  • 在 TypeScript 中,任何包含顶层importexport的文件都被认为是一个模块。
  • 通常我们会推荐尽可能地在模块的顶层导出,这样可以直接导出类、接口、函数等,而不是把它们放到一个对象里再导出。

例如:

// v1.ts
export class MyClass {
  //...
}

export function myFunction() {
  //...
}
  • 当你有大量内容需要导出时,你可以使用import * as MyLargeModule from './MyLargeModule'的形式来一次性导入所有导出。

例如,如果你有一个非常大的模块,里面有很多功能需要导出:

// MyLargeModule.ts
export class Class1 {
  /* ... */
}
export class Class2 {
  /* ... */
}
// ...更多类
export function func1() {
  /* ... */
}
export function func2() {
  /* ... */
}
// ...更多函数

你可以在另一个文件中这样导入全部内容:

// AnotherFile.ts
import * as MyLargeModule from "./MyLargeModule";

let instance1 = new MyLargeModule.Class1();
let instance2 = new MyLargeModule.Class2();
MyLargeModule.func1();
  • 使用命名空间导入模式(import * as Alias)不仅可以帮助你一次性导入大量内容,还能使得导入的内容通过别名Alias来访问,保持清晰的命名空间,并且当你只需要部分功能时可以避免导入整个模块。
MyLargeModule.ts
  • 模块在 TypeScript 中是一个文件,它包含了一些相关的代码—变量、函数、类等。当你的模块或者库很大时,你可能会有很多要导出的内容。

  • 导出大量内容时,尽可能在顶层导出,这意味着应该直接从模块导出而不是嵌套在某个对象或命名空间中。这使得其他模块可以更容易地导入和使用这些导出的内容。

  • 例如,你有一个大型模块MyLargeModule.ts,它包括很多功能。如果你将所有的导出放在文件的顶层,那么其他开发者可以简单地通过名称来导入他们需要的部分。

// MyLargeModule.ts
export class SomeClass {
  /* ... */
}
export function someFunction() {
  /* ... */
}
export const someVariable = 123;
  • 使用者在另一个文件中想要使用这些导出的内容,可以直接引用具体的名称。
import { SomeClass, someFunction } from "./MyLargeModule";

const instance = new SomeClass();
someFunction();
  • 当使用命名空间导入模式(也就是使用import * as MyLargeModule from './MyLargeModule')时,你是把整个模块作为一个单独的对象导入。这在你需要大量内容时特别有用,因为你可以用点语法(.)访问所有导出的类型。
import * as MyLargeModule from "./MyLargeModule";

const instance = new MyLargeModule.SomeClass();
MyLargeModule.someFunction();
  • 这种方式能帮助组织代码和保持清晰的命名空间。但是,过度使用命名空间导入可能会降低代码的可读性,尤其是当只需要一个模块中的几个导出时。

  • 基于模块的结构和使用场景,选择适合的导出和导入方法对于保持代码的清晰性和可维护性是非常重要的。

Consumer.ts

在 TypeScript 中,当你有一个大量导出的模块时,推荐使用命名空间导入模式。这意味着你应该使用import * as SomeIdentifier from '...'来导入你需要的内容。

例子:

  1. 假设有一个名为utilities.ts的文件,它导出了多个工具函数:
// utilities.ts
export function add(x: number, y: number): number {
  return x + y;
}

export function multiply(x: number, y: number): number {
  return x * y;
}

// 更多工具函数...
  1. 当你需要在另一个文件(比如consumer.ts)中使用这些函数时,你可以用命名空间导入全部内容:
// consumer.ts
import * as Utilities from "./utilities";

let sum = Utilities.add(1, 2);
let product = Utilities.multiply(3, 4);

// 使用其他导出的工具函数...

通过使用命名空间导入,你不仅避免了不必要的长列表导入带来的繁琐,也提高了代码的可读性,并且清楚地表明了这些函数属于同一个模块。当别人阅读你的代码时,他们可以很容易地识别出这些函数是从utilities.ts模块导入的。

使用重新导出进行扩展

使用重新导出进行扩展的目的是让你能够创建一个模块,该模块从其他模块导入一部分内容后,可能还会添加一些自己的内容或对导入的内容做一些修改,然后再将这些内容作为一个整体导出。这种方式可以帮助你在保持代码结构清晰和模块化的同时,便于代码的复用和维护。

  • 基本概念:当你使用export关键字将模块中的变量、函数、类等导出时,其他模块通过import关键字可以导入这些内容。重新导出(Re-exporting)意味着你在导入一些内容之后,又使用export将它们导出。

  • 为什么需要重新导出

    • 减少冗余代码:避免每个需要这些模块的地方都写一遍相同的导入语句。
    • 创建更高层次的模块接口:可以将多个小模块汇总成一个大模块对外提供服务。
  • 如何进行重新导出

    • 直接重新导出:当你不需要修改模块内部的任何内容,只是想将它再次导出时,可以直接使用export { name1, name2 } from 'module'的形式。

      // file: MathOperations.ts
      export { add, subtract } from "./SimpleMath";
      
      // 使用
      import { add, subtract } from "./MathOperations";
      
    • 修改或添加后重新导出:如果你想在导出前对其进行一些修改,或者添加一些新的内容,可以先导入,然后修改或添加,最后统一导出。

      // file: ExtendedMath.ts
      import { multiply as basicMultiply } from "./SimpleMath";
      export const multiply = (a: number, b: number) => basicMultiply(a, b);
      export const square = (x: number) => multiply(x, x);
      
      // 使用
      import { multiply, square } from "./ExtendedMath";
      
  • 示例场景:假设你有一个处理字符串的模块和一个处理数字的模块,你可能想要创建一个工具库模块,它包含了字符串和数字处理的所有功能,并且还添加了一些新的功能。

    • 首先,你会从这两个模块导入所需的功能。
    • 然后,可能会添加一些新的或者改进的功能。
    • 最后,将这些功能作为一个整体再次导出,以便其他部分的代码可以轻松地使用这个大而全的工具库模块。

通过使用重新导出进行扩展,TypeScript 提供了一种灵活的模块组织方式,有助于你构建易于管理和维护的代码库。

Calculator.ts

在 TypeScript 中,模块是包含类、接口、函数、变量和其他模块的代码块。与 JavaScript 的 ES6 模块相似,TypeScript 也使用importexport语句来交互。当你想要创建一个模块结构并且需要扩展现有模块功能时,可以使用重新导出(re-export)。这对于创建一个封装了多个模块并仅暴露必要部分的公共 API 特别有用。

Calculator.ts为例,假设我们有一些基础的数学运算函数分散在不同的模块里,我们可能希望在一个统一的模块中重新导出它们。下面是如何使用重新导出来组织这些函数:

  • 原始的模块可能看起来像这样:

    // add.ts
    export function add(x: number, y: number): number {
      return x + y;
    }
    
    // multiply.ts
    export function multiply(x: number, y: number): number {
      return x * y;
    }
    
  • 然后,在Calculator.ts中,我们可以重新导出这些函数:

    // Calculator.ts
    export { add } from "./add";
    export { multiply } from "./multiply";
    
  • 这样,任何导入Calculator.ts的模块都可以访问到addmultiply函数:

    // 使用Calculator中的功能
    import { add, multiply } from "./Calculator";
    
    let sum = add(1, 2); // 结果为3
    let product = multiply(2, 3); // 结果为6
    

通过这种方式,你可以构建一个整洁的 API,并且对外隐藏内部模块的结构细节,使得其他开发者更容易使用你的模块,同时也更加方便你管理代码。这是在大型项目中组织和封装代码的常见做法。

TestCalculator.ts

在 TypeScript 中,模块是包含代码和逻辑的独立文件,可以通过导出(export)和导入(import)来共享功能。模块系统允许你组织和重用代码。使用重新导出进行扩展(Re-exporting to Extend)是一种模式,它允许你从一个模块导入一些功能,并将它们(可能连同一些新功能)再导出。

假设我们有一个名为 TestCalculator.ts 的模块示例,我们想要说明如何使用重新导出来扩展现有模块。

  • 首先,我们创建一个基本的计算器模块 Calculator.ts

    // Calculator.ts
    export class Calculator {
      public add(a: number, b: number): number {
        return a + b;
      }
    
      public subtract(a: number, b: number): number {
        return a - b;
      }
    }
    
  • 现在我们想要创建一个扩展的计算器,添加更多功能,但同时保留原有的加减功能。我们创建一个新文件 TestCalculator.ts 并从 Calculator.ts 导入所有内容:

    // TestCalculator.ts
    export * from "./Calculator"; // Re-export everything from Calculator.ts
    
  • 接下来,在 TestCalculator.ts 中添加新的功能,比如乘法和除法:

    // 继续 TestCalculator.ts 文件
    export class TestCalculator {
      public multiply(a: number, b: number): number {
        return a * b;
      }
    
      public divide(a: number, b: number): number {
        if (b === 0) throw new Error("Cannot divide by zero");
        return a / b;
      }
    }
    
  • 在其他模块中使用扩展的计算器,我们只需导入 TestCalculator.ts 而不是原始的 Calculator.ts

    // 使用 TestCalculator.ts 的其他文件
    import { Calculator, TestCalculator } from "./TestCalculator";
    
    const calc = new Calculator();
    console.log(calc.add(2, 3)); // 输出: 5
    
    const testCalc = new TestCalculator();
    console.log(testCalc.multiply(3, 4)); // 输出: 12
    

这样,TestCalculator.ts 不仅提供了原始 Calculator.ts 的功能(如加法和减法),还增加了乘法和除法的新功能。这就是使用重新导出进行扩展的方式:我们导入需要的功能并将其重新导出,同时添加任何新的或扩展的功能。

ProgrammerCalculator.ts

在 TypeScript 中,模块是一个可以包含类、接口、函数、变量等的代码文件。它们用于组织和封装功能,这样代码就更易于管理和维护。通过使用导出(export)和导入(import)语句,你可以在不同模块之间共享这些功能。

"使用重新导出进行扩展"指的是从一个模块中导出部分内容的同时,也可以对其进行扩展或添加额外的功能。这种方法可以帮助你创建一个更加丰富的模块接口,而无需修改原始模块的源码。

  • 重新导出现有模块:你可以简单地重新导出另一个模块中的内容,这样做的好处是可以将多个小模块组合成一个大模块。

    // calculator.ts
    export function add(a: number, b: number) {
      return a + b;
    }
    
    export function subtract(a: number, b: number) {
      return a - b;
    }
    
    // programmerCalculator.ts
    export * from "./calculator"; // 重新导出'calculator'模块中所有的内容
    
  • 扩展并重新导出模块:当你想要向现有模块添加新功能时,你可以首先导入模块,然后添加新功能,并一起导出。

    // extendedCalculator.ts
    import * as Calculator from "./calculator";
    
    export function multiply(a: number, b: number) {
      return a * b;
    }
    
    // Re-exporting original functionalities along with the new one
    export { Calculator };
    

    在上面的例子中,extendedCalculator.ts 导入了 calculator.ts 模块中的所有内容,并添加了一个新的 multiply 函数。接着,它将原模块(Calculator)的所有功能连同新的multiply功能一起导出。

  • 限制重新导出的内容:如果你只想重新导出某些特定的功能,而不是全部,你可以明确指定要导出的内容。

    // selectiveCalculator.ts
    export { add as BasicAdd } from "./calculator";
    
    // 只重新导出了add函数,并且重命名为BasicAdd
    

    在这个示例中,我们只从 calculator.ts 中重新导出了 add 函数,并且在导出时将其重命名为 BasicAdd

通过使用重新导出,你可以创建一个层次化的模块结构,使得顶层模块可以作为一个单一的访问点来暴露多个子模块的功能。这对于开发大型应用程序是非常有帮助的,因为它可以提高代码的可读性和可维护性。

TestProgrammerCalculator.ts

模块中的"使用重新导出进行扩展"是关于如何在 TypeScript 中创建和扩展模块,同时保持代码组织有序且易于管理。这里的"TestProgrammerCalculator.ts"可能是一个具体示例文件名,用来说明如何应用这一概念。

  • 重新导出:允许你将一个模块导入的功能直接导出,而无需先导入后再导出。这让你可以建立一个派生模块,它在原有模块基础上做扩展。

    • 例子

      // calculator.ts
      export class Calculator {
        // ... 具有一些方法和属性
      }
      
      // programmerCalculator.ts
      export class ProgrammerCalculator extends Calculator {
        // ... 添加了一些特定于程序员的方法
      }
      
      // testProgrammerCalculator.ts
      export * from "./programmerCalculator";
      // 重新导出programmerCalculator模块的所有导出
      
      import { ProgrammerCalculator } from "./testProgrammerCalculator";
      // 现在可以直接从testProgrammerCalculator导入ProgrammerCalculator类
      
  • 扩展:你可以在不更改原始模块的情况下扩展一个模块的功能。通过导入原始模块,添加新的功能或者修改后,然后重新导出新的模块。

    • 例子

      // 假设我们已经有了上面的calculator.ts和programmerCalculator.ts
      
      // extendedCalculator.ts
      import { ProgrammerCalculator } from "./programmerCalculator";
      
      export class ExtendedCalculator extends ProgrammerCalculator {
        // ... 在ProgrammerCalculator的基础上添加更多功能
      }
      
      // 可以选择性地只导出ExtendedCalculator
      export default ExtendedCalculator;
      
  • 使用这种方式,可以方便地构造出层次分明的模块体系,并且使得每个模块专注于单一职责。当需要引入新功能或者构建子类时,能够在不破坏原有逻辑和结构的前提下进行增加。

  • 这个机制使得维护和更新大型项目变得更加简洁,因为每个模块都是独立的,通过明确的导入和导出与其他模块相互作用。这也有助于减少命名冲突,因为所有的导入都是显式的,重命名也更加容易实施。

注意:由于没有具体的 "TestProgrammerCalculator.ts" 的内容,在此给出的是根据标题推断可能的解释和相关示例。实际的官方文档可能会提供具体的代码示例和详细的说明。

模块里不要使用命名空间

在 TypeScript 中,模块是自身作用域的一个文件;也就是说,在一个模块内部定义的变量、函数、类等,默认情况下在模块外部是不可见的。如果你想在另一个文件中访问模块内部定义的成员,你必须导出它们并在另一文件中导入。

  • 命名空间(以前称为"内部模块")是 TypeScript 早期版本中使用的概念,用于组织代码并防止全局作用域污染,但现在建议使用模块来代替。

通常,你应该避免在一个模块中使用命名空间,因为模块本身已经提供了其成员的作用域隔离。以下是一些关于为什么应该在模块中避免使用命名空间的理由:

  • 模块具有自己的作用域,并且只有明确导出的成分才对其他模块可见。命名空间的主要目的是提供额外的作用域层次来防止名称冲突,这在模块系统中天然就被解决了。

  • 使用模块可以提高代码的重用性,并能更好地管理依赖关系。当你从模块中导入绑定时,这会告诉读者这些功能来自哪里,而不需要额外的命名空间层次。

  • 现代 JavaScript 模块加载器和打包工具(如 Webpack, Rollup, esbuild)与 ES6 模块协同工作得很好,使得模块化开发更加高效。

下面是一些示例,说明如何使用 TypeScript 中的模块,而不是命名空间:

// 示例模块 math.ts
export function add(x: number, y: number): number {
  return x + y;
}

export function subtract(x: number, y: number): number {
  return x - y;
}

// 使用模块的示例 consumer.ts
import { add, subtract } from "./math";

let result = add(1, 2); // 结果是 3
result = subtract(result, 1); // 结果是 2

math.ts文件中,函数addsubtract被导出,这意味着其他文件可以导入并使用这些函数。在consumer.ts中,我们导入了所需的函数,并直接使用它们,无需命名空间。

危险信号

在 TypeScript 文档中,"危险信号"一节通常是用来指导开发者避免在使用模块时犯一些常见的错误。这些错误可能会导致代码的可维护性和可扩展性变差。下面是一些模块结构中的危险信号及其解释:

  • 整体导入

    • 当你看到有代码使用星号(*)导入所有内容时,这通常是一个警告。
    • 例子:import * as utils from "./utils";
    • 这种做法使得模块间的依赖关系变得不明确,而且可能包含未使用的代码。
  • 使用模块作为命名空间

    • TypeScript 的模块不应该被当作命名空间(namespace)来使用。
    • 命名空间最初设计是为了提供给没有模块系统的 JavaScript 代码组织方式。
    • 在使用模块系统时,应该直接将功能分散到各自的文件中,而不是一个大文件中。
  • 非模块的模块

    • 文件应该清晰地表明它们的依赖关系。
    • 如果一个文件不导出或导入任何东西,它可能就不应该作为一个模块。
    • 每个模块都应有特定目的,并只包含与该目的相关的代码。
  • 导出大量内容

    • 看到一个模块导出很多细小的功能时,这可能意味着它需要被拆分成更小的模块。
    • 模块应该专注于单一目的,如果有许多导出项,可能难以理解和重构。
  • 混合导出类型和值

    • 尽量避免在同一个模块中同时导出值(如函数、变量)和类型。
    • 分开导出可以让类型的重用性变得更高,因为类型在编译后的 JavaScript 代码中会被删除。

避免这些危险信号可以帮助你写出更清晰、更容易维护的 TypeScript 代码。记住,模块化的核心是关注点分离,每个文件或模块都应该有一个清晰定义的角色和责任。