TypeScript新特性

1,785 阅读24分钟

TypeScript新特性之项目引用(project references)


项目引用是TypeScript 3.0中的一项新功能,允许您将TypeScript程序构建为更小的部分。

通过这样做,您可以大大缩短构建时间,实现组件之间的逻辑分离,并以新的更好的方式组织代码。

我们还为tsc引入了一种新模式,即--build标志,它与项目引用协同工作,以实现更快的TypeScript构建。

示例项目

让我们看一个相当正常的程序,看看项目引用如何帮助我们更好地组织它。
想象一下,你有一个项目有两个模块,转换器和单元,以及每个模块的相应测试文件:

/src/converter.ts
/src/units.ts
/test/converter-tests.ts
/test/units-tests.ts
/tsconfig.json

测试文件导入实现文件并进行一些测试:

// converter-tests.ts
import * as converter from "../converter";

assert.areEqual(converter.celsiusToFahrenheit(0), 32);

以前,如果您使用单个tsconfig文件,则此结构很难处理:

1. 实现文件可以导入测试文件
2. 在输出文件夹名称中没有出现src的情况下,无法同时构建test和src,这可能是您不想要的
3. 仅更改实现文件中的内部结构需要再次检查测试,即使这不会导致新的错误
4. 仅更改测试需要再次对实现进行检查,即使没有任何改变

您可以使用多个tsconfig文件来解决其中的一些问题,但会出现新的问题:

1. 没有内置的最新检查,因此您最终总是运行两次tsc
2. 两次调用tsc会导致更多的启动时间开销
3. tsc -w无法一次在多个配置文件上运行

项目引用(project references)可以解决所有这些问题等等。

什么是项目引用(project references)?

tsconfig.json文件有一个新的顶级属性"references"。
它是一个对象数组,指定要引用的项目:

{
    "compilerOptions": {
        // The usual
    },
    "references": [
        { "path": "../src" }
    ]
}

每个引用的path属性可以指向包含tsconfig.json文件的目录,也可以指向配置文件本身(可以具有任何名称)。
当您引用项目时,会发生新的事情:

1. 从引用的项目导入模块将改为加载其输出声明文件(.d.ts)
2. 如果引用的项目生成outFile,则输出文件.d.ts文件的声明将在此项目中可见
3. 如果需要,构建模式(下面会提到)将自动构建引用的项目

通过分成多个项目,您可以大大提高类型检查和编译的速度,减少使用编辑器时的内存使用量,并改进程序逻辑分组的实施。

composite

引用的项目必须启用新的composite设置。
需要此设置以确保TypeScript可以快速确定在何处查找引用项目的输出。
启用composite标志会改变一些事情:

1. rootDir设置(如果未显式设置)默认为包含tsconfig文件的目录
2. 所有实现文件必须由include模式匹配或在files数组中列出。如果违反此约束,tsc将通知您未指定哪些文件
3. declaration必须打开

declarationMaps

我们还增加了对declaration source maps的支持。如果启用--declarationMap,您将能够使用编辑器功能,如"转到定义"和重命名,以在支持的编辑器中跨项目边界透明地导航和编辑代码。

以outFile为前缀

您还可以使用引用中的prepend选项启用前置依赖项的输出:

"references": [
   { "path": "../utils", "prepend": true }
]

预先设置项目将包括项目的输出高于当前项目的输出。
这适用于.js文件和.d.ts文件,源代码映射文件也将正确发出。

tsc只会使用磁盘上的现有文件来执行此过程,因此可以创建一个项目,其中无法生成正确的输出文件,因为某些项目的输出将在结果文件中出现多次。
例如:

   A
  ^ ^
 /   \
B     C
 ^   ^
  \ /
   D


在这种情况下,重要的是不要在每个参考文献中添加前缀,因为在D的输出中最终会得到两个A副本 - 这可能会导致意外结果。

项目引用的注意事项

项目引用有一些您应该注意的权衡。

因为依赖项目使用从其依赖项构建的.d.ts文件,所以您必须在克隆之后签入某些构建输出或构建项目,然后才能在编辑器中导航项目而不会看到虚假错误。
我们正在开发一个能够缓解这种情况的幕后.d.ts生成过程,但是现在我们建议告知开发人员他们应该在克隆之后构建它们。

此外,为了保持与现有构建工作流的兼容性,除非使用--build开关调用,否则tsc不会自动构建依赖项。
让我们了解更多关于--build的信息。

TypeScript的构建模式

期待已久的功能是TypeScript项目的智能增量构建。
在3.0中,您可以将-build标志与tsc一起使用。
这实际上是tsc的新入口点,其行为更像构建协调器而不是简单的编译器。

运行

tsc --build

(简称tsc -b)将执行以下操作:

1. 查找所有引用的项目
2. 检测它们是否是最新的
3. 按正确的顺序构建过时的项目

您可以为tsc -b提供多个配置文件路径(例如tsc -b src test)。
就像tsc -p一样,如果命名为tsconfig.json,则不需要指定配置文件名本身。

