# 模块

从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。

模块在其自身的作用域里执行,而不是在全局作用域里;这意味着定义在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式 (opens new window)之一导出它们。 相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导入它们,可以使用 import形式 (opens new window)之一。

模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。

模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。 大家最熟知的JavaScript模块加载器是服务于Node.js的 CommonJS (opens new window)和服务于Web应用的Require.js (opens new window)

TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。

# 导出、导入

任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。这个其实和ES module的用法是类似的, 看下面举例子很明白

type.ts

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

hello.ts

import { Person } from "type.ts"
const p: Person = {name: 'x', age: 10}

这就是ES Module的用法,当然也支持类似 默认导出 default, 重命名 as

export { Perosn as NewPerson }
import { NewPerson as Person2 } from "type.ts"

对于使用 *的全局导出与导入,也是一样的。

# export = / import module =

TS为了兼容CommonJS和AMD所提供的一种方式

// a.ts
interface Name1 {
  name: string;
}
export = Name1;

// b.ts
import Name1 = require('./a');
const A: Name1 = { name: 'ss' };

# 使用其他的三方库

如果想要描述不是由ts编写的类库的类型, 我们需要声明类库锁提供的API。 我们称为声明是因为这不是 外部程序 的具体实现。 一般都是在 d.ts 文件里声明。类似于C语言的 .h 文件。一般会配合 package.json 中的 types字段

{
	"types": "index.d.ts"
}

# 外部模块

在Node.js里大部分工作是通过加载一个或多个模块实现的。 我们可以使用顶级的 export声明来为每个模块都定义一个.d.ts文件,但最好还是写在一个大的.d.ts文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用 module关键字并且把名字用引号括起来,方便之后import。 例如:

node.d.ts


declare module 'url' {
  export interface Url {
    protocol?: string;
    hostname?: string;
    pathname?: string;
  }
  export function parse(
    urlStr: string,
    parseQueryString?,
    slashesDenoteHost?
  ): Url;
}

declare module 'path' {
  export function normalize(p: string): string;
  export function join(...paths: any[]): string;
  export let sep: string;
}

demo.ts

/// <reference path="node.d.ts"/>
import * as URL from 'url';
import Path = require('path')
let myUrl = URL.parse('http://www.typescriptlang.org');
Path.join(['a', 'b'])

上面的 declare module 'url' 其实就是声明一个模块, 我们可以通过 import * as URL from 'url'或者import =的方式引。

/// <reference path="node.d.ts"/> 三斜线这个表明依赖这个声明

# 外部模块简写

一般外部模块的API很多, 我们很难一下子写完, 除非是作者提供的,如果作者还没有提供的, 我们可以简写一个, 以便我们可以再ts中快速的使用。

declare module "jquery"

简写模块里所有导出的类型将是any

# 模块声明通配符

某些模块加载器如SystemJS (opens new window)AMD (opens new window)支持导入非JavaScript内容。 它们通常会使用一个前缀或后缀来表示特殊的加载语法。 模块声明通配符可以用来表示这些情况。

declare module "*!text" {
    const content: string;
    export default content;
}
// Some do it the other way around.
declare module "json!*" {
    const value: any;
    export default value;
}

现在就可以使用了

import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);

这个可能确实用的少, 很多时候可能需要断言一下, 也能解决

# UMD模块

有些模块被设计成兼容多个模块加载器,或者不使用模块加载器(全局变量)。 它们以 UMD (opens new window)模块为代表。 这些库可以通过导入的形式或全局变量的形式访问。 例如:

lib.d.ts

export function isPrime(x: number): boolean;
export as namespace mathLib;

然后就可以正常使用了

import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // 错误: 不能在模块内使用全局定义。

它同样可以通过全局变量的形式使用,但只能在某个脚本(指不带有模块导入或导出的脚本文件)里。

mathLib.isPrime(2);

# 命名空间

“内部模块”现在叫做“命名空间”。 另外,任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。 这就避免了让新的使用者被相似的名称所迷惑。

namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }
  const lettersRegexp = /^[a-zA-Z]+$/;
  const numberRegexp = /^[0-9]+$/;

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return lettersRegexp.test(s);
    }
  }
  export class NumberOnlyValidator implements StringValidator {
    isAcceptable(s: string) {
      return numberRegexp.test(s);
    }
  }
}

const lettersOnlyValidator = new Validation.LettersOnlyValidator();
lettersOnlyValidator.isAcceptable('abc');

TIP

同时命名空间也可以分离到多个文件, 然后使用三斜线方法引用到一起

# 外部命名空间

比如一些通过<script>标签加载的库,不是通过模块加载器,它的声明文件使用内部模块来定义它的类型。

declare namespace AMap {
  interface Marker {
    new ();
  }
  export interface AMap {
    Marker: Marker;
  }
}

declare var AMap: AMap.AMap;

# 编译器模块定位

一个常见的错误是使用/// <reference>引用模块文件,应该使用import。 要理解这之间的区别,我们首先应该弄清编译器是如何根据 import路径(例如,import x from "...";import x = require("...")里面的...,等等)来定位模块的类型信息的。

编译器首先尝试去查找相应路径下的.ts.tsx再或者.d.ts。 如果这些文件都找不到,编译器会查找 外部模块声明。 回想一下,它们是在 .d.ts文件里声明的。

myModules.d.ts

// In a .d.ts file or .ts file that is not a module:
declare module "SomeModule" {
    export function fn(): string;
}

myOtherModule.ts

/// <reference path="myModules.d.ts" />
import * as m from "SomeModule";

TypeScript里模块的一个特点是不同的模块永远也不会在相同的作用域内使用相同的名字。 因为使用模块的人会为它们命名,所以完全没有必要把导出的符号包裹在一个命名空间里。

export namespace Shapes {
    export class Triangle { /* ... */ }
    export class Square { /* ... */ }
}


import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes?

上述的用法是不合适的,不应该对模块使用命名空间,使用命名空间是为了提供逻辑分组和避免命名冲突。

正确的做法

shapes.ts

export class Triangle { /* ... */ }
export class Square { /* ... */ }

shapeConsumer.ts

import * as shapes from "./shapes";
let t = new shapes.Triangle();
上次更新: 1/22/2025, 9:39:13 AM