这是我参与「第五届青训营 」伴学笔记创造活动的第 11 天
TypeScript 介绍
- TypeScript 是 JavaScript 的超集,供给了 JavaScript 的一切功用,并供给了可选的静态类型、Mixin、类、接口和泛型等特性。
- TypeScript 的方针是经过其类型体系协助及早发现过错并进步 JavaScript 开发功率。
- 经过 TypeScript 编译器或 Babel 转码器转译为 JavaScript 代码,可运转在任何浏览器,任何操作体系。
- 任何现有的 JavaScript 程序都能够运转在 TypeScript 环境中,并只对其间的 TypeScript 代码进行编译。
- 在完整保存 JavaScript 运转时行为的基础上,经过引进静态类型界说来进步代码的可维护性,减少或许呈现的 bug。
- 永久不会改动 JavaScript 代码的运转时行为,例如数字除以零等于 Infinity。这意味着,假定将代码从 JavaScript 迁移到 TypeScript ,即便 TypeScript 认为代码有类型过错,也能够保证以相同的办法运转。
- 对 JavaScript 类型进行了扩展,添加了例如
any
、unknown
、never
、void
。 - 一旦 TypeScript 的编译器完成了查看代码的作业,它就会 擦除 类型以生成终究的“已编译”代码。这意味着一旦代码被编译,生成的一般 JS 代码便没有类型信息。这也意味着 TypeScript 绝不会依据它推断的类型更改程序的 行为。最重要的是,虽然或许会在编译过程中看到类型过错,但类型体系自身与程序怎么运转无关。
- 在较大型的项目中,能够在单独的文件 tsconfig.json 中声明 TypeScript 编译器的装备,并细化地调整其作业办法、严格程度、以及将编译后的文件存储在何处。
三斜杠指令
- 三斜杠指令是包括单个 XML 符号的单行注释,注释的内容会做为编译器指令运用。
- 三斜线指令仅可放在包括它的文件的最顶端。一个三斜线指令的前面只能呈现单行或多行注释,这包括其它的三斜线指令。假定它们呈现在一个句子或声明之后,那么它们会被作为一般的单行注释,而且不具有特别的寓意。
/// <reference path="..." />
-
/// <reference path="..." />
引证指令是三斜线指令中最常见的一种,它用于声明文件间的依靠,告诉编译器在编译过程中要引进的额定的文件。 - 当运用
--out
或--outFile
时,它也能够做为调整输出内容次序的一种办法,文件在输出文件内容中的方位与经过预处理后的输入次序共同。 - 编译器会对输入文件进行预处理来解析一切三斜线引证指令。在这个过程中,额定的文件会加到编译过程中,该过程从一组 根文件 开端;这些文件是在命令行中指定或是在
tsconfig.json
中的"files"
列表里指定;这些 根文件 按指定的次序进行预处理。在一个文件被参加列表前,它包括的一切三斜线引证都要被处理,还有它们包括的方针。三斜线引证以它们在文件里呈现的次序,运用深度优先的办法解析。 - 一个三斜线引证途径是相关于包括它的文件的,假定不是根文件。引证不存在的文件会报错,一个文件用三斜线指令引证自己也会报错。
- 假定指定了
noResolve
编译选项,三斜线引证会被忽略;它们不会添加新文件,也不会改动给定文件的次序。
/// <reference types="..." />
- 与
/// <reference path="..." />
指令类似,三斜线类型引证指令是用来声明依靠 的;一个/// <reference types="..." />
指令则声明晰对某个包的依靠。 - 对这些包的姓名的解析与在
import
句子里对模块名的解析类似。能够简略地把三斜线类型引证指令作为import
声明包的一种简略办法。 - 例如,把
/// <reference types="node" />
引进到声明文件,表明这个文件运用了@types/node/index.d.ts
里边声明的姓名;而且,这个包需求在编译阶段与声明文件一同被包括进来。 - 仅当在你需求写一个
d.ts
文件时才运用这个指令。 - 关于那些在编译阶段生成的声明文件,编译器会自动地添加
/// <reference types="..." />
;当且仅当 结果文件中运用了引证的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />
句子。 - 若要在
.ts
文件里声明一个对@types
包的依靠,运用--types
命令行选项或在tsconfig.json
里指定types
。 - 经过指令包括的办法,假定咱们每一个文件都写一个这种指令,会非常的烦,所以能够在
tsconfig.json
里边装备,分别是types
指定文件,typeRoots
指定目录,挑选相同即可。
/// <reference no-default-lib="true"/>
- 这个指令把一个文件符号成 默许库。你会在
lib.d.ts
文件和它不同的变体的顶部看到这个注释。 - 这个指令告诉编译器在编译过程中不要包括默许库(即
lib.d.ts
)。这与在命令行上运用--noLib
类似。 - 还要留意,当传递了
--skipDefaultLibCheck
时,编译器只会忽略查看带有/// <reference no-default-lib="true"/>
的文件。
命名空间
TypeScript 1.5 里术语名现已发生了改动。“内部模块”现在称做“命名空间”。“外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015 里的术语保持共同。另外,任何运用
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);
}
}
let strings = ["Hello", "98052", "101"];
// 运用的验证器
let validators: { [s: string]: StringValidator } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
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);
}
}
}
let strings = ["Hello", "98052", "101"];
// 运用的验证器
let validators: { [s: string]: Validation.StringValidator } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
for (let s of strings) {
for (let name in validators) {
console.log(
`"${s}" - ${validators[name]!.isAcceptable(s) ? "matches" : "does not match"
} '${name}'`
);
}
}
- 当运用变得越来越大时,咱们需求将代码分离到不同的文件中以便于维护。现在,咱们把
Validation
命名空间分割成多个文件。虽然是不同的文件,它们仍是同一个命名空间,而且在运用的时分就如同它们在一个文件中界说的相同。由于不同文件之间存在依靠联系,所以咱们参加了引证标签来告诉编译器文件之间的关联。
Validation.ts
namespace Validation {
export interface StringValidator {
isAcceptable(s: string): boolean;
}
}
LettersOnlyValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable(s: string) {
return lettersRegexp.test(s);
}
}
}
ZipCodeValidator.ts
/// <reference path="Validation.ts" />
namespace Validation {
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
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"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
for (let s of strings) {
for (let name in validators) {
console.log(
`"${s}" - ${
validators[name].isAcceptable(s) ? "matches" : "does not match"
} ${name}`
);
}
}
当涉及到多文件时,咱们有必要保证一切编译后的代码都被加载了。咱们有两种办法。
第一种办法,把一切的输入文件编译为一个输出文件,需求运用 --outFile
符号:
tsc --outFile sample.js Test.ts
编译器会依据源码里的引证标签自动地对输出进行排序。你也能够单独地指定每个文件。
tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
第二种办法,咱们能够运用按文件编译(默许)为每个输入文件生成一个 JavaScript 文件。然后,在页面上经过<script>
标签把一切生成的 JavaScript 文件按正确的次序引进来。
<!-- MyTestPage.html -->
<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" />
- 另一种简化命名空间操作的办法是运用
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(); // Same as 'new Shapes.Polygons.Square()'
留意,咱们并没有运用 require
关键字,而是直接运用导入符号的约束名赋值。这与运用var
类似,但它还适用于导入符号的类型和命名空间意义。重要的是,关于值来讲,import
会生成与原始符号不同的引证,所以改动别号的值并不会影响原始变量的值。
- 为了描绘不是用 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;
模块
- 从 ECMAScript 2015 开端,JavaScript 有了模块的概念。TypeScript 与 es6 的模块基本是共同的。
- TypeScript 与 ECMAScript 2015 相同,任何包括尖端
import
或许export
的文件都被当成一个模块。 - 相反地,假定一个文件不带有尖端的
import
或许export
声明,那么它的内容被视为大局可见的(因而对模块也是可见的)。
导出
导出声明
任何声明(例如变量、函数、类、类型别号或接口)都能够经过添加 export
关键字来导出。
// StringValidator.ts
export interface StringValidator {
isAcceptable(s: string): boolean;
}
// ZipCodeValidator.ts
import { StringValidator } from "./StringValidator";
export const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
导出句子
当咱们需求对导出的部分重命名时,导出句子很便利,所以上面的比如能够这样改写:
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
从头导出
咱们经常会去扩展其它模块,而且只导出那个模块的部分内容。从头导出功用并不会在当时模块导入那个模块或界说一个新的局部变量。
// ParseIntBasedZipCodeValidator.ts
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
// 导出原先的验证器但做了重命名
export { ZipCodeValidator as RegExpBasedZipCodeValidator } from "./ZipCodeValidator";
或许一个模块能够包裹多个模块,并把他们导出的内容联合在一同经过语法:export * from "module"
。
// AllValidators.ts
export * from "./StringValidator"; // exports 'StringValidator' interface
export * from "./ZipCodeValidator"; // exports 'ZipCodeValidator' class and 'numberRegexp' constant value
export * from "./ParseIntBasedZipCodeValidator"; // exports the 'ParseIntBasedZipCodeValidator' class
// and re-exports 'RegExpBasedZipCodeValidator' as alias
// of the 'ZipCodeValidator' class from 'ZipCodeValidator.ts' module.
导入
导入一个模块中的单个导出内容
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";
导入类型
从 TypeScript 3.8,能够运用 import
句子或运用 import type
导入类型。
import { APIResponseType } from "./api";
// 显式运用导入类型
import type { APIResponseType } from "./api";
// 显式导入一个值(getResponse)和一个类型(APIResponseType)
import { getResponse, type APIResponseType} from "./api";
默许导出
- 每个模块都能够有一个
default
导出。 - 默许导出运用
default
关键字符号;而且一个模块只能够有一个default
导出。 - 比方,像 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();
-
default
导出也能够是一个值。
// OneTwoThree.ts
export default "123";
// Log.ts
import num from "./OneTwoThree";
console.log(num); // "123"
export = 和 import = require()
- CommonJS 和 AMD 都有一个
exports
方针的概念,它包括一个模块的一切导出。 -
exports
能够被赋值为一个方针
, 这种情况下其效果就类似于 es6 语法里的默许导出,即export default
语法了。虽然效果类似,可是export default
语法并不能兼容 CommonJS 和 AMD 的exports
。 - 为了支撑 CommonJS 和 AMD 的
exports
, TypeScript 供给了export =
语法。 -
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"];
let validator = new zip();
strings.forEach((s) => {
console.log(
`"${s}" - ${validator.isAcceptable(s) ? "matches" : "does not match"}`
);
});
可选的模块加载和其它高档加载场景
- 有时分,你只想在某种条件下才加载某个模块。在 TypeScript 里,运用下面的办法来完成它和其它高档的加载场景,咱们能够直接调用模块加载器而且能够保证类型彻底。
- 编译器会检测是否每个模块都会在生成的 JavaScript 中用到。假定一个模块标识符只在类型注解部分运用,而且彻底没有在表达式中运用时,就不会生成
require
这个模块的代码。 - 这种办法的核心是
import id = require("...")
句子能够让咱们拜访模块导出的类型。模块加载器会被动态调用(经过require
),就像下面if
代码块里那样。它利用了省略引证的优化,所以模块只在被需求时加载。为了让这个模块作业,必定要留意import
界说的标识符只能在表明类型处运用(不能在会转换成 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("...")) {
/* ... */
}
}
运用其他 JavaScript 库
要想描绘非 TypeScript 编写的类库的类型,咱们需求声明类库所暴露出的 API。咱们叫它声明由于它不是“外部程序”的详细完成。它们一般是在 .d.ts
文件里界说的。假定你了解 C/C++,你能够把它们作为 .h
文件。
外部模块
在 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 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 myUrl = URL.parse("https://www.typescriptlang.org");
外部模块简写
假定你不想在运用一个新模块之前花时间去编写声明,你能够采用声明的简写办法以便能够快速运用它。
// 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;
}
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); // 过错: 不能在模块内运用大局界说。
它相同能够经过大局变量的办法运用,但只能在某个脚本(指不带有模块导入或导出的脚本文件)里。
mathLib.isPrime(2);
构建模块的指南
尽或许地在顶层导出
- 用户应该更容易地运用你模块导出的内容,嵌套层次过多会变得难以处理。
- 从你的模块中导出一个命名空间便是一个添加嵌套的比如。虽然命名空间有时分有它们的用处,在运用模块的时分它们额定地添加了一层,这对用户来说是很不便的而且一般是剩余的。
- 导出类的静态办法也有相同的问题 – 这个类自身就添加了一层嵌套。除非它能便利表述或便于清晰运用,否则请考虑直接导出一个辅佐办法。
假定仅导出单个class或function,运用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
模块里界说了一个简略的计算器完成。这个模块相同供给了一个辅佐函数来测试计算器的功用,经过传入一系列输入的字符串并在最终给出结果。
// 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()}'`);
}
下面运用导出的 test
函数来测试计算器。
// TestCalculator.ts
import { Calculator, test } from "./Calculator";
let c = new Calculator();
test(c, "1+2*33/11="); // 9
现在扩展它,添加支撑输入其它进制(十进制以外)。
// ProgrammerCalculator.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 };
export { test } from "./Calculator";
新的 ProgrammerCalculator
模块导出的 API 与原先的 Calculator
模块很类似,但却没有改动原模块里的方针。
// TestProgrammerCalculator.ts
import { Calculator, test } from "./ProgrammerCalculator";
let c = new Calculator(2);
test(c, "001+010="); // 3
模块里不要运用命名空间
当初次进入基于模块的开发办法时,或许总会控制不住要将导出包裹在一个命名空间里。模块具有其自己的效果域,而且只要导出的声明才会在模块外部可见。记住这点,命名空间在运用模块时几乎没什么价值。
在安排方面,命名空间关于在大局效果域内对逻辑上相关的方针和类型进行分组是很便利的。例如,在 C# 里,你会从System.Collections
里找到一切集合的类型。经过将类型有层次地安排在命名空间里,能够便利用户找到与运用那些类型。可是,模块自身现已存在于文件体系之中,咱们有必要经过途径和文件名找到它们,这现已供给了一种逻辑上的安排办法。例如咱们能够创立/collections/generic/
文件夹,把相应模块放在这儿边。
命名空间对解决大局效果域里命名抵触来说是很重要的。比方,你能够有一个My.Application.Customer.AddForm
和 My.Application.Order.AddForm
— 两个类型的姓名相同,但命名空间不同。可是,这关于模块来说却不是一个问题。在一个模块里,没有理由两个方针具有同一个姓名。从模块的运用视点来说,运用者会挑出他们用来引证模块的姓名,所以也没有理由发生重名的情况。
常见过错
- 文件的顶层声明是
export namespace Foo { ... }
(删除Foo
并把一切内容向上层移动一层)。 - 文件只要一个
export class
或export function
(考虑运用export default
)。 - 多个文件的顶层具有相同的
export namespace Foo {
(不要认为这些会兼并到一个Foo
中!)。
命名空间和模块
运用命名空间
- 命名空间是一种特定于 TypeScript 的代码安排办法。
- 命名空间只是在大局命名空间中一个一般的带有姓名的 JavaScript 方针,这使得命名空间成为一个非常简略的结构来运用。
- 与模块不同,它们能够跨过多个文件,并能够经过
--outFile
标志结合在一同。 - 命名空间是在 Web 运用程序中构建代码的好办法,你能够把一切依靠都放在 HTML 页面的
<script>
标签里。 - 但就像其它的大局命名空间污染相同,它很难去识别组件之间的依靠联系,尤其是在大型的运用中。
运用模块
- 像命名空间相同,模块能够包括代码和声明。不同的是模块能够声明它的依靠。
- 模块还依靠于模块加载器(例如 CommonJs/Require.js)或支撑 ES 模块的运转时。关于小型的JS运用来说或许没必要,可是关于大型运用,这一点点的花费会带来持久的模块化和可维护性上的便利。模块也供给了更好的代码重用,更强的封闭性以及更好的运用工具进行优化。
- 关于 Node.js 运用来说,模块是默许并引荐的安排代码的办法,咱们主张在现代代码中运用模块而不是称号空间。
- 从 ECMAScript 2015 开端,模块成为了言语内置的部分,应该会被一切正常的解说引擎所支撑。因而,关于新项目来说引荐运用模块做为安排代码的办法。
常见圈套
这部分咱们会描绘常见的命名空间和模块的运用圈套和怎么去防止它们。
- 一个常见的过错是运用
/// <reference ... />
语法来引证模块文件,而不是运用import
句子。要了解这之间的差异,咱们首要应该弄清编译器是怎么依据import
途径(例如import x from "...";
或import x = require("...")
里边的...
)来定位模块的类型信息的。编译器首要尝试去查找相应途径下的.ts
,.tsx
,然后是.d.ts
。假定这些文件都找不到,编译器会查找外部模块声明。回想一下,这些需求在.d.ts
文件中声明。
myModules.d.ts
// 在 .d.ts 文件或不是模块的 .ts 文件中:
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 以上的版别,当target
为amd
或system
时能够运用outFile
选项。
声明兼并
- TypeScript 中有些独特的概念能够在类型层面上描绘 JavaScript 方针的模型。这其间尤其独特的一个比如是“声明兼并”的概念。
- “声明兼并”是指编译器将针对同一个姓名的两个独立声明兼并为单一声明。兼并后的声明一起具有原先两个声明的特性。任何数量的声明都可被兼并;不局限于两个声明。
- 了解了这个概念,将有助于操作现有的 JavaScript 代码。一起,也会有助于了解更多高档抽象的概念。
基础概念
TypeScript 中的声明会创立以下三种实体之一:命名空间,类型或值。下表说明晰声明类型都创立了什么实体:
声明类型 | 创立了命名空间 | 创立了类型 | 创立了值 |
---|---|---|---|
Namespace | √ | √ | |
Class | √ | √ | |
Enum | √ | √ | |
Interface | √ | ||
Type Alias | √ | ||
Function | √ | ||
Variable | √ |
创立命名空间的声明会新建一个命名空间,它包括了用(.
)符号来拜访时运用的姓名。创立类型的声明是:用声明的模型创立一个类型并绑定到给定的姓名上。最终,创立值的声明会创立在 JavaScript 输出中看到的值。
兼并接口
最简略也最常见的声明兼并类型是接口兼并。从底子上说,兼并的机制是把双方的成员放到一个同名的接口里。
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = { height: 5, width: 6, scale: 10 };
- 接口的非函数的成员应该是仅有的。假定它们不是仅有的,那么它们有必要是相同的类型。假定两个接口中一起声明晰同名的非函数成员且它们的类型不同,则编译器会报错。
- 关于函数成员,每个同名函数声明都会被当成这个函数的一个重载。一起需求留意,当接口
A
与后边的接口A
兼并时,后边的接口具有更高的优先级。
interface Cloner {
clone(animal: Animal): Animal;
}
interface Cloner {
clone(animal: Sheep): Sheep;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}
这三个接口将兼并成一个声明,每组接口里的声明次序保持不变,但各组接口之间的次序是后来的接口重载呈现在靠前方位。
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
有一个例外是当呈现特别的函数签名时,假定签名里有一个参数的类型是 单一的字符串字面量(例如不是字符串字面量的联合类型),那么它将会被提升到重载列表的最顶端。
interface Document {
createElement(tagName: any): Element;
}
interface Document {
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
}
interface Document {
createElement(tagName: string): HTMLElement;
createElement(tagName: "canvas"): HTMLCanvasElement;
}
兼并后的Document
如下:
interface Document {
createElement(tagName: "canvas"): HTMLCanvasElement;
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}
兼并命名空间
与接口类似,同名的命名空间也会兼并它们的成员。由于命名空间一起创立命名空间和值,咱们需求了解两者怎么兼并。
- 为了兼并命名空间,模块导出的同名接口进行兼并,构成单一命名空间,内含兼并后的接口。
- 关于命名空间里值的兼并,假定当时现已存在给定姓名的命名空间,那么后来的命名空间的导出成员会被添加到第一个命名空间来扩展它。
namespace Animals {
export class Zebra {}
}
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Dog {}
}
兼并后:
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Zebra {}
export class Dog {}
}
- 非导出成员仅在其原有的(兼并前的)命名空间内可见,也便是说兼并之后,从其它命名空间兼并进来的成员无法拜访非导出成员。
namespace Animal {
let haveMuscles = true;
export function animalsHaveMuscles() {
return haveMuscles;
}
}
namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // Error, because haveMuscles is not accessible here
}
}
由于haveMuscles
并没有导出,只要animalsHaveMuscles
函数共享了原始未兼并的命名空间能够拜访这个变量。doAnimalsHaveMuscles
函数虽是兼并命名空间的一部分,可是拜访不了未导出的成员。
将命名空间与类、函数和枚举兼并
命名空间满足灵敏,能够与其他类型的声明兼并。只要命名空间的界说符合将要兼并类型的界说,兼并结果包括两者的声明类型。TypeScript 运用这个功用去完成一些 JavaScript 里的规划办法。
- 将命名空间与类兼并,这为用户供给了一种描绘内部类的办法。
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel {};
export const num = 10;
}
console.log(new Album().label, Album.AlbumLabel, Album.num) // undefined [Function: AlbumLabel] 10
- 命名空间内的成员有必要导出,兼并后的类才干拜访。
- 命名空间内导出的成员,相当于兼并后类的静态特点。
- 命名空间要放在类的界说后边。
- 创立一个函数稍后扩展它添加一些特点也是很常见的。TypeScript 运用声明兼并来到达这个意图并保证类型安全。
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}
namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}
console.log(buildLabel('Mr.Pioneer')) // Hello, Mr.Pioneer.C
- 相同,命名空间可用于扩展具有静态成员的枚举。
enum Color {
red = 1,
green = 2,
blue = 4,
}
namespace Color {
export function mixColor(colorName: string) {
if (colorName == "yellow") {
return Color.red + Color.green;
} else if (colorName == "white") {
return Color.red + Color.green + Color.blue;
} else if (colorName == "magenta") {
return Color.red + Color.blue;
} else if (colorName == "cyan") {
return Color.green + Color.blue;
}
}
}
console.log(Color.mixColor('yellow')); // 3
- 目前,类不能与其它类或变量兼并。
模块扩展
虽然 JavaScript 模块不支撑兼并,但你能够为导入的方针打补丁以更新它们。
// observable.ts
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}
// map.ts
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
// ... another exercise for the reader
};
这在 TypeScript 中也能正常作业,但编译器不知道 Observable.prototype.map
,你能够运用扩展模块来将它告诉编译器:
// observable.ts
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function(f) {
let rets = f(1);
return new Observable<typeof rets>();
};
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number> = new Observable();
o.map((x) => x.toFixed());
模块名的解析和用import
/export
解析模块标识符的办法是共同的,当这些声明在扩展中兼并时,就好像在原始方位被声明晰相同。可是有两个约束:
- 不能在扩展中声明新的尖端声明-仅能够扩展模块中现已存在的声明。
- 默许导出也不能扩展-只能扩展命名导出。由于需求经过导出称号扩展导出,而
default
是保存字。
大局扩展
- 还能够从模块内部向大局规模添加声明。
- 大局扩展与模块扩展的行为和约束是相同的。
- 像 JQuery 那样,在浏览器中大局就能够拜访的方针,一般咱们会运用
namespace
,优点便是防止命名抵触。 - 一般大局变量在源码中会有如下特性:
- 尖端的
var
句子或function
声明。 - 挂载变量到
window
上。
export class Observable<T> {
// ... still no implementation ...
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
return {};
};
namespace jQuery {
export let $: { version: number };
}
let $ = jQuery.$;
declare global {
interface Window {
$
}
}