> tsc -b                                # 在当前目录中构建tsconfig.json
> tsc -b src                            # 构建src/tsconfig.json
> tsc -b foo/release.tsconfig.json bar  # 构建foo/release.tsconfig.json和构建bar/tsconfig.json

不要担心您在命令行上传递的排过序的文件 - 如果需要,tsc将重新排序它们,以便始终首先构建依赖项。
还有一些特定于tsc -b的标志:

--verbose: 打印详细日志记录以解释正在发生的事情(可能与任何其他标志组合)
--dry: 显示将要完成的但实际上不构建任何内容
--clean: 删除指定项目的输出(可以与--dry结合使用)
--force: 就好像所有项目都已过时一样
--watch: 监视模式(除了--verbose外,不得与任何标志组合使用)

注意事项

通常,除非出现noEmitOnError,否则tsc将在出现语法或类型错误时生成输出(.js和.d.ts)。
在增量构建系统中执行此操作将非常糟糕 - 如果您的一个过时的依赖项出现新错误,您只能看到它一次,因为后续构建将跳过构建现在最新的项目。
因此,tsc -b实际上就像为所有项目启用noEmitOnError一样。
如果您检查任何构建输出(.js,.d.ts,.d.ts.map等),您可能需要在某些源控制操作之后运行--force构建,具体取决于源控制工具是否保留
本地副本和远程副本之间的时间映射。

MSBuild

如果您有msbuild项目,则可以通过添加如下代码到您的proj文件来启用构建模式

<TypeScriptBuildMode>true</TypeScriptBuildMode>

这将启用自动增量构建和清洁。

请注意,与tsconfig.json/-p一样,不会遵循现有… - 应使用tsconfig文件管理所有设置。

一些团队已经设置了基于msbuild的工作流,其中tsconfig文件与他们配对的托管项目具有相同的隐式图表排序。
如果您的解决方案是这样的,您可以继续使用msbuild和tsc -p以及项目引用;
这些是完全可互操作的。

指导(Guidance)

整体结构

使用更多tsconfig.json文件,您通常需要使用配置文件继承来集中您的常用编译器选项。
这样,您可以在一个文件中更改设置,而不必编辑多个文件。

另一个好的做法是拥有一个"解决方案"tsconfig.json文件,该文件只引用了所有leaf-node项目。
这提供了一个简单的切入点;
例如,在TypeScript repo中,我们只运行tsc -b src来构建所有端点,因为我们列出了src/tsconfig.json中的所有子项目。请注意,从3.0开始,如果在tsconfig.json中至少有一个reference将不会针对空的files数组报错

您可以在TypeScript存储库中看到这些模式 -

src/tsconfig_base.json

src/tsconfig.json

src/tsc/tsconfig.json

作为关键示例。

构建相关模块

通常,使用相关模块transition a repo并不需要太多。
只需将tsconfig.json文件放在给定父文件夹的每个子目录中,并添加对这些配置文件的引用以匹配程序的预期分层。
您需要将outDir设置为输出文件夹的显式子文件夹,或将rootDir设置为所有项目文件夹的公共根目录。

构建outFiles

使用outFile进行编译的布局更灵活,因为相对路径无关紧要。
要记住的一件事是,您通常希望在"最后"项目之前不使用前置 - 这将改善构建时间并减少任何给定构建中所需的I/O量。
TypeScript repo本身就是一个很好的参考 - 我们有一些"库"项目和一些"端点"项目;
"端点"项目尽可能小,只吸引他们需要的库。

原文地址:
www.typescriptlang.org/docs/handbo…

TypeScript基础入门之Symbols

介绍

自ECMAScript 2015起,symbol成为了一种新的原生类型,就像number和string一样。

symbol类型的值是通过Symbol构造函数创建的。

let sym1 = Symbol();
let sym2 = Symbol("key"); // 可选的字符串key

Symbols是不可改变且唯一的。

let sym2 = Symbol("key");
let sym3 = Symbol("key");
sym2 === sym3; // false, symbols是唯一的

像字符串一样,symbols也可以被用做对象属性的键。

let sym = Symbol();

let obj = {
    [sym]: "value"
};

console.log(obj[sym]); // "value"

Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员。

const getClassNameSymbol = Symbol();

class C {
    [getClassNameSymbol](){
       return "C";
    }
}

let c = new C();
let className = c[getClassNameSymbol](); // "C"

众所周知的Symbols

除了用户定义的symbols,还有一些已经众所周知的内置symbols。 内置symbols用来表示语言内部的行为。

以下为这些symbols的列表:

Symbol.hasInstance

方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。

Symbol.isConcatSpreadable

布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。

Symbol.iterator

方法,被for-of语句调用。返回对象的默认迭代器。

Symbol.match

方法,被String.prototype.match调用。正则表达式用来匹配字符串。

Symbol.replace

方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。

Symbol.search

方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。

Symbol.species

函数值,为一个构造函数。用来创建派生对象。

Symbol.split

方法,被String.prototype.split调用。正则表达式来用分割字符串。

Symbol.toPrimitive

方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。

Symbol.toStringTag

方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。

Symbol.unscopables

对象,它自己拥有的属性会被with作用域排除在外。

迭代性

如果对象具有Symbol.iterator属性的实现,则该对象被视为可迭代。
一些内置类型,如Array,Map,Set,String,Int32Array,Uint32Array等,已经实现了Symbol.iterator属性。
对象上的Symbol.iterator函数负责返回值列表以进行迭代。

for..of语句

for..of

循环遍历可迭代对象,调用对象上的Symbol.iterator属性。下面是一个简单的for..of循环数组:

let someArray = [1, "string", false];

for (let entry of someArray) {
    console.log(entry); // 1, "string", false
}

for..of vs. for..in语句

for..of

for..in

语句都遍历列表;迭代的值是不同的,for..in返回正在迭代的对象上的键列表,而for..of返回正在迭代的对象的数值属性的值列表。下面展示一个对比的例子:

let list = [4, 5, 6];

for (let i in list) {
   console.log(i); // "0", "1", "2",
}

for (let i of list) {
   console.log(i); // "4", "5", "6"
}

另一个区别是

for..in

可以操作任何物体;它用作检查此对象的属性的方法。另一方面,

for..of

主要关注可迭代对象的值。Map和Set等内置对象实现了Symbol.iterator属性,允许访问存储的值。如下实例演示

let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";

for (let pet in pets) {
   console.log(pet); // "species"
}

for (let pet of pets) {
    console.log(pet); // "Cat", "Dog", "Hamster"
}

上面这段代码我在运行的时候是报错了的,不知道是不是官方哪里弄错了,也可能是需要做另外一些配置。如果您也遇到了跟我一样的错误,请留言指导

生成器

目标为 ES5 和 ES3

在针对ES5或ES3时,只允许在Array类型的值上使用迭代器。在非数组值上使用for循环是错误的,即使这些非数组值实现了Symbol.iterator属性也是如此。编译器将为

for..of

循环生成一个简单的for循环,例如:

let numbers = [1, 2, 3];
for (let num of numbers) {
    console.log(num);
}

编译后生成的代码如下

var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
    var num = numbers[_i];
    console.log(num);
}

在针对ECMAScipt 2015兼容引擎时,编译器将生成for..of循环以定位引擎中的内置迭代器实现。

TypeScript基础入门之模块(一)

模块

关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 "内部模块"现在称做"命名空间"。 "外部模块"现在则简称为"模块",这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

介绍

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

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

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

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

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

导出

导出声明

任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。

Validation.ts

export interface StringValidator {
  isAcceptable(s: string): boolean;
}

ZipCodeValidator.ts

export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string): boolean {
    return s.length === 5 && numberRegexp.test(s);
  }
}

导出语句

导出语句很便利,因为我们可能需要对导出的部分重命名,所以上面的例子可以这样改写:

export const numberRegexp = /^[0-9]+$/;

class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string): boolean {
    return s.length === 5 && numberRegexp.test(s);
  }
}

export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator }

重新导出

我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
ParseIntBasedZipCodeValidator.ts

export class ParseIntBasedZipCodeValidator {
  isAcceptable(s: string): boolean {
    return s.length === 5 && parseInt(s).toString() === s;
  }
}

export { ZipCodeValidator as RegExpBaseZipCodeValidator } from './ZipCodeValidator';

或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"。

AllValidators.ts

export * from "./StringValidator"; // exports interface 'StringValidator'
export * from "./ZipCodeValidator";  // exports class 'ZipCodeValidator'

导入

模块的导入操作与导出一样简单。 可以使用以下import形式之一来导入其它模块中的导出内容。

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

import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();

可以对导入内容重命名

import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();

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

import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

具有副作用的导入模块

尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块:

import "./my-module.js";

默认导出

每个模块都可以有一个default导出。 默认导出使用 default关键字标记;并且一个模块只能够有一个default导出。 需要使用一种特殊的导入形式来导入 default导出。

default导出十分便利。 比如,像JQuery这样的类库可能有一个默认导出 jQuery或$,并且我们基本上也会使用同样的名字jQuery或$导出JQuery。

JQuery.d.ts

declare let $: JQuery;
export default $;

App.ts

import $ from "JQuery";

$("button.continue").html( "Next Step..." );

类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。

ZipCodeValidator.ts

export default class ZipCodeValidator {
    static numberRegexp = /^[0-9]+$/;
    isAcceptable(s: string) {
        return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
    }
}

Test.ts

import validator from "./ZipCodeValidator";

let myValidator = new validator();

或者

StaticZipCodeValidator.ts

const numberRegexp = /^[0-9]+$/;

export default function (s: string) {
    return s.length === 5 && numberRegexp.test(s);
}

Test.ts

import validate from "./StaticZipCodeValidator";

let strings = ["Hello", "98052", "101"];

// Use function validate
strings.forEach(s => {
  console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});

default
导出也可以是一个值
OneTwoThree.ts

export default "123";

Log.ts

import num from "./OneTwoThree";

console.log(num); // "123"

export =
import = require()

CommonJS和AMD都有一个exports对象的概念,它包含了一个模块的所有导出内容。

它们也支持把exports替换为一个自定义对象。 默认导出就好比这样一个功能;然而,它们却并不相互兼容。 TypeScript模块支持 export =语法以支持传统的CommonJS和AMD的工作流模型。

export =语法定义一个模块的导出对象。 它可以是类,接口,命名空间,函数或枚举。

若要导入一个使用了export =的模块时,必须使用TypeScript提供的特定语法import module = require("module")。

ZipCodeValidator.ts

let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}
export = ZipCodeValidator;

Test.ts

import zip = require("./ZipCodeValidator");

// 尝试一些字符
let strings = ["Hello", "98052", "101"];

// 使用validator
let validator = new zip();

// 检测每个字符串,是否通过验证
strings.forEach(s => {
  console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});

TypeScript基础入门之模块(二)

生成模块代码

根据编译期间指定的模块目标,编译器将为Node.js(CommonJS),require.js(AMD),UMD,SystemJS或ECMAScript 2015本机模块(ES6)模块加载系统生成适当的代码。
有关生成的代码中的define, require 和 register调用的更多信息,请参阅每个模块加载器的文档。

下面这个简单的示例展示了导入和导出期间使用的名称如何转换为模块加载代码。

SimpleModule.ts

import m = require("mod");
export let t = m.something + 1;

AMD/RequireJS SimpleModule.js

define(["require", "exports", "./mod"], function (require, exports, mod_1) {
    exports.t = mod_1.something + 1;
});

CommonJS/Node SimpleModule.js

var mod_1 = require("./mod");
exports.t = mod_1.something + 1;

UMD SimpleModule.js

(function (factory) {
    if (typeof module === "object" && typeof module.exports === "object") {
        var v = factory(require, exports); if (v !== undefined) module.exports = v;
    }
    else if (typeof define === "function" && define.amd) {
        define(["require", "exports", "./mod"], factory);
    }
})(function (require, exports) {
    var mod_1 = require("./mod");
    exports.t = mod_1.something + 1;
});

System SimpleModule.js

System.register(["./mod"], function(exports_1) {
    var mod_1;
    var t;
    return {
        setters:[
            function (mod_1_1) {
                mod_1 = mod_1_1;
            }],
        execute: function() {
            exports_1("t", t = mod_1.something + 1);
        }
    }
});

Native ECMAScript 2015 modules SimpleModule.js

import { something } from "./mod";
export var t = something + 1;

简单实例

下面,我们整合了前面【TypeScript基础入门之模块(一)】文章中使用的Validator实现,只导出每个模块的单个命名导出。

要编译,我们必须在命令行上指定模块目标。
对于Node.js,使用--module commonjs;
对于require.js,请使用--module amd。如下

tsc --module commonjs Test.ts


编译时,每个模块将成为一个单独的.js文件。
与引用标记一样,编译器将遵循import语句来编译依赖文件。Validation.ts

export interface StringValidator {
  isAcceptable(s: string): boolean;
}

ZipCodeValidator.ts

import { StringValidator } from './Validation';

export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
  isAcceptable(s: string): boolean {
    return s.length === 5 && numberRegexp.test(s);
  }
}

LettersOnlyValidator.ts

import { StringValidator } from './Validation';

const letterRegexp = /^[A-Za-z]+/;

export class LettersOnlyValidator implements StringValidator {
  isAcceptable(s: string): boolean {
    return letterRegexp.test(s);
  }
}

Test.ts

import { StringValidator } from './Validation';
import { ZipCodeValidator } from './ZipCodeValidator';
import { LettersOnlyValidator } from './LettersOnlyValidator';

// 测试数据
let strings = ["Hello", "98052", "101"];
// 
let validators:{ [s: string]: StringValidator } = {};
validators["zip code validator"] = new ZipCodeValidator();
validators["letter validator"] = new LettersOnlyValidator();

strings.forEach((e) => {
  for (let name in validators) {
    console.log(`"${e}" - ${ validators[name].isAcceptable(e) ? "matches" : 'does not match'} ${name}`)
  }
});


编译后运行得到如下结果

"Hello" - does not match zip code validator
"Hello" - matches letter validator
"98052" - matches zip code validator
"98052" - does not match letter validator
"101" - does not match zip code validator
"101" - does not match letter validator

TypeScript基础入门之模块(三)

可选模块加载和其他高级加载方案

在某些情况下,您可能只想在某些条件下加载模块。在TypeScript中,我们可以使用下面显示的模式来实现此模式和其他高级加载方案,以直接调用模块加载器而不会丢失类型安全性。

编译器检测是否生成的JavaScript中使用了每个模块。如果模块标识符仅用作类型注释的一部分而从不用作表达式,则不会为该模块生成require调用。这种未使用的引用的省略是一种良好的性能优化,并且还允许可选地加载这些模块。

该模式的核心思想是

import id = require("...")
语句使我们能够访问模块公开的类型。
模块加载器是动态调用的(通过require),如下面的if块所示。
这利用了参考省略优化,因此模块仅在需要时加载。
为了使这个模式起作用,重要的是通过导入定义的符号仅用于类型位置(即从不在将被生成到JavaScript中的位置)。
为了保持类型安全,我们可以使用typeof关键字。
typeof关键字在类型位置使用时会生成值的类型,在本例中为模块的类型。

**示例:Node.js里的动态模块加载**

declare function require(moduleName: string): any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
    let ZipCodeValidator: typeof Zip = require("./ZipCodeValidator");
    let validator = new ZipCodeValidator();
    if (validator.isAcceptable("...")) { /* ... */ }
}

**示例:require.js里的动态模块加载**

declare function require(moduleNames: string[], onLoad: (...args: any[]) => void): void;

import * as Zip from "./ZipCodeValidator";

if (needZipValidation) {
    require(["./ZipCodeValidator"], (ZipCodeValidator: typeof Zip) => {
        let validator = new ZipCodeValidator.ZipCodeValidator();
        if (validator.isAcceptable("...")) { /* ... */ }
    });
}

**示例:System.js里的动态模块加载**

declare const System: any;

import { ZipCodeValidator as Zip } from "./ZipCodeValidator";

if (needZipValidation) {
    System.import("./ZipCodeValidator").then((ZipCodeValidator: typeof Zip) => {
        var x = new ZipCodeValidator();
        if (x.isAcceptable("...")) { /* ... */ }
    });
}

TypeScript基础入门之模块(四)

使用其他JavaScript库

要描述不是用TypeScript编写的库的形状,我们需要声明库公开的API。
我们称之为未定义实现"环境"的声明。
通常,这些是在.d.ts文件中定义的。
如果您熟悉C/C++,可以将它们视为.h文件。
我们来看几个例子。

外部模块

在Node.js中,大多数任务是通过加载一个或多个模块来完成的。
我们可以使用顶级导出声明在自己的.d.ts文件中定义每个模块,但将它们编写为一个较大的.d.ts文件会更方便。
为此,我们使用类似于环境名称空间的构造,但我们使用模块关键字和模块的引用名称,以便稍后导入。
例如:

node.d.ts (simplified excerpt)

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 var sep: string;
}

现在我们可以

/// <reference> node.d.ts
并且使用
import url = require("url");
import * as URL from "url"
加载模块。

/// <reference path="node.d.ts" />
import * as URL from "url";

let testUrl = URL.parse("https://www.gowhich.com");

外部模块简写

如果您不想在使用新模块之前花时间写出声明,则可以使用速记声明快速入门。
declarations.d.ts

declare module "hot-new-module";

从速记模块导入的所有内容都将具有any类型

import x, {y} from "hot-new-module";
x(y);

通配符模块声明

某些模块加载器(如SystemJS和AMD)允许导入非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;
}

现在您可以导入与"*text"或"json*"匹配的内容。

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

UMD模块

有些库设计用于许多模块加载器,或者没有模块加载(全局变量)。
这些被称为UMD模块。
可以通过导入或全局变量访问这些库。
例如:

math-lib.d.ts

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

然后,该库可用作模块中的导入:

import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // ERROR: can't use the global definition from inside a module

它也可以用作全局变量,但仅限于脚本内部。(脚本是没有导入或导出的文件。)

mathLib.isPrime(2);

TypeScript基础入门之模块(五)

构建模块的指南

导出尽可能接近顶级

使用您导出的东西时,模块的消费者应尽可能少地摩擦。
添加太多级别的嵌套往往很麻烦,因此请仔细考虑如何构建事物。

从模块导出命名空间是添加太多嵌套层的示例。
虽然名称空间有时会有用,但在使用模块时会增加额外的间接级别。
这很快就会成为用户的痛点,而且通常是不必要的。

导出类上的静态方法也有类似的问题 - 类本身会添加一层嵌套。
除非以明显有用的方式增加表达性或意图,否则请考虑简单地导出辅助函数。

如果您只导出单个类或函数,请使用export default

正如"顶级附近的出口"减少了模块消费者的摩擦,引入默认导出也是如此。
如果模块的主要用途是容纳一个特定的导出,那么您应该考虑将其导出为默认导出。
这使导入和实际使用导入更容易一些。
例如:

MyClass.ts

export default class SomeType {
  constructor() { ... }
}

MyFunc.ts

export default function getThing() { return "thing"; }

Consumer.ts

import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());

这对消费者来说是最佳的。
他们可以根据需要命名您的类型(在这种情况下为t),并且不必进行任何过多的点击来查找对象。
如果您要导出多个对象,请将它们全部放在顶层

MyThings.ts

export class SomeType { /* ... */ }
export function someFunc() { /* ... */ }

相反,导入时:
明确列出导入的名称
Consumer.ts

import { SomeType, someFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();

如果要导入大量内容,请使用命名空间导入模式

MyLargeModule.ts

export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }

Consumer.ts

import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();

重新导出扩展延伸

通常,您需要扩展模块的功能。
一个常见的JS模式是使用扩展来扩充原始对象,类似于JQuery扩展的工作方式。
正如我们之前提到的,模块不像全局命名空间对象那样合并。
建议的解决方案是不改变原始对象,而是导出提供新功能的新实体。
考虑模块Calculator.ts中定义的简单计算器实现。
该模块还导出一个辅助函数,通过传递输入字符串列表并在结尾写入结果来测试计算器功能。

export class Calculator {
    private current = 0;
    private memory = 0;
    private operator: string;

    protected processDigit(digit: string, currentValue: number) {
        if (digit >= "0" && digit <= "9") {
            return currentValue * 10 + (digit.charCodeAt(0) - "0".charCodeAt(0));
        }
    }

    protected processOperator(operator: string) {
        if (["+", "-", "*", "/"].indexOf(operator) >= 0) {
            return operator;
        }
    }

    protected evaluateOperator(operator: string, left: number, right: number): number {
        switch (this.operator) {
            case "+": return left + right;
            case "-": return left - right;
            case "*": return left * right;
            case "/": return left / right;
        }
    }

    private evaluate() {
        if (this.operator) {
            this.memory = this.evaluateOperator(this.operator, this.memory, this.current);
        }
        else {
            this.memory = this.current;
        }
        this.current = 0;
    }

    public handleChar(char: string) {
        if (char === "=") {
            this.evaluate();
            return;
        }
        else {
            let value = this.processDigit(char, this.current);
            if (value !== undefined) {
                this.current = value;
                return;
            }
            else {
                let value = this.processOperator(char);
                if (value !== undefined) {
                    this.evaluate();
                    this.operator = value;
                    return;
                }
            }
        }
        throw new Error(`Unsupported input: '${char}'`);
    }

    public getResult() {
        return this.memory;
    }
}

export function test(c: Calculator, input: string) {
    for (let i = 0; i < input.length; i++) {
        c.handleChar(input[i]);
    }

    console.log(`result of '${input}' is '${c.getResult()}'`);
}

这是使用暴露测试功能的计算器的简单测试。

import { Calculator, test } from "./Calculator";


let c = new Calculator();
test(c, "1+2*33/11="); // prints 9

现在扩展这个以添加对10以外基数的输入的支持,让我们创建ProgrammerCalculator.tsProgrammerCalculator.ts

import { Calculator } from "./Calculator";

class ProgrammerCalculator extends Calculator {
    static digits = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"];

    constructor(public base: number) {
        super();
        const maxBase = ProgrammerCalculator.digits.length;
        if (base <= 0 || base > maxBase) {
            throw new Error(`base has to be within 0 to ${maxBase} inclusive.`);
        }
    }

    protected processDigit(digit: string, currentValue: number) {
        if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
            return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
        }
    }
}

// Export the new extended calculator as Calculator
export { ProgrammerCalculator as Calculator };

// Also, export the helper function
export { test } from "./Calculator";

新模块ProgrammerCalculator导出类似于原始Calculator模块的API形状,但不会扩充原始模块中的任何对象。
这是我们的ProgrammerCalculator类的测试:TestProgrammerCalculator.ts

import { Calculator, test } from "./ProgrammerCalculator";

let c = new Calculator(2);
test(c, "001+010="); // prints 3

不要在模块中使用名称空间

首次迁移到基于模块的组织时,常见的趋势是将导出包装在额外的命名空间层中。
模块有自己的范围,只有模块外部才能看到导出的声明。
考虑到这一点,在使用模块时,命名空间提供的值很小(如果有的话)。

在组织方面,命名空间可以方便地将全局范围内与逻辑相关的对象和类型组合在一起。
例如,在C#中,您将在System.Collections中找到所有集合类型。
通过将我们的类型组织成分层命名空间,我们为这些类型的用户提供了良好的“发现”体验。
另一方面,模块必然存在于文件系统中。
我们必须通过路径和文件名来解决它们,因此我们可以使用逻辑组织方案。
我们可以在 /collections/generic/文件夹中包含一个列表模块。

命名空间对于避免在全局范围内命名冲突很重要。
例如,您可能拥有My.Application.Customer.AddForm和My.Application.Order.AddForm - 两个具有相同名称但具有不同名称空间的类型。
然而,这不是模块的问题。
在一个模块中,没有合理的理由让两个具有相同名称的对象。
从消费方面来看,任何给定模块的消费者都会选择他们用来引用模块的名称,因此不可能发生意外命名冲突。

有关模块和命名空间的更多讨论,请参阅命名空间和模块。

以下所有内容都是模块结构的红色标志。如果其中任何一个适用于您的文件,请仔细检查您是否尝试命名外部模块:

1. 一个文件,其唯一的顶级声明是导出命名空间Foo {...}(删除Foo并将所有内容"向上移动"一个级别)
2. 具有单个导出类或导出功能的文件(请考虑使用导出默认值)
3. 具有相同```export namespace Foo {```的多个文件在顶层(不要认为这些将组合成一个Foo!)

TypeScript基础入门之命名空间(一)

命名空间

关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

介绍

这篇文章描述了如何在TypeScript里使用命名空间(之前叫做“内部模块”)来组织你的代码。 就像我们在术语说明里提到的那样,“内部模块”现在叫做“命名空间”。 另外,任何使用 module关键字来声明一个内部模块的地方都应该使用namespace关键字来替换。 这就避免了让新的使用者被相似的名称所迷惑。

第一步

我们先来写一段程序并将在整篇文章中都使用这个例子。 我们定义几个简单的字符串验证器,假设你会使用它们来验证表单里的用户输入或验证外部数据。

所有的验证器都放在一个文件里

interface StringValidator {
    isAcceptable(s: string): boolean;
}

let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;

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

class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string) {
        return s.length === 5 && numberRegexp.test(s);
    }
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        let isMatch = validators[name].isAcceptable(s);
        console.log(`'${ s }' ${ isMatch ? "matches" : "does not match" } '${ name }'.`);
    }
}

命名空间

随着更多验证器的加入,我们需要一种手段来组织代码,以便于在记录它们类型的同时还不用担心与其它对象产生命名冲突。 因此,我们把验证器包裹到一个命名空间内,而不是把它们放在全局命名空间下。

下面的例子里,把所有与验证器相关的类型都放到一个叫做Validation的命名空间里。 因为我们想让这些接口和类在命名空间之外也是可访问的,所以需要使用 export。 相反的,变量 lettersRegexp和numberRegexp是实现的细节,不需要导出,因此它们在命名空间外是不能访问的。 在文件末尾的测试代码里,由于是在命名空间之外访问,因此需要限定类型的名称,比如 Validation.LettersOnlyValidator。

使用命名空间的验证器

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 ZipCodeValidator implements StringValidator {
        isAcceptable(s: string) {
            return s.length === 5 && numberRegexp.test(s);
        }
    }
}

// Some samples to try
let strings = ["Hello", "98052", "101"];

// Validators to use
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();

// Show whether each string passed each validator
for (let s of strings) {
    for (let name in validators) {
        console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`);
    }
}

TypeScript基础入门之命名空间(二)

继续上篇文章[TypeScript基础入门之命名空间(一)]

跨文件拆分

当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。

多文件名称空间

现在,我们把Validation命名空间分割成多个文件。 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。 因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。 我们的测试代码保持不变。


Validation.ts

namespace Validation {
  export interface StringValidator {
    isAcceptable(s: string): boolean;
  }
}

LettersOnlyValidator.ts

/// <reference path="Validation.ts" />

namespace Validation {
  const letterRegexp = /^[A-Za-z]+/;

  export class LettersOnlyValidator implements StringValidator {
    isAcceptable(s: string): boolean {
      return letterRegexp.test(s);
    }
  }
}

ZipCodeValidator.ts

/// <reference path="Validation.ts" />
namespace Validation {
  export const numberRegexp = /^[0-9]+$/;

  export class ZipCodeValidator implements StringValidator {
    isAcceptable(s: string): boolean {
      return s.length === 5 && numberRegexp.test(s);
    }
  }
}

Test.ts

/// <reference path="Validation.ts" />
/// <reference path="LettersOnlyValidator.ts" />
/// <reference path="ZipCodeValidator.ts" />

// 测试数据
let strings = ["Hello", "98052", "101"];
// 
let validators:{ [s: string]: Validation.StringValidator } = {};
validators["zip code validator"] = new Validation.ZipCodeValidator();
validators["letter validator"] = new Validation.LettersOnlyValidator();

strings.forEach((e) => {
  for (let name in validators) {
    console.log(`"${e}" - ${ validators[name].isAcceptable(e) ? "matches" : 'does not match'} ${name}`)
  }
});

编译运行后的结果如下

$ tsc --outFile src/module_demo/Test.js src/module_demo/Test.ts
$ node src/module_demo/Test.js
"Hello" - does not match zip code validator
"Hello" - matches letter validator
"98052" - matches zip code validator
"98052" - does not match letter validator
"101" - does not match zip code validator
"101" - does not match letter validator


一旦涉及多个文件,我们需要确保加载所有已编译的代码。
有两种方法可以做到这一点。
首先,我们可以使用--outFile标志使用连接输出将所有输入文件编译为单个JavaScript输出文件:

tsc --outFile sample.js Test.ts

编译器将根据文件中存在的引用标记自动排序输出文件。
您还可以单独指定每个文件:

tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts

或者,我们可以使用每个文件编译(默认)为每个输入文件发出一个JavaScript文件。
如果生成了多个JS文件,我们需要在我们的网页上使用<script>标签以适当的顺序加载每个发出的文件,例如:

<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidator.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />

TypeScript基础入门之命名空间(三)

继续上篇文章[TypeScript基础入门之命名空间(二)]

别名

另一种可以简化名称空间使用方法的方法是使用import q = x.y.z为常用对象创建较短的名称。
不要与用于加载模块的import x = require("name")语法相混淆,此语法只是为指定的符号创建别名。
您可以将这些类型的导入(通常称为别名)用于任何类型的标识符,包括从模块导入创建的对象。

namespace Shapes {
    export namespace Polygons {
        export class Triangle { }
        export class Square { }
    }
}

import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // 类似于 'new Shapes.Polygons.Square()'

请注意,我们不使用require关键字;
相反,我们直接从我们导入的符号的限定名称中分配。
这类似于使用var,但也适用于导入符号的类型和名称空间含义。
重要的是,对于值,import是与原始符号的不同引用,因此对别名var的更改不会反映在原始变量中。

使用其他JavaScript库

要描述不是用TypeScript编写的库的形状,我们需要声明库公开的API。
因为大多数JavaScript库只公开一些顶级对象,所以命名空间是表示它们的好方法。

我们称之为未定义实现“环境”的声明。
通常,这些是在.d.ts文件中定义的。
如果您熟悉C/C++,可以将它们视为.h文件。
我们来看几个例子。

环境命名空间

流行的库D3在名为d3的全局对象中定义其功能。
因为此库是通过<script>标记(而不是模块加载器)加载的,所以它的声明使用命名空间来定义其形状。
要让TypeScript编译器看到这个形状,我们使用环境命名空间声明。
例如,我们可以开始编写如下:D3.d.ts(简化摘录)

declare namespace D3 {
    export interface Selectors {
        select: {
            (selector: string): Selection;
            (element: EventTarget): Selection;
        };
    }

    export interface Event {
        x: number;
        y: number;
    }

    export interface Base extends Selectors {
        event: Event;
    }
}

declare var d3: D3.Base;

TypeScript基础入门之命名空间和模块

命名空间和模块

关于术语的说明:值得注意的是,在TypeScript 1.5中,命名法已经改变。
"内部模块"现在是"命名空间"。
"外部模块"现在只是"模块",以便与ECMAScript 2015的术语保持一致(即module X {相当于现在首选的namespace X {)。

介绍

本文概述了使用TypeScript中的命名空间和模块组织代码的各种方法。
我们还将讨论如何使用命名空间和模块的一些高级主题,并解决在TypeScript中使用它们时常见的一些陷阱。

有关模块的更多信息,请参阅模块文档。
有关命名空间的更多信息,请参阅命名空间文档。

使用命名空间

命名空间只是全局命名空间中的JavaScript对象。
这使命名空间成为一个非常简单的构造。
它们可以跨多个文件,并且可以使用--outFile连接。
命名空间可以是在Web应用程序中构建代码的好方法,所有依赖项都包含在HTML页面中的<script>标记中。

就像所有全局命名空间污染一样,很难识别组件依赖性,尤其是在大型应用程序中。

使用模块

就像命名空间一样,模块可以包含代码和声明。
主要区别在于模块声明了它们的依赖关系。

模块还依赖于模块加载器(例如CommonJs/Require.js)。
对于小型JS应用程序而言,这可能不是最佳选择,但对于大型应用程序,成本具有长期模块化和可维护性优势。
模块为捆绑提供了更好的代码重用,更强的隔离和更好的工具支持。

值得注意的是,对于Node.js应用程序,模块是构造代码的默认方法和推荐方法。

从ECMAScript 2015开始,模块是该语言的本机部分,并且应该受到所有兼容引擎实现的支持。
因此,对于新项目,模块将是推荐的代码组织机制。

命名空间和模块的缺陷

下面我们将描述使用命名空间和模块时的各种常见缺陷,以及如何避免它们。

///-ing a module

一个常见的错误是尝试使用

/// <reference ... />
语法来引用模块文件,而不是使用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示例使用的node.d.ts文件的方式。

无需命名空间

如果您要将程序从命名空间转换为模块,则可以很容易地得到如下所示的文件:

shapes.ts

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

这里的顶级模块Shapes无缘无故地包装了Triangle和Square。
这对您的模块的消费者来说是令人困惑和恼人的:

shapeConsumer.ts

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

TypeScript中模块的一个关键特性是两个不同的模块永远不会为同一范围提供名称。
因为模块的使用者决定分配它的名称,所以不需要主动地将命名空间中的导出符号包装起来。

为了重申您不应该尝试命名模块内容的原因,命名空间的一般概念是提供构造的逻辑分组并防止名称冲突。
由于模块文件本身已经是逻辑分组,并且其顶级名称由导入它的代码定义,因此不必为导出的对象使用其他模块层。

这是一个修改过的例子:
shapes.ts

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

shapeConsumer.ts

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

模块的权衡

正如JS文件和模块之间存在一对一的对应关系一样,TypeScript在模块源文件与其发出的JS文件之间具有一对一的对应关系。
这样做的一个结果是,根据您定位的模块系统,无法连接多个模块源文件。
例如,在定位commonjs或umd时不能使用outFile选项,但使用TypeScript 1.8及更高版本时,可以在定位amd或system时使用outFile。