TypeScript是由微软开发的自由和开源的编程语言。
TypeScript是JavaScript的一个超集,从今天数以百万计的JavaScript开发者所熟悉的语法和语义开始。可以使用现有的JavaScript代码,包括流行的JavaScript库,并从JavaScript代码中调用TypeScript代码。
TypeScript可以编译出纯净、 简洁的JavaScript代码,并且可以运行在任何浏览器上、Node.js环境中和任何支持ECMAScript 3(或更高版本)的JavaScript引擎中。
TypeScript的优势在于:它有更多的规则和类型限制,代码具有更高的预测性、可控性,易于维护和调试;对模块、命名空间和面向对象的支持,更容易组织代码开发大型复杂程序。
另外,TypeScript的编译步骤可以捕获运行之前的错误。
接下来,让我们使用TypeScript开始构建一个简单的web应用程序。
有两种主要的方式获取TypeScript工具。
Visual Studio2015和Visual Studio 2013 Update 2默认包含了TypeScript。如果你没有安装包含TypeScript的Visual Studio ,你仍然可以下载。
使用NPM的开发者:
npm install -g typescript
在编辑器中创建greeter.ts
文件,并输入以下JavaScript代码:
function greeter(person){ return "Hello," + person;}var user = "Jane User";document.body.innerHTML = greeter(user);
虽然我们使用了.ts
作为文件扩展名,但是这些代码仅仅是JavaScript代码。你可以将代码直接复制粘贴到已有的JavaScript应用程序中。
在命令行中运行TypeScript编译器:
tsc greeter.ts
其结果你得到一个包含相同JavaScript代码的greeter.js
文件。在我们启动和运行的JavaScript应用程序中使用TypeScript。
现在我们可以开始利用TypeScript提供的新工具。给函数参数‘person’添加: string
类型注解,如下所示:
function greeter(person: string){ return "Hello," + person;}var user = "Jane User";document.body.innerHTML = greeter(user);
类型注解在TypeScript中是记录函数或变量约束的简便方法。在这个示例中,我们想要在调用greeter函数时传入一个字符串类型参数。我们可以尝试在调用greeter函数时变为传入一个数组:
function greeter(person: string){return"Hello, " + person;}var user = [0, 1 , 2];document.body.innerHTML = greeter(user);
重新编译,将看到一个错误:
greeter.ts(7,26): Supplied parameters do not match any signature of call target
同样,在调用greeter函数时尝试不传入任何参数。TypeScript将会告诉你调用这个函数时需要带一个参数。在这两个示例中,TypeScript基于你的代码结构和类型注解可以提供静态分析。
注意,虽然有错误,但是仍然编译创建了greeter.js
文件。即使你的代码中有错误,你仍旧可以使用TypeScript。但是在这种情况,TypeScript会发出警告:你的代码可能不能按照你预想的那样运行。
让我们进一步开发我们的demo。 在这里我们使用一个接口,它描述了具有firstName和lastName字段的对象。在TypeScript中,如果两个类型其内部结构兼容,那么这两种类型兼容。这使我们实现一个接口,仅仅只需必要的结构形状,而不必有明确的implements
子句。
interface Person { firstName: string ; lastName: string ;} function greeter ( person: Person ) { return "Hello, " + person.firstName + " " + person.lastName;} var user = { firstName: "Jane" , lastName: "User" }; document .body.innerHTML = greeter(user);
最后,让我们最后一次使用类来继续开发demo。TypeScript支持新的JavaScript特性,像基于类的面向对象编程的支持。
在这里,我们创建一个具有构造函数和一些公共字段的Student
类。注意:类和接口的良好配合使用,决定一个程序员的抽象水平。
此外,在构造函数参数中使用public
是一种简写形式,它将自动创建具有该名称的属性。
class Student { fullName: string ; constructor( public firstName, public middleInitial, public lastName) { this .fullName = firstName + " " + middleInitial + " " + lastName; }} interface Person { firstName: string ; lastName: string ;} function greeter ( person : Person ) { return "Hello, " + person.firstName + " " + person.lastName;} var user = new Student( "Jane" , "M." , "User" );document.body.innerHTML = greeter(user);
再次运行tsc greeter.ts
,你将看到生成的JavaScript代码和以前的一样。TypeScript中的类只是对于经常在JavaScript中使用了相同的基于原型的面向对象的简写。
现在在greeter.html
中输入以下代码:
<!DOCTYPE html> <html> <head><title>TypeScript Greeter</title></head> <body> <script src = "greeter.js"></script> </body> </html>
在浏览器中打开greeter.html
去运行第一个TypeScript web应用程序demo!
可选:在Visual Studio中打开greeter.ts
,或者复制代码到TypeScript学习乐园中。你可以将鼠标悬浮到标识符上查看类型。注意,在某些情况下这些类型会为你自动推断。重新输入最后一行,查看完成列表和基于DOM元素类型的参数帮助。将光标放到引用greeter函数的地方,并且按下F12键去转到定义。同样注意,你也可以在符号上右击使用重构来重命名。
所提供的类型信息和工具以及JavaScript在应用程序中一起工作。TypeScript更多可能性的示例,请浏览网站的案例。
TypeScript现在有两个特殊的类型:Null和Undefined, 它们的值分别是null
和undefined
。 以前这是不可能明确地命名这些类型的,但是现在 null
和undefined
不管在什么类型检查模式下都可以作为类型名称使用。
以前类型检查器认为null
和undefined
赋值给一切。实际上,null
和undefined
是每一个类型的有效值, 并且不能明确排除它们(因此不可能检测到错误)。
--strictNullChecks
--strictNullChecks
可以切换到新的严格空检查模式中。
在严格空检查模式中,null
和undefined
值不再属于任何类型的值,仅仅属于它们自己类型和any
类型的值 (还有一个例外, undefined
也能赋值给void
)。因此,尽管在常规类型检查模式下T
和T | undefined
被认为是相同的 (因为 undefined
被认为是任何T
的子类型),但是在严格类型检查模式下它们是不同的, 并且仅仅 T | undefined
允许有undefined
值,T
和T | null
的关系同样如此。
// 使用--strictNullChecks参数进行编译的let x: number;let y: number | undefined;let z: number | null | undefined;x = 1; // 正确y = 1; // 正确z = 1; // 正确x = undefined; // 错误y = undefined; // 正确z = undefined; // 正确x = null; // 错误y = null; // 错误z = null; // 正确x = y; // 错误x = z; // 错误y = x; // 正确y = z; // 错误z = x; // 正确z = y; // 正确
在严格空检查模式中,编译器要求未包含undefined
类型的局部变量在使用之前必须先赋值。
// 使用--strictNullChecks参数进行编译let x: number;let y: number | null;let z: number | undefined;x; // 错误,使用前未赋值y; // 错误,使用前未赋值z; // 正确x = 1;y = null;x; // 正确y; // 正确
编译器通过执行基于控制流的类型分析检查变量明确被赋过值。在本篇文章后面会有进一步的细节。
可选参数和属性会自动把undefined
添加到他们的类型中,即使他们的类型注解明确不包含undefined
。例如,下面两个类型是完全相同的:
// 使用--strictNullChecks参数进行编译type T1 = (x?: number) => string; // x的类型是 number | undefinedtype T2 = (x?: number | undefined) => string; // x的类型是 number | undefined
如果对象或者函数的类型包含null
和undefined
,那么访问属性或调用函数时就会产生编译错误。因此,对类型保护进行了扩展,以支持对非null和非undefined的检查。
// 使用--strictNullChecks参数进行编译declare function f(x: number): string;let x: number | null | undefined;if (x) { f(x); // 正确,这里的x类型是number}else { f(x); // 错误,这里的x类型是number?}let a = x != null ? f(x) : ""; // a的类型是stringlet b = x && f(x); // b的类型是 string | 0 | null | undefined
非null和非undefined类型保护可以使用==
、!=
、===
或!==
操作符和null
或undefined
进行比较,如x != null
或x === undefined
。对被试变量类型的影响准确地反映了JavaScript的语义(比如,双等号运算符检查两个值无论你指定的是null还是undefined,然而三等于号运算符仅仅检查指定的那一个值)。
类型保护以前仅仅支持对局部变量和参数的检查。现在类型保护支持检查由变量或参数名称后跟一个或多个访问属性组成的“点名称”。
interface Options { location?: { x?: number; y?: number; };}function foo(options?: Options) { if (options && options.location && options.location.x) { const x = options.location.x; // x的类型是number }}
点名称的类型保护和用户定义的类型保护函数,还有typeof
和instanceof
操作符一起工作,并且不依赖--strictNullChecks
编译参数。
对点名称进行类型保护后给点名称任一部分赋值都会导致类型保护无效。例如,对x.y.z
进行了类型保护后给x
、x.y
或x.y.z
赋值,都会导致x.y.z
类型保护无效。
表达式操作符允许运算对象的类型包含null
和/或undefined
,但是总是产生非null和非undefined类型的结果值。
// 使用--strictNullChecks参数进行编译function sum(a: number | null, b: number | null) { return a + b; // 计算的结果值类型是number}
&&
操作符添加null
和/或undefined
到右边操作对象的类型中取决于当前左边操作对象的类型,||
操作符从左边联合类型的操作对象的类型中将null
和undefined
同时删除。
// 使用--strictNullChecks参数进行编译interface Entity { name: string;}let x: Entity | null;let s = x && x.name; // s的类型是string | nulllet y = x || { name: "test" }; // y的类型是Entity
在严格空检查模式中,null
和undefined
类型是不会扩展到any
类型中的。
let z = null; // z的类型是null
在常规类型检查模式中,由于扩展,会推断z
的类型是any
,但是在严格空检查模式中,推断z
是null
类型(因此,如果没有类型注释,null
是z
的唯一值)。
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符!
可以用于断言操作对象是非null和非undefined类型的。具体而言,运算x!
产生一个不包含null
和undefined
的x
的值。断言的形式类似于<T>x
和x as T
,!
非空断言操作符会从编译成的JavaScript代码中移除。
// 使用--strictNullChecks参数进行编译function validateEntity(e?: Entity) { // 如果e是null或者无效的实体,就会抛出异常}function processEntity(e?: Entity) { validateEntity(e); let s = e!.name; // 断言e是非空并访问name属性}
这些新特性是经过设计的,使得它们能够在严格空检查模式和常规类型检查模式下都能够使用。尤其是在常规类型检查模式中,null
和undefined
类型会自动从联合类型中删除(因为它们是其它所有类型的子类型),!
非空断言表达式操作符也被允许使用但是没有任何作用。因此,声明文件使用null和undefined敏感类型更新后,在常规类型模式中仍然是可以向后兼容使用的。
在实际应用中,严格空检查模式要求编译的所有文件都是null和undefined敏感类型。
TypeScript 2.0实现了对局部变量和参数的控制流类型分析。以前,对类型保护进行类型分析仅限于if
语句和?:
条件表达式,并且不包括赋值和控制流结构的影响,例如return
和break
语句。使用TypeScript 2.0,类型检查器会分析语句和表达式所有可能的控制流,在任何指定的位置对声明为联合类型的局部变量或参数产生最可能的具体类型(缩小范围的类型)。
function foo(x: string | number | boolean) { if (typeof x === "string") { x; // 这里x的类型是string x = 1; x; // 这里x的类型是number } x; // 这里x的类型是number | boolean}function bar(x: string | number) { if (typeof x === "number") { return; } x; // 这里x的类型是string}
基于控制流的类型分析在--strictNullChecks
模式中尤为重要,因为可空类型使用联合类型来表示:
function test(x: string | null) { if (x === null) { return; } x; // 在函数的剩余部分中,x类型是string}
而且,在--strictNullChecks
模式中,基于控制流的分析包括,对类型不允许为undefined
的局部变量有明确赋值的分析。
function mumble(check: boolean) { let x: number; // 类型不允许为undefined x; // 错误,x是undefined if (check) { x = 1; x; // 正确 } x; // 错误,x可能是undefi x = 2; x; // 正确}
TypeScript 2.0实现了标记(或区分)联合类型。具体而言,TS编译器现在支持类型保护,基于判别属性的检查来缩小联合类型的范围,并且switch
语句也支持此特性。
interface Square { kind: "square"; size: number;}interface Rectangle { kind: "rectangle"; width: number; height: number;}interface Circle { kind: "circle"; radius: number;}type Shape = Square | Rectangle | Circle;function area(s: Shape) { // 在下面的switch语句中,s的类型在每一个case中都被缩小 // 根据判别属性的值,变量的其它属性不使用类型断言就可以被访问 switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.width * s.height; case "circle": return Math.PI * s.radius * s.radius; }}function test1(s: Shape) { if (s.kind === "square") { s; // Square } else { s; // Rectangle | Circle }}function test2(s: Shape) { if (s.kind === "square" || s.kind === "rectangle") { return; } s; // Circle}
判别属性类型保护是x.p == v
、x.p === v
、x.p != v
或者x.p !== v
其中的一种表达式,p
和v
是一个属性和字符串字面量类型或字符串字面量联合类型的表达式。判别属性类型保护缩小x
的类型到由判别属性p
和v
的可能值之一组成的类型。
请注意,我们目前只支持字符串字面值类型的判别属性。我们打算以后添加对布尔值和数字字面量类型的支持。
never
类型TypeScript 2.0引入了一个新原始类型never
。never
类型表示值的类型从不出现。具体而言,never
是永不返回函数的返回类型,也是变量在类型保护中永不为true的类型。
never
类型具有以下特征:
never
是所有类型的子类型并且可以赋值给所有类型。never
的子类型或能赋值给never
(never
类型本身除外)。return
语句,或者只有never
类型表达式的return
语句,并且如果函数是不可执行到终点的(例如通过控制流分析决定的),则推断函数的返回类型是never
。never
返回类型注解的函数中,所有return
语句(如果有的话)必须有never
类型的表达式并且函数的终点必须是不可执行的。因为never
是每一个类型的子类型,所以它总是在联合类型中被省略,并且在函数中只要其它类型被返回,类型推断就会忽略never
类型。
一些返回never
函数的示例:
// 函数返回never必须无法执行到终点function error(message: string): never { throw new Error(message);}// 推断返回类型是neverfunction fail() { return error("Something failed");}// 函数返回never必须无法执行到终点function infiniteLoop(): never { while (true) { }}
一些函数返回never
的使用示例:
// 推断返回类型是numberfunction move1(direction: "up" | "down") { switch (direction) { case "up": return 1; case "down": return -1; } return error("Should never get here");}// 推断返回类型是numberfunction move2(direction: "up" | "down") { return direction === "up" ? 1 : direction === "down" ? -1 : error("Should never get here");}// 推断返回类型是Tfunction check<T>(x: T | undefined) { return x || error("Undefined value");}
因为never
可以赋值给每一个类型,当需要回调函数返回一个更加具体的类型时,函数返回never
类型可以用于检测返回类型是否正确:
function test(cb: () => string) { let s = cb(); return s;}test(() => "hello");test(() => fail());test(() => { throw new Error(); })
属性或索引签名现在可以使用readonly
修饰符声明为只读的。
只读属性可以初始化和在同一个类的构造函数中被赋值,但是在其它情况下对只读属性的赋值是不允许的。
此外,有几种情况下实体隐式只读的:
get
访问器而没有使用set
访问器被视为只读的。const
变量被视为只读属性。import
语句中声明的实体被视为只读的。foo
当作import * as foo from "foo"
声明时,foo.x
是只读的)。interface Point { readonly x: number; readonly y: number;}var p1: Point = { x: 10, y: 20 };p1.x = 5; // 错误,p1.x是只读的var p2 = { x: 1, y: 1 };var p3: Point = p2; // 正确,p2的只读别名p3.x = 5; // 错误,p3.x是只读的p2.x = 5; // 正确,但是因为别名使用,同时也改变了p3.x
class Foo { readonly a = 1; readonly b: string; constructor() { this.b = "hello"; // 在构造函数中允许赋值 }}
let a: Array<number> = [0, 1, 2, 3, 4];let b: ReadonlyArray<number> = a;b[5] = 5; // 错误,元素是只读的b.push(5); // 错误,没有push方法(因为这会修改数组)b.length = 3; // 错误,length是只读的a = b; // 错误,缺少修改数组的方法
this
类型紧跟着类和接口,现在函数和方法也可以声明this
的类型了。
函数中this
的默认类型是any
。从TypeScript 2.0开始,你可以提供一个明确的this
参数。this
参数是伪参数,它位于函数参数列表的第一位:
function f(this: void) { // 确保`this`在这个独立的函数中无法使用}
this
参数库也可以使用this
参数声明回调函数如何被调用。
interface UIElement { addClickListener(onclick: (this: void, e: Event) => void): void;}
this:void
意味着addClickListener
预计onclick
是一个this
参数不需要类型的函数。
现在如果你在调用代码中对this
进行了类型注释:
class Handler { info: string; onClickBad(this: Handler, e: Event) { // 哎哟,在这里使用this.在运行中使用这个回调函数将会崩溃。 this.info = e.message; };}let h = new Handler();uiElement.addClickListener(h.onClickBad); // 错误!
--noImplicitThis
TypeScript 2.0还增加了一个新的编译选项用来标记函数中所有没有明确类型注释的this
的使用。
tsconfig.json
支持文件通配符文件通配符来啦!!支持文件通配符一直是最需要的特性之一。
类似文件通配符的文件模式支持两个属性"include"
和"exclude"
。
{ "compilerOptions": { "module": "commonjs", "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, "outFile": "../../built/local/tsc.js", "sourceMap": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules", "**/*.spec.ts" ]}
支持文件通配符的符号有:
*
匹配零个或多个字符(不包括目录)?
匹配任意一个字符(不包括目录)**/
递归匹配所有子目录如果文件通配符模式语句中只包含*
或.*
,那么只匹配带有扩展名的文件(例如默认是.ts
、.tsx
和.d.ts
,如果allowJs
设置为true
,.js
和.jsx
也属于默认)。
如果"files"
和"include"
都没有指定,编译器默认包含所有目录中的TypeScript文件(.ts
、.d.ts
和.tsx
),除了那些使用exclude
属性排除的文件外。如果allowJs
设置为true,JS文件(.js
和.jsx
)也会被包含进去。
如果"files"
和"include"
都指定了,编译器将包含这两个属性指定文件的并集。使用ourDir
编译选项指定的目录文件总是被排除,即使"exclude"
属性指定的文件也会被删除,但是files
属性指定的文件不会排除。
"exclude"
属性指定的文件会对"include"
属性指定的文件过滤。但是对"files"
指定的文件没有任何作用。当没有明确指定时,"exclude"
属性默认会排除node_modules
、bower_components
和jspm_packages
目录。
TypeScript 2.0提供了一系列额外的模块解析属性告诉编译器去哪里可以找到给定模块的声明。
更多详情,请参阅模块解析文档。
使用了AMD模块加载器并且模块在运行时”部署“到单文件夹的应用程序中使用baseUrl
是一种常用的做法。所有非相对名称的模块导入被认为是相对于baseUrl
的。
{ "compilerOptions": { "baseUrl": "./modules" }}
现在导入moduleA
将会在./modules/moduleA
中查找。
import A from "moduleA";
有时模块没有直接位于baseUrl中。加载器使用映射配置在运行时去映射模块名称和文件,请参阅RequireJs文档和SystemJS文档。
TypeScript编译器支持tsconfig
文件中使用"paths"
属性映射的声明。
例如,导入"jquery"
模块在运行时会被转换为"node_modules/jquery/dist/jquery.slim.min.js"
。
{ "compilerOptions": { "baseUrl": "./node_modules", "paths": { "jquery": ["jquery/dist/jquery.slim.min"] }}
使用"paths"
也允许更复杂的映射,包括多次后退的位置。考虑一个只有一个地方的模块是可用的,其它的模块都在另一个地方的项目配置。
rootDirs
和虚拟目录使用rootDirs
,你可以告知编译器的根目录组合这些“虚拟”目录。因此编译器在这些“虚拟”目录中解析相对导入模块,仿佛是合并到一个目录中一样。
给定的项目结构
src └── views └── view1.ts (imports './template1') └── view2.ts generated └── templates └── views └── template1.ts (imports './view2')
构建步骤将复制/src/views
和/generated/templates/views
目录下的文件输出到同一个目录中。在运行时,视图期望它的模板和它存在同一目录中,因此应该使用相对名称"./template"
导入。
"rootDir"
指定的一组根目录的内容将会在运行时合并。因此在我们的例子,tsconfig.json
文件应该类似于:
{ "compilerOptions": { "rootDirs": [ "src/views", "generated/templates/views" ] }}
--traceResolution
提供了一种方便的方法,以了解模块如何被编译器解析的。
tsc --traceResolution
当你使用一个新模块时,如果不想要花费时间书写一个声明时,现在你可以使用快捷声明以便以快速开始。
declare module "hot-new-module";
所有从快捷模块的导入都具有任意类型。
import x, {y} from "hot-new-module";x(y);
以前使用模块加载器(例如AMD和SystemJS)导入没有代码的资源是不容易的。之前,必须为每个资源定义一个外部模块声明。
TypeScript 2.0支持使用通配符符号(*
)定义一类模块名称。这种方式,一个声明只需要一次扩展名,而不再是每一个资源。
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);
当从一个基于非类型化的代码迁移时,通配符模块的名称可能更加有用。结合快捷外部模块声明,一组模块可以很容易地声明为any
。
declare module "myLibrary/*";
所有位于myLibrary
目录之下的模块的导入都被编译器认为是any
类型,因此这些模块的任何类型检查都会被关闭。
import { readFile } from "myLibrary/fileSystem/readFile`;readFile(); // readFile是'any'类型
一些库被设计为可以使用多种模块加载器或者不是使用模块加载器(全局变量)来使用,这被称为UMD或同构模块。这些库可以通过导入或全局变量访问。
举例:
export const isPrime(x: number): boolean;export as namespace mathLib;
然后,该库可作为模块导入使用:
import { isPrime } from "math-lib";isPrime(2);mathLib.isPrime(2); // 错误:无法在模块内部使用全局定义
它也可以被用来作为一个全局变量,只限于没有import
和export
脚本文件中。
mathLib.isPrime(2);
现在可以在类中声明可选属性和方法,与接口类似。
class Bar { a: number; b?: number; f() { return 1; } g?(): number; // 可选方法的方法体可以省略 h?() { return 2; }}
在--strictNullChecks
模式下编译时,可选属性和方法会自动添加undefined
到它们的类型中。因此,上面的b
属性类型是number | undefined
,上面g
方法的类型是(()=> number) | undefined
。使用类型保护可以去除undefined
。
类的构造函数可以被标记为private
或protected
。私有构造函数的类不能在类的外部实例化,并且也不能被继承。受保护构造函数的类不能再类的外部实例化,但是可以被继承。
class Singleton { private static instance: Singleton; private constructor() { } static getInstance() { if (!Singleton.instance) { Singleton.instance = new Singleton(); } return Singleton.instance; } }let e = new Singleton(); // 错误:Singleton的构造函数是私有的。let v = Singleton.getInstance();
抽象类可以声明抽象属性和、或访问器。所有子类将需要声明抽象属性或者被标记为抽象的。抽象属性不能初始化。抽象访问器不能有具体代码块。
abstract class Base { abstract name: string; abstract get value(); abstract set value(v: number);}class Derived extends Base { name = "derived"; value = 1;}
如果对象字面量中所有已知的属性是赋值给索引签名,那么现在对象字面量类型可以赋值给索引签名类型。这使得一个使用对象字面量初始化的变量作为参数传递给期望参数是map或dictionary的函数成为可能:
function httpService(path: string, headers: { [x: string]: string }) { }const headers = { "Content-Type": "application/x-www-form-urlencoded"};httpService("", { "Content-Type": "application/x-www-form-urlencoded" }); // 可以httpService("", headers); // 现在可以,以前不可以。
--lib
编译参数包含内置类型声明获取ES6/ES2015内置API声明仅限于target: ES6
。输入--lib
,你可以使用--lib
指定一组项目所需要的内置API。比如说,如果你希望项目运行时支持Map
、Set
和Promise
(例如现在静默更新浏览器),直接写--lib es2015.collection,es2015.promise
就好了。同样,你也可以排除项目中不需要的声明,例如在node项目中使用--lib es5,es6
排除DOM。
下面是列出了可用的API:
tsc --target es5 --lib es5,es2015.promise
"compilerOptions": { "lib": ["es5", "es2015.promise"]}
--noUnusedParameters
和--noUnusedLocals
标记未使用的声明TypeScript 2.0有两个新的编译参数来帮助你保持一个干净的代码库。-noUnusedParameters
编译参数标记所有未使用的函数或方法的参数错误。--noUnusedLocals
标记所有未使用的局部(未导出)声明像变量、函数、类和导入等等,另外未使用的私有类成员在--noUnusedLocals
作用下也会标记为错误。
import B, { readFile } from "./b";// ^ 错误:`B`声明了,但是没有使用。readFile();export function write(message: string, args: string[]) { // ^^^^ 错误:'arg'声明了,但是没有使用。 console.log(message);}
使用以_
开头命名的参数声明不会被未使用参数检查。例如:
function returnNull(_a) { // 正确 return null;}
.js
扩展名TypeScript 2.0之前,模块名称总是被认为是没有扩展名的。例如,导入一个模块import d from "./moduleA.js"
,则编译器在./moduleA.js.ts
或./moduleA.js.d.ts
中查找"moduleA.js"
的定义。这使得像 SystemJS这种期望模块名称是URI的打包或加载工具很难使用。
使用TypeScript 2.0,编译器将在./moduleA.ts
或./moduleA.d.ts
中查找"moduleA.js"
的定义。
target : es5
和module: es6
同时使用之前编译参数target : es5
和module: es6
同时使用被认为是无效的,但是现在是有效的。这将有助于使用基于ES2015的tree-shaking(将无用代码移除)比如 rollup。
现在函数形参和实参列表末尾允许有逗号。这是对第三阶段的ECMAScript提案的实现, 并且会编译为可用的 ES3/ES5/ES6。
function foo( bar: Bar, baz: Baz, // 形参列表末尾添加逗号是没有问题的。) { // 具体实现……}foo( bar, baz, // 实参列表末尾添加逗号同样没有问题);
--skipLibCheck
TypeScript 2.0添加了一个新的编译参数--skipLibCheck
,该参数可以跳过声明文件(以.d.ts
为扩展名的文件)的类型检查。当一个程序包含有大量的声明文件时,编译器需要花费大量时间对已知不包含错误的声明进行类型检查,通过跳过声明文件的类型检查,编译时间可能会大大缩短。
由于一个文件中的声明可以影响其他文件中的类型检查,当指定--skipLibCheck
时,一些错误可能检测不到。比如说, 如果一个非声明文件中的类型被声明文件用到, 可能仅在声明文件被检查时能发现错误. 不过这种情况在实际使用中并不常见。
这是重复定义错误的一个常见来源。多个声明文件定义相同的接口成员。
TypeScript 2.0放宽了这一约束,并允许可以不同代码块中出现重复的标识符, 只要它们有完全相同的类型。
在同一代码块重复定义仍不允许。
interface Error { stack?: string;}interface Error { code?: string; path?: string; stack?: string; // OK}
--declarationDir
--declarationDir
可以使生成的声明文件和JavaScript文件不在同一个位置中。
在JavaScript中,使用期望属性名称作为参数的API是相当普遍的,但到目前为止,还无法表达这些API中出现的类型关系。
输入索引类型查询或keyof
;索引类型查询keyof T
可以为T生成允许的属性名称类型。keyof T
类型被认为是一种string的子类型。
interface Person { name: string; age: number; location: string;}type K1 = keyof Person; // "name" | "age" | "location"type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...type K3 = keyof { [x: string]: Person }; // string
其中的双重属性是索引访问类型,也称为lookup类型。从语法上讲,它们看起来完全像元素访问,但是写成类型:
type P1 = Person["name"]; // stringtype P2 = Person["name" | "age"]; // string | numbertype P3 = string["charAt"]; // (pos: number) => stringtype P4 = string[]["push"]; // (...items: string[]) => numbertype P5 = string[][0]; // string
您可以将此模式与类型系统的其他部分一起使用,以获得类型安全的查找。
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // Inferred type is T[K]}function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) { obj[key] = value;}let x = { foo: 10, bar: "hello!" };let foo = getProperty(x, "foo"); // numberlet bar = getProperty(x, "bar"); // stringlet oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"setProperty(x, "foo", "string"); // Error!, string expected number
一个常见的任务是采用现有类型并使其每个属性完全可选。假设我们有一个Person:
interface Person { name: string; age: number; location: string;}
它的部分版本是:
interface PartialPerson { name?: string; age?: number; location?: string;}
使用Mapped类型,PartialPerson可以写为Person类型的广义转换,例如:
type Partial<T> = { [P in keyof T]?: T[P];};type PartialPerson = Partial<Person>;
映射类型是通过获取文字类型的并集,并为新对象类型计算一组属性来生成的。它们与Python中的列表推导类似,但它们不是在列表中生成新元素,而是在类型中生成新属性。
除此了Partial之外,Mapped Types还可以在类型上表达许多有用的转换:
// Keep types the same, but make each property to be read-only.type Readonly<T> = { readonly [P in keyof T]: T[P];};// Same property names, but make the value a promise instead of a concrete onetype Deferred<T> = { [P in keyof T]: Promise<T[P]>;};// Wrap proxies around properties of Ttype Proxify<T> = { [P in keyof T]: { get(): T[P]; set(v: T[P]): void }};
Partial和Readonly,如前所述,是非常有用的结构。您可以使用它们来描述一些常见的JS例程,例如:
function assign<T>(obj: T, props: Partial<T>): void;function freeze<T>(obj: T): Readonly<T>;
因此,它们现在默认包含在标准库中。
我们还包括另外两种实用程序类型:Record和Pick。
// From T pick a set of properties Kdeclare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;const nameAndAgeOnly = pick(person, "name", "age"); // { name: string, age: number }
// For every properties K of type T, transform it to Ufunction mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>const names = { foo: "hello", bar: "world", baz: "bye" };const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }
TypeScript 2.1支持ESnext Spread和Rest。
与数组扩展类似,扩展对象可以很方便地获得浅层副本:
let copy = { ...original };
同样,您可以合并多个不同的对象。在以下示例中,merged将具有来自foo,bar和baz的属性。
let merged = { ...foo, ...bar, ...baz };
您还可以覆盖现有属性并添加新属性:
let obj = { x: 1, y: "string" };var newObj = {...obj, z: 3, y: 4}; // { x: number, y: number, z: number }
指定扩展操作的顺序决定了生成的对象中最终的属性;以后的属性会在以前创建的属性上“win out”。
对象休息是对象扩展的双重对象,因为它们可以提取在解构元素时不会被拾取的任何额外属性:
let obj = { x: 1, y: 1, z: 1 };let { z, ...obj1 } = obj;obj1; // {x: number, y:number};
在TypeScript 2.1之前支持此功能,但仅在定位ES6/ES2015时。TypeScript 2.1为ES3和ES5运行时提供了功能,这意味着无论您使用何种环境,您都可以自由地利用它。
注意:首先,我们需要确保我们的运行时具有全局可用的ECMAScript兼容性Promise。这可能涉及为Promise获取一个polyfill,或者依赖一个你可能在你所定位的运行时间。我们还需要确保TypeScript知道Promise是存在的,通过将lib标志设置为类似于"dom", "es2015"或"dom", "es2015.promise", "es5"的东西。
{ "compilerOptions": { "lib": ["dom", "es2015.promise", "es5"] }}
function delay(milliseconds: number) { return new Promise<void>(resolve => { setTimeout(resolve, milliseconds); });}async function dramaticWelcome() { console.log("Hello"); for (let i = 0; i < 3; i++) { await delay(500); console.log("."); } console.log("World!");}dramaticWelcome();
编译和运行输出应该会导致ES3/ES5引擎上的正确行为。
TypeScript注入了一些辅助函数,例如用于继承的__extends,在对象文字中用于扩展操作的__assign和JSX元素以及用于异步函数的__awaiter。
以前有两种选择:
这两个选项还有待改进;将帮助器捆绑在每个文件中对于试图保持其包装尺寸较小的客户来说是一个痛点。并且不包括帮助器,意味着客户必须维护自己的帮助程序库。
TypeScript 2.1允许在项目中将这些文件包含在一个单独的模块中,编译器将根据需要向它们发出导入。
首先,安装tslib实用程序库:
npm install tslib
其次,使用--importHelpers命令编译文件:
tsc --module commonjs --importHelpers a.ts
因此,给定以下输入,生成的.js文件将包含导入到tslib并使用其中的__assign帮助程序而不是内联它。
export const o = { a: 1, name: "o" };export const copy = { ...o };
"use strict";var tslib_1 = require("tslib");exports.o = { a: 1, name: "o" };exports.copy = tslib_1.__assign({}, exports.o);
传统上,TypeScript对于如何导入模块过于严格。这是为了避免拼写错误并阻止用户错误地使用模块。
但是,在很多时候,您可能只想导入可能没有自含.d.ts文件的现有模块。以前这是一个错误。从TypeScript 2.1开始,这现在变得更加容易。
使用TypeScript 2.1,您可以导入JavaScript模块而无需类型声明。如果存在类型声明(例如,declare module "foo" { ... },或node_modules/@types/foo)仍然具有优先权。
对没有声明文件的模块的导入仍将被标记为--noImplicitAny下的错误。
// Succeeds if `node_modules/asdf/index.js` existsimport { x } from "asdf";
TypeScript 2.1支持三个新的目标值--target ES2016,--target ES2017和--target ESNext。
使用target --target ES2016将指示编译器不要转换特定于ES2016的功能,例如**运算符。
同样,--target ES2017将指示编译器不要转换特定于ES2017的特性,如async/ await。
--target ESNext针对最新支持的ES提议功能。
以前,如果TypeScript无法确定变量的类型,则会选择any类型。
let x; // implicitly 'any'let y = []; // implicitly 'any[]'let z: any; // explicitly 'any'.
使用TypeScript 2.1,而不仅仅是选择any,TypeScript将根据您最后分配的内容推断类型。
仅在设置--noImplicitAny时才启用此选项。
let x;// You can still assign anything you want to 'x'.x = () => 42;// After that last assignment, TypeScript 2.1 knows that 'x' has type '() => number'.let y = x();// Thanks to that, it will now tell you that you can't add a number to a function!console.log(x + y);// ~~~~~// Error! Operator '+' cannot be applied to types '() => number' and 'number'.// TypeScript still allows you to assign anything you want to 'x'.x = "Hello world!";// But now it also knows that 'x' is a 'string'!x.toLowerCase();
现在也对空数组进行了相同类型的跟踪。
声明为没有类型注释且初始值为[]的变量被视为隐式any[]变量。然而,每个后续的x.push(value),x.unshift(value)或者x[n] = value操作根据添加到的元素来演变变量的类型。
function f1() { let x = []; x.push(5); x[1] = "hello"; x.unshift(true); return x; // (string | number | boolean)[]}function f2() { let x = null; if (cond()) { x = []; while (cond()) { x.push("hello"); } } return x; // string[] | null}
这样做的一个很大好处是,在运行--noImplicitAny时你会看到更少的隐式any错误。仅当编译器无法知道没有类型注释的变量类型时,才会报告隐式any错误。
function f3() { let x = []; // Error: Variable 'x' implicitly has type 'any[]' in some locations where its type cannot be determined. x.push(5); function g() { x; // Error: Variable 'x' implicitly has an 'any[]' type. }}
字符串,数字和布尔文字类型(例如"abc",1和true)仅在存在显式类型注释时才推断。从TypeScript 2.1开始,始终为const变量和readonly属性推断文字类型。
为没有类型注释的const变量或readonly属性推断的类型是文字初始值设定项的类型。为具有初始值设定项且没有类型注释的let变量,var变量,参数或非readonly属性推断的类型是初始化程序的扩展文字类型。对于字符串文字类型的加宽类型是string,number对于数字文字类型,boolean对于true或false,以及包含枚举文字类型的枚举。
const c1 = 1; // Type 1const c2 = c1; // Type 1const c3 = "abc"; // Type "abc"const c4 = true; // Type trueconst c5 = cond ? 1 : "abc"; // Type 1 | "abc"let v1 = 1; // Type numberlet v2 = c2; // Type numberlet v3 = c3; // Type stringlet v4 = c4; // Type booleanlet v5 = c5; // Type number | string
可以通过显式类型注释来控制文字类型扩展。具体来说,当为没有类型注释的const位置推断出文字类型的表达式时,该const变量将推断出一个加宽的文字类型。但是,当const位置具有显式文字类型注释时,该const变量将获得非加宽文字类型。
const c1 = "hello"; // Widening type "hello"let v1 = c1; // Type stringconst c2: "hello" = "hello"; // Type "hello"let v2 = c2; // Type "hello"
在ES2015中,返回对象的构造函数隐式地将this值替换为super()的任何调用者。因此,有必要捕获super()的任何潜在返回值,并且使用this替换它。此更改允许使用Custom Elements,该元素利用此特性用用户编写的构造函数初始化浏览器分配的元素。
class Base { x: number; constructor() { // return a new object other than `this` return { x: 1, }; }}class Derived extends Base { constructor() { super(); this.x = 2; }}
输出:
var Derived = (function (_super) { __extends(Derived, _super); function Derived() { var _this = _super.call(this) || this; _this.x = 2; return _this; } return Derived;}(Base));
这种变化导致扩展内置类(如,Error,Array,Map,等)的行为中断。
通常,一个项目有多个输出目标,例如ES5和ES2015,调试和生产,CommonJS和System;这两个目标之间只有少数配置选项发生变化,维护多个tsconfig.json文件可能很麻烦。
TypeScript 2.1支持使用extends继承配置,其中:
configs/base.json:
{ "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true }}
tsconfig.json:
{ "extends": "./configs/base", "files": [ "main.ts", "supplemental.ts" ]}
tsconfig.nostrictnull.json:
{ "extends": "./tsconfig", "compilerOptions": { "strictNullChecks": false }}
使用--alwaysStrict调用编译器的原因:
模块在严格模式下自动解析。对于非模块代码,建议使用新标志。
在 TypeScript 3.1 中,元组和数组上的映射对象类型现在产生新的元组/数组,而不是创建一个新的类型,其中的成员,比如push()
,pop()
和length
将被转换。例如:
type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };type Coordinate = [number, number]type PromiseCoordinate = MapToPromise<Coordinate>; // [Promise<number>, Promise<number>]
MapToPromise
使用一种类型T
,当该类型是一个元组,例如Coordinate
,它只转换数字属性。在[number, number]
中,有两个数字命名的属性:0
和1
。当给出这样的元组时,MapToPromise
将创建一个新的元组,其中0
和1
属性是原始类型的Promise
s。因此,结果类型PromiseCoordinate
最终会得到类型[Promise<number>, Promise<number>]
。
TypeScript 3.1 能够在函数声明和const
-declared functons 上定义属性,只需在同一范围内分配这些函数的属性即可。这允许我们编写规范的JavaScript代码而不求助于namespace
。例如:
function readImage(path: string, callback: (err: any, image: Image) => void) { // ...}readImage.sync = (path: string) => { const contents = fs.readFileSync(path); return decodeImageSync(contents);}
在这里,我们有一个以非阻塞异步方式读取图像的readImage
函数。除了readImage
外,我们在readImage
提供了一个方便的函数,名为readImage.sync
。
虽然ECMAScript导出通常是提供此功能的更好方式,但这种新的支持允许使用此样式编写的代码“仅仅工作” TypeScript。另外,这种属性声明方法让我们在React无状态功能组件(SFCs)上表达常用模式,例如defaultProps
和propTypes
。
export const FooComponent => ({ name }) => ( <div>Hello! I am {name}</div>);FooComponent.defaultProps = { name: "(anonymous)",};
更具体地说,同态映射类型类似于上面的形式。
来自社区的反馈以及相关的经验告诉我们,利用最新的TypeScript功能同时也容纳旧版本的用户是很困难的。TypeScript引入了一项名为typesVersions
的新功能,以帮助满足这些方案。
在 TypeScript 3.1 中使用节点模块分辨率时,当 TypeScript 破解打开package.json
文件以确定需要读取哪些文件时,它首先会查看名为typesVersions
的新字段。带有typesVersions
字段的package.json
可能如下所示:
{ "name": "package-name", "version": "1.0", "types": "./index.d.ts", "typesVersions": { ">=3.1": { "*": ["ts3.1/*"] } }}
这package.json
告诉TypeScript检查当前版本的TypeScript是否正在运行。如果它是3.1或更高版本,它会计算出您相对于包导入的路径,并从包的ts3.1
文件夹中读取。这就是{ "*": ["ts3.1/*"] }
表达的意思 - 如果你现在熟悉路径映射,它的工作原理就是这样的。
因此,在上面的示例中,如果我们从"package-name"
中导入,在TypeScript 3.1中运行时,我们将尝试从[...]/node_modules/package-name/ts3.1/index.d.ts
(和其他相关路径)解析。如果我们从package-name/foo
导入,我们将尝试寻找[...]/node_modules/package-name/ts3.1/foo.d.ts
和[...]/node_modules/package-name/ts3.1/foo/index.d.ts
。
如果我们在这个例子中没有在 TypeScript 3.1 中运行怎么办?好吧,如果typesVersions
中没有匹配的字段,TypeScript会回退到types
字段,因此 TypeScript 3.0 及更早版本将被重定向到[...]/node_modules/package-name/index.d.ts
。
TypeScript决定编译器和语言版本是否匹配的方式是使用Node的semver范围。
typesVersions
可以支持多个字段,其中每个字段名称由要匹配的范围指定。
{ "name": "package-name", "version": "1.0", "types": "./index.d.ts", "typesVersions": { ">=3.2": { "*": ["ts3.2/*"] }, ">=3.1": { "*": ["ts3.1/*"] } }}
由于范围可能会重叠,因此确定应用哪种重定向是特定于订单的。这意味着在上面的示例中,即使>=3.2
和>=3.1
匹配器都支持 TypeScript 3.2 及更高版本,反转顺序也可能有不同的行为,因此上述示例将不等同于以下示例:
{ "name": "package-name", "version": "1.0", "types": "./index.d.ts", "typesVersions": { // NOTE: this doesn't work! ">=3.1": { "*": ["ts3.1/*"] }, ">=3.2": { "*": ["ts3.2/*"] } }}
TypeScript是由微软开发的自由和开源的编程语言。
TypeScript是JavaScript的一个超集,从今天数以百万计的JavaScript开发者所熟悉的语法和语义开始。可以使用现有的JavaScript代码,包括流行的JavaScript库,并从JavaScript代码中调用TypeScript代码。
TypeScript可以编译出纯净、 简洁的JavaScript代码,并且可以运行在任何浏览器上、Node.js环境中和任何支持ECMAScript 3(或更高版本)的JavaScript引擎中。
TypeScript的优势在于:它有更多的规则和类型限制,代码具有更高的预测性、可控性,易于维护和调试;对模块、命名空间和面向对象的支持,更容易组织代码开发大型复杂程序。
另外,TypeScript的编译步骤可以捕获运行之前的错误。
接下来,让我们使用TypeScript开始构建一个简单的web应用程序。
有两种主要的方式获取TypeScript工具。
Visual Studio2015和Visual Studio 2013 Update 2默认包含了TypeScript。如果你没有安装包含TypeScript的Visual Studio ,你仍然可以下载。
使用NPM的开发者:
npm install -g typescript
在编辑器中创建greeter.ts
文件,并输入以下JavaScript代码:
function greeter(person){ return "Hello," + person;}var user = "Jane User";document.body.innerHTML = greeter(user);
虽然我们使用了.ts
作为文件扩展名,但是这些代码仅仅是JavaScript代码。你可以将代码直接复制粘贴到已有的JavaScript应用程序中。
在命令行中运行TypeScript编译器:
tsc greeter.ts
其结果你得到一个包含相同JavaScript代码的greeter.js
文件。在我们启动和运行的JavaScript应用程序中使用TypeScript。
现在我们可以开始利用TypeScript提供的新工具。给函数参数‘person’添加: string
类型注解,如下所示:
function greeter(person: string){ return "Hello," + person;}var user = "Jane User";document.body.innerHTML = greeter(user);
类型注解在TypeScript中是记录函数或变量约束的简便方法。在这个示例中,我们想要在调用greeter函数时传入一个字符串类型参数。我们可以尝试在调用greeter函数时变为传入一个数组:
function greeter(person: string){return"Hello, " + person;}var user = [0, 1 , 2];document.body.innerHTML = greeter(user);
重新编译,将看到一个错误:
greeter.ts(7,26): Supplied parameters do not match any signature of call target
同样,在调用greeter函数时尝试不传入任何参数。TypeScript将会告诉你调用这个函数时需要带一个参数。在这两个示例中,TypeScript基于你的代码结构和类型注解可以提供静态分析。
注意,虽然有错误,但是仍然编译创建了greeter.js
文件。即使你的代码中有错误,你仍旧可以使用TypeScript。但是在这种情况,TypeScript会发出警告:你的代码可能不能按照你预想的那样运行。
让我们进一步开发我们的demo。 在这里我们使用一个接口,它描述了具有firstName和lastName字段的对象。在TypeScript中,如果两个类型其内部结构兼容,那么这两种类型兼容。这使我们实现一个接口,仅仅只需必要的结构形状,而不必有明确的implements
子句。
interface Person { firstName: string ; lastName: string ;} function greeter ( person: Person ) { return "Hello, " + person.firstName + " " + person.lastName;} var user = { firstName: "Jane" , lastName: "User" }; document .body.innerHTML = greeter(user);
最后,让我们最后一次使用类来继续开发demo。TypeScript支持新的JavaScript特性,像基于类的面向对象编程的支持。
在这里,我们创建一个具有构造函数和一些公共字段的Student
类。注意:类和接口的良好配合使用,决定一个程序员的抽象水平。
此外,在构造函数参数中使用public
是一种简写形式,它将自动创建具有该名称的属性。
class Student { fullName: string ; constructor( public firstName, public middleInitial, public lastName) { this .fullName = firstName + " " + middleInitial + " " + lastName; }} interface Person { firstName: string ; lastName: string ;} function greeter ( person : Person ) { return "Hello, " + person.firstName + " " + person.lastName;} var user = new Student( "Jane" , "M." , "User" );document.body.innerHTML = greeter(user);
再次运行tsc greeter.ts
,你将看到生成的JavaScript代码和以前的一样。TypeScript中的类只是对于经常在JavaScript中使用了相同的基于原型的面向对象的简写。
现在在greeter.html
中输入以下代码:
<!DOCTYPE html> <html> <head><title>TypeScript Greeter</title></head> <body> <script src = "greeter.js"></script> </body> </html>
在浏览器中打开greeter.html
去运行第一个TypeScript web应用程序demo!
可选:在Visual Studio中打开greeter.ts
,或者复制代码到TypeScript学习乐园中。你可以将鼠标悬浮到标识符上查看类型。注意,在某些情况下这些类型会为你自动推断。重新输入最后一行,查看完成列表和基于DOM元素类型的参数帮助。将光标放到引用greeter函数的地方,并且按下F12键去转到定义。同样注意,你也可以在符号上右击使用重构来重命名。
所提供的类型信息和工具以及JavaScript在应用程序中一起工作。TypeScript更多可能性的示例,请浏览网站的案例。
TypeScript现在有两个特殊的类型:Null和Undefined, 它们的值分别是null
和undefined
。 以前这是不可能明确地命名这些类型的,但是现在 null
和undefined
不管在什么类型检查模式下都可以作为类型名称使用。
以前类型检查器认为null
和undefined
赋值给一切。实际上,null
和undefined
是每一个类型的有效值, 并且不能明确排除它们(因此不可能检测到错误)。
--strictNullChecks
--strictNullChecks
可以切换到新的严格空检查模式中。
在严格空检查模式中,null
和undefined
值不再属于任何类型的值,仅仅属于它们自己类型和any
类型的值 (还有一个例外, undefined
也能赋值给void
)。因此,尽管在常规类型检查模式下T
和T | undefined
被认为是相同的 (因为 undefined
被认为是任何T
的子类型),但是在严格类型检查模式下它们是不同的, 并且仅仅 T | undefined
允许有undefined
值,T
和T | null
的关系同样如此。
// 使用--strictNullChecks参数进行编译的let x: number;let y: number | undefined;let z: number | null | undefined;x = 1; // 正确y = 1; // 正确z = 1; // 正确x = undefined; // 错误y = undefined; // 正确z = undefined; // 正确x = null; // 错误y = null; // 错误z = null; // 正确x = y; // 错误x = z; // 错误y = x; // 正确y = z; // 错误z = x; // 正确z = y; // 正确
在严格空检查模式中,编译器要求未包含undefined
类型的局部变量在使用之前必须先赋值。
// 使用--strictNullChecks参数进行编译let x: number;let y: number | null;let z: number | undefined;x; // 错误,使用前未赋值y; // 错误,使用前未赋值z; // 正确x = 1;y = null;x; // 正确y; // 正确
编译器通过执行基于控制流的类型分析检查变量明确被赋过值。在本篇文章后面会有进一步的细节。
可选参数和属性会自动把undefined
添加到他们的类型中,即使他们的类型注解明确不包含undefined
。例如,下面两个类型是完全相同的:
// 使用--strictNullChecks参数进行编译type T1 = (x?: number) => string; // x的类型是 number | undefinedtype T2 = (x?: number | undefined) => string; // x的类型是 number | undefined
如果对象或者函数的类型包含null
和undefined
,那么访问属性或调用函数时就会产生编译错误。因此,对类型保护进行了扩展,以支持对非null和非undefined的检查。
// 使用--strictNullChecks参数进行编译declare function f(x: number): string;let x: number | null | undefined;if (x) { f(x); // 正确,这里的x类型是number}else { f(x); // 错误,这里的x类型是number?}let a = x != null ? f(x) : ""; // a的类型是stringlet b = x && f(x); // b的类型是 string | 0 | null | undefined
非null和非undefined类型保护可以使用==
、!=
、===
或!==
操作符和null
或undefined
进行比较,如x != null
或x === undefined
。对被试变量类型的影响准确地反映了JavaScript的语义(比如,双等号运算符检查两个值无论你指定的是null还是undefined,然而三等于号运算符仅仅检查指定的那一个值)。
类型保护以前仅仅支持对局部变量和参数的检查。现在类型保护支持检查由变量或参数名称后跟一个或多个访问属性组成的“点名称”。
interface Options { location?: { x?: number; y?: number; };}function foo(options?: Options) { if (options && options.location && options.location.x) { const x = options.location.x; // x的类型是number }}
点名称的类型保护和用户定义的类型保护函数,还有typeof
和instanceof
操作符一起工作,并且不依赖--strictNullChecks
编译参数。
对点名称进行类型保护后给点名称任一部分赋值都会导致类型保护无效。例如,对x.y.z
进行了类型保护后给x
、x.y
或x.y.z
赋值,都会导致x.y.z
类型保护无效。
表达式操作符允许运算对象的类型包含null
和/或undefined
,但是总是产生非null和非undefined类型的结果值。
// 使用--strictNullChecks参数进行编译function sum(a: number | null, b: number | null) { return a + b; // 计算的结果值类型是number}
&&
操作符添加null
和/或undefined
到右边操作对象的类型中取决于当前左边操作对象的类型,||
操作符从左边联合类型的操作对象的类型中将null
和undefined
同时删除。
// 使用--strictNullChecks参数进行编译interface Entity { name: string;}let x: Entity | null;let s = x && x.name; // s的类型是string | nulllet y = x || { name: "test" }; // y的类型是Entity
在严格空检查模式中,null
和undefined
类型是不会扩展到any
类型中的。
let z = null; // z的类型是null
在常规类型检查模式中,由于扩展,会推断z
的类型是any
,但是在严格空检查模式中,推断z
是null
类型(因此,如果没有类型注释,null
是z
的唯一值)。
在上下文中当类型检查器无法断定类型时,一个新的后缀表达式操作符!
可以用于断言操作对象是非null和非undefined类型的。具体而言,运算x!
产生一个不包含null
和undefined
的x
的值。断言的形式类似于<T>x
和x as T
,!
非空断言操作符会从编译成的JavaScript代码中移除。
// 使用--strictNullChecks参数进行编译function validateEntity(e?: Entity) { // 如果e是null或者无效的实体,就会抛出异常}function processEntity(e?: Entity) { validateEntity(e); let s = e!.name; // 断言e是非空并访问name属性}
这些新特性是经过设计的,使得它们能够在严格空检查模式和常规类型检查模式下都能够使用。尤其是在常规类型检查模式中,null
和undefined
类型会自动从联合类型中删除(因为它们是其它所有类型的子类型),!
非空断言表达式操作符也被允许使用但是没有任何作用。因此,声明文件使用null和undefined敏感类型更新后,在常规类型模式中仍然是可以向后兼容使用的。
在实际应用中,严格空检查模式要求编译的所有文件都是null和undefined敏感类型。
TypeScript 2.0实现了对局部变量和参数的控制流类型分析。以前,对类型保护进行类型分析仅限于if
语句和?:
条件表达式,并且不包括赋值和控制流结构的影响,例如return
和break
语句。使用TypeScript 2.0,类型检查器会分析语句和表达式所有可能的控制流,在任何指定的位置对声明为联合类型的局部变量或参数产生最可能的具体类型(缩小范围的类型)。
function foo(x: string | number | boolean) { if (typeof x === "string") { x; // 这里x的类型是string x = 1; x; // 这里x的类型是number } x; // 这里x的类型是number | boolean}function bar(x: string | number) { if (typeof x === "number") { return; } x; // 这里x的类型是string}
基于控制流的类型分析在--strictNullChecks
模式中尤为重要,因为可空类型使用联合类型来表示:
function test(x: string | null) { if (x === null) { return; } x; // 在函数的剩余部分中,x类型是string}
而且,在--strictNullChecks
模式中,基于控制流的分析包括,对类型不允许为undefined
的局部变量有明确赋值的分析。
function mumble(check: boolean) { let x: number; // 类型不允许为undefined x; // 错误,x是undefined if (check) { x = 1; x; // 正确 } x; // 错误,x可能是undefi x = 2; x; // 正确}
TypeScript 2.0实现了标记(或区分)联合类型。具体而言,TS编译器现在支持类型保护,基于判别属性的检查来缩小联合类型的范围,并且switch
语句也支持此特性。
interface Square { kind: "square"; size: number;}interface Rectangle { kind: "rectangle"; width: number; height: number;}interface Circle { kind: "circle"; radius: number;}type Shape = Square | Rectangle | Circle;function area(s: Shape) { // 在下面的switch语句中,s的类型在每一个case中都被缩小 // 根据判别属性的值,变量的其它属性不使用类型断言就可以被访问 switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.width * s.height; case "circle": return Math.PI * s.radius * s.radius; }}function test1(s: Shape) { if (s.kind === "square") { s; // Square } else { s; // Rectangle | Circle }}function test2(s: Shape) { if (s.kind === "square" || s.kind === "rectangle") { return; } s; // Circle}
判别属性类型保护是x.p == v
、x.p === v
、x.p != v
或者x.p !== v
其中的一种表达式,p
和v
是一个属性和字符串字面量类型或字符串字面量联合类型的表达式。判别属性类型保护缩小x
的类型到由判别属性p
和v
的可能值之一组成的类型。
请注意,我们目前只支持字符串字面值类型的判别属性。我们打算以后添加对布尔值和数字字面量类型的支持。
never
类型TypeScript 2.0引入了一个新原始类型never
。never
类型表示值的类型从不出现。具体而言,never
是永不返回函数的返回类型,也是变量在类型保护中永不为true的类型。
never
类型具有以下特征:
never
是所有类型的子类型并且可以赋值给所有类型。never
的子类型或能赋值给never
(never
类型本身除外)。return
语句,或者只有never
类型表达式的return
语句,并且如果函数是不可执行到终点的(例如通过控制流分析决定的),则推断函数的返回类型是never
。never
返回类型注解的函数中,所有return
语句(如果有的话)必须有never
类型的表达式并且函数的终点必须是不可执行的。因为never
是每一个类型的子类型,所以它总是在联合类型中被省略,并且在函数中只要其它类型被返回,类型推断就会忽略never
类型。
一些返回never
函数的示例:
// 函数返回never必须无法执行到终点function error(message: string): never { throw new Error(message);}// 推断返回类型是neverfunction fail() { return error("Something failed");}// 函数返回never必须无法执行到终点function infiniteLoop(): never { while (true) { }}
一些函数返回never
的使用示例:
// 推断返回类型是numberfunction move1(direction: "up" | "down") { switch (direction) { case "up": return 1; case "down": return -1; } return error("Should never get here");}// 推断返回类型是numberfunction move2(direction: "up" | "down") { return direction === "up" ? 1 : direction === "down" ? -1 : error("Should never get here");}// 推断返回类型是Tfunction check<T>(x: T | undefined) { return x || error("Undefined value");}
因为never
可以赋值给每一个类型,当需要回调函数返回一个更加具体的类型时,函数返回never
类型可以用于检测返回类型是否正确:
function test(cb: () => string) { let s = cb(); return s;}test(() => "hello");test(() => fail());test(() => { throw new Error(); })
属性或索引签名现在可以使用readonly
修饰符声明为只读的。
只读属性可以初始化和在同一个类的构造函数中被赋值,但是在其它情况下对只读属性的赋值是不允许的。
此外,有几种情况下实体隐式只读的:
get
访问器而没有使用set
访问器被视为只读的。const
变量被视为只读属性。import
语句中声明的实体被视为只读的。foo
当作import * as foo from "foo"
声明时,foo.x
是只读的)。interface Point { readonly x: number; readonly y: number;}var p1: Point = { x: 10, y: 20 };p1.x = 5; // 错误,p1.x是只读的var p2 = { x: 1, y: 1 };var p3: Point = p2; // 正确,p2的只读别名p3.x = 5; // 错误,p3.x是只读的p2.x = 5; // 正确,但是因为别名使用,同时也改变了p3.x
class Foo { readonly a = 1; readonly b: string; constructor() { this.b = "hello"; // 在构造函数中允许赋值 }}
let a: Array<number> = [0, 1, 2, 3, 4];let b: ReadonlyArray<number> = a;b[5] = 5; // 错误,元素是只读的b.push(5); // 错误,没有push方法(因为这会修改数组)b.length = 3; // 错误,length是只读的a = b; // 错误,缺少修改数组的方法
this
类型紧跟着类和接口,现在函数和方法也可以声明this
的类型了。
函数中this
的默认类型是any
。从TypeScript 2.0开始,你可以提供一个明确的this
参数。this
参数是伪参数,它位于函数参数列表的第一位:
function f(this: void) { // 确保`this`在这个独立的函数中无法使用}
this
参数库也可以使用this
参数声明回调函数如何被调用。
interface UIElement { addClickListener(onclick: (this: void, e: Event) => void): void;}
this:void
意味着addClickListener
预计onclick
是一个this
参数不需要类型的函数。
现在如果你在调用代码中对this
进行了类型注释:
class Handler { info: string; onClickBad(this: Handler, e: Event) { // 哎哟,在这里使用this.在运行中使用这个回调函数将会崩溃。 this.info = e.message; };}let h = new Handler();uiElement.addClickListener(h.onClickBad); // 错误!
--noImplicitThis
TypeScript 2.0还增加了一个新的编译选项用来标记函数中所有没有明确类型注释的this
的使用。
tsconfig.json
支持文件通配符文件通配符来啦!!支持文件通配符一直是最需要的特性之一。
类似文件通配符的文件模式支持两个属性"include"
和"exclude"
。
{ "compilerOptions": { "module": "commonjs", "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, "outFile": "../../built/local/tsc.js", "sourceMap": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules", "**/*.spec.ts" ]}
支持文件通配符的符号有:
*
匹配零个或多个字符(不包括目录)?
匹配任意一个字符(不包括目录)**/
递归匹配所有子目录如果文件通配符模式语句中只包含*
或.*
,那么只匹配带有扩展名的文件(例如默认是.ts
、.tsx
和.d.ts
,如果allowJs
设置为true
,.js
和.jsx
也属于默认)。
如果"files"
和"include"
都没有指定,编译器默认包含所有目录中的TypeScript文件(.ts
、.d.ts
和.tsx
),除了那些使用exclude
属性排除的文件外。如果allowJs
设置为true,JS文件(.js
和.jsx
)也会被包含进去。
如果"files"
和"include"
都指定了,编译器将包含这两个属性指定文件的并集。使用ourDir
编译选项指定的目录文件总是被排除,即使"exclude"
属性指定的文件也会被删除,但是files
属性指定的文件不会排除。
"exclude"
属性指定的文件会对"include"
属性指定的文件过滤。但是对"files"
指定的文件没有任何作用。当没有明确指定时,"exclude"
属性默认会排除node_modules
、bower_components
和jspm_packages
目录。
TypeScript 2.0提供了一系列额外的模块解析属性告诉编译器去哪里可以找到给定模块的声明。
更多详情,请参阅模块解析文档。
使用了AMD模块加载器并且模块在运行时”部署“到单文件夹的应用程序中使用baseUrl
是一种常用的做法。所有非相对名称的模块导入被认为是相对于baseUrl
的。
{ "compilerOptions": { "baseUrl": "./modules" }}
现在导入moduleA
将会在./modules/moduleA
中查找。
import A from "moduleA";
有时模块没有直接位于baseUrl中。加载器使用映射配置在运行时去映射模块名称和文件,请参阅RequireJs文档和SystemJS文档。
TypeScript编译器支持tsconfig
文件中使用"paths"
属性映射的声明。
例如,导入"jquery"
模块在运行时会被转换为"node_modules/jquery/dist/jquery.slim.min.js"
。
{ "compilerOptions": { "baseUrl": "./node_modules", "paths": { "jquery": ["jquery/dist/jquery.slim.min"] }}
使用"paths"
也允许更复杂的映射,包括多次后退的位置。考虑一个只有一个地方的模块是可用的,其它的模块都在另一个地方的项目配置。
rootDirs
和虚拟目录使用rootDirs
,你可以告知编译器的根目录组合这些“虚拟”目录。因此编译器在这些“虚拟”目录中解析相对导入模块,仿佛是合并到一个目录中一样。
给定的项目结构
src └── views └── view1.ts (imports './template1') └── view2.ts generated └── templates └── views └── template1.ts (imports './view2')
构建步骤将复制/src/views
和/generated/templates/views
目录下的文件输出到同一个目录中。在运行时,视图期望它的模板和它存在同一目录中,因此应该使用相对名称"./template"
导入。
"rootDir"
指定的一组根目录的内容将会在运行时合并。因此在我们的例子,tsconfig.json
文件应该类似于:
{ "compilerOptions": { "rootDirs": [ "src/views", "generated/templates/views" ] }}
--traceResolution
提供了一种方便的方法,以了解模块如何被编译器解析的。
tsc --traceResolution
当你使用一个新模块时,如果不想要花费时间书写一个声明时,现在你可以使用快捷声明以便以快速开始。
declare module "hot-new-module";
所有从快捷模块的导入都具有任意类型。
import x, {y} from "hot-new-module";x(y);
以前使用模块加载器(例如AMD和SystemJS)导入没有代码的资源是不容易的。之前,必须为每个资源定义一个外部模块声明。
TypeScript 2.0支持使用通配符符号(*
)定义一类模块名称。这种方式,一个声明只需要一次扩展名,而不再是每一个资源。
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);
当从一个基于非类型化的代码迁移时,通配符模块的名称可能更加有用。结合快捷外部模块声明,一组模块可以很容易地声明为any
。
declare module "myLibrary/*";
所有位于myLibrary
目录之下的模块的导入都被编译器认为是any
类型,因此这些模块的任何类型检查都会被关闭。
import { readFile } from "myLibrary/fileSystem/readFile`;readFile(); // readFile是'any'类型
一些库被设计为可以使用多种模块加载器或者不是使用模块加载器(全局变量)来使用,这被称为UMD或同构模块。这些库可以通过导入或全局变量访问。
举例:
export const isPrime(x: number): boolean;export as namespace mathLib;
然后,该库可作为模块导入使用:
import { isPrime } from "math-lib";isPrime(2);mathLib.isPrime(2); // 错误:无法在模块内部使用全局定义
它也可以被用来作为一个全局变量,只限于没有import
和export
脚本文件中。
mathLib.isPrime(2);
现在可以在类中声明可选属性和方法,与接口类似。
class Bar { a: number; b?: number; f() { return 1; } g?(): number; // 可选方法的方法体可以省略 h?() { return 2; }}
在--strictNullChecks
模式下编译时,可选属性和方法会自动添加undefined
到它们的类型中。因此,上面的b
属性类型是number | undefined
,上面g
方法的类型是(()=> number) | undefined
。使用类型保护可以去除undefined
。
类的构造函数可以被标记为private
或protected
。私有构造函数的类不能在类的外部实例化,并且也不能被继承。受保护构造函数的类不能再类的外部实例化,但是可以被继承。
class Singleton { private static instance: Singleton; private constructor() { } static getInstance() { if (!Singleton.instance) { Singleton.instance = new Singleton(); } return Singleton.instance; } }let e = new Singleton(); // 错误:Singleton的构造函数是私有的。let v = Singleton.getInstance();
抽象类可以声明抽象属性和、或访问器。所有子类将需要声明抽象属性或者被标记为抽象的。抽象属性不能初始化。抽象访问器不能有具体代码块。
abstract class Base { abstract name: string; abstract get value(); abstract set value(v: number);}class Derived extends Base { name = "derived"; value = 1;}
如果对象字面量中所有已知的属性是赋值给索引签名,那么现在对象字面量类型可以赋值给索引签名类型。这使得一个使用对象字面量初始化的变量作为参数传递给期望参数是map或dictionary的函数成为可能:
function httpService(path: string, headers: { [x: string]: string }) { }const headers = { "Content-Type": "application/x-www-form-urlencoded"};httpService("", { "Content-Type": "application/x-www-form-urlencoded" }); // 可以httpService("", headers); // 现在可以,以前不可以。
--lib
编译参数包含内置类型声明获取ES6/ES2015内置API声明仅限于target: ES6
。输入--lib
,你可以使用--lib
指定一组项目所需要的内置API。比如说,如果你希望项目运行时支持Map
、Set
和Promise
(例如现在静默更新浏览器),直接写--lib es2015.collection,es2015.promise
就好了。同样,你也可以排除项目中不需要的声明,例如在node项目中使用--lib es5,es6
排除DOM。
下面是列出了可用的API:
tsc --target es5 --lib es5,es2015.promise
"compilerOptions": { "lib": ["es5", "es2015.promise"]}
--noUnusedParameters
和--noUnusedLocals
标记未使用的声明TypeScript 2.0有两个新的编译参数来帮助你保持一个干净的代码库。-noUnusedParameters
编译参数标记所有未使用的函数或方法的参数错误。--noUnusedLocals
标记所有未使用的局部(未导出)声明像变量、函数、类和导入等等,另外未使用的私有类成员在--noUnusedLocals
作用下也会标记为错误。
import B, { readFile } from "./b";// ^ 错误:`B`声明了,但是没有使用。readFile();export function write(message: string, args: string[]) { // ^^^^ 错误:'arg'声明了,但是没有使用。 console.log(message);}
使用以_
开头命名的参数声明不会被未使用参数检查。例如:
function returnNull(_a) { // 正确 return null;}
.js
扩展名TypeScript 2.0之前,模块名称总是被认为是没有扩展名的。例如,导入一个模块import d from "./moduleA.js"
,则编译器在./moduleA.js.ts
或./moduleA.js.d.ts
中查找"moduleA.js"
的定义。这使得像 SystemJS这种期望模块名称是URI的打包或加载工具很难使用。
使用TypeScript 2.0,编译器将在./moduleA.ts
或./moduleA.d.ts
中查找"moduleA.js"
的定义。
target : es5
和module: es6
同时使用之前编译参数target : es5
和module: es6
同时使用被认为是无效的,但是现在是有效的。这将有助于使用基于ES2015的tree-shaking(将无用代码移除)比如 rollup。
现在函数形参和实参列表末尾允许有逗号。这是对第三阶段的ECMAScript提案的实现, 并且会编译为可用的 ES3/ES5/ES6。
function foo( bar: Bar, baz: Baz, // 形参列表末尾添加逗号是没有问题的。) { // 具体实现……}foo( bar, baz, // 实参列表末尾添加逗号同样没有问题);
--skipLibCheck
TypeScript 2.0添加了一个新的编译参数--skipLibCheck
,该参数可以跳过声明文件(以.d.ts
为扩展名的文件)的类型检查。当一个程序包含有大量的声明文件时,编译器需要花费大量时间对已知不包含错误的声明进行类型检查,通过跳过声明文件的类型检查,编译时间可能会大大缩短。
由于一个文件中的声明可以影响其他文件中的类型检查,当指定--skipLibCheck
时,一些错误可能检测不到。比如说, 如果一个非声明文件中的类型被声明文件用到, 可能仅在声明文件被检查时能发现错误. 不过这种情况在实际使用中并不常见。
这是重复定义错误的一个常见来源。多个声明文件定义相同的接口成员。
TypeScript 2.0放宽了这一约束,并允许可以不同代码块中出现重复的标识符, 只要它们有完全相同的类型。
在同一代码块重复定义仍不允许。
interface Error { stack?: string;}interface Error { code?: string; path?: string; stack?: string; // OK}
--declarationDir
--declarationDir
可以使生成的声明文件和JavaScript文件不在同一个位置中。
在JavaScript中,使用期望属性名称作为参数的API是相当普遍的,但到目前为止,还无法表达这些API中出现的类型关系。
输入索引类型查询或keyof
;索引类型查询keyof T
可以为T生成允许的属性名称类型。keyof T
类型被认为是一种string的子类型。
interface Person { name: string; age: number; location: string;}type K1 = keyof Person; // "name" | "age" | "location"type K2 = keyof Person[]; // "length" | "push" | "pop" | "concat" | ...type K3 = keyof { [x: string]: Person }; // string
其中的双重属性是索引访问类型,也称为lookup类型。从语法上讲,它们看起来完全像元素访问,但是写成类型:
type P1 = Person["name"]; // stringtype P2 = Person["name" | "age"]; // string | numbertype P3 = string["charAt"]; // (pos: number) => stringtype P4 = string[]["push"]; // (...items: string[]) => numbertype P5 = string[][0]; // string
您可以将此模式与类型系统的其他部分一起使用,以获得类型安全的查找。
function getProperty<T, K extends keyof T>(obj: T, key: K) { return obj[key]; // Inferred type is T[K]}function setProperty<T, K extends keyof T>(obj: T, key: K, value: T[K]) { obj[key] = value;}let x = { foo: 10, bar: "hello!" };let foo = getProperty(x, "foo"); // numberlet bar = getProperty(x, "bar"); // stringlet oops = getProperty(x, "wargarbl"); // Error! "wargarbl" is not "foo" | "bar"setProperty(x, "foo", "string"); // Error!, string expected number
一个常见的任务是采用现有类型并使其每个属性完全可选。假设我们有一个Person:
interface Person { name: string; age: number; location: string;}
它的部分版本是:
interface PartialPerson { name?: string; age?: number; location?: string;}
使用Mapped类型,PartialPerson可以写为Person类型的广义转换,例如:
type Partial<T> = { [P in keyof T]?: T[P];};type PartialPerson = Partial<Person>;
映射类型是通过获取文字类型的并集,并为新对象类型计算一组属性来生成的。它们与Python中的列表推导类似,但它们不是在列表中生成新元素,而是在类型中生成新属性。
除此了Partial之外,Mapped Types还可以在类型上表达许多有用的转换:
// Keep types the same, but make each property to be read-only.type Readonly<T> = { readonly [P in keyof T]: T[P];};// Same property names, but make the value a promise instead of a concrete onetype Deferred<T> = { [P in keyof T]: Promise<T[P]>;};// Wrap proxies around properties of Ttype Proxify<T> = { [P in keyof T]: { get(): T[P]; set(v: T[P]): void }};
Partial和Readonly,如前所述,是非常有用的结构。您可以使用它们来描述一些常见的JS例程,例如:
function assign<T>(obj: T, props: Partial<T>): void;function freeze<T>(obj: T): Readonly<T>;
因此,它们现在默认包含在标准库中。
我们还包括另外两种实用程序类型:Record和Pick。
// From T pick a set of properties Kdeclare function pick<T, K extends keyof T>(obj: T, ...keys: K[]): Pick<T, K>;const nameAndAgeOnly = pick(person, "name", "age"); // { name: string, age: number }
// For every properties K of type T, transform it to Ufunction mapObject<K extends string, T, U>(obj: Record<K, T>, f: (x: T) => U): Record<K, U>const names = { foo: "hello", bar: "world", baz: "bye" };const lengths = mapObject(names, s => s.length); // { foo: number, bar: number, baz: number }
TypeScript 2.1支持ESnext Spread和Rest。
与数组扩展类似,扩展对象可以很方便地获得浅层副本:
let copy = { ...original };
同样,您可以合并多个不同的对象。在以下示例中,merged将具有来自foo,bar和baz的属性。
let merged = { ...foo, ...bar, ...baz };
您还可以覆盖现有属性并添加新属性:
let obj = { x: 1, y: "string" };var newObj = {...obj, z: 3, y: 4}; // { x: number, y: number, z: number }
指定扩展操作的顺序决定了生成的对象中最终的属性;以后的属性会在以前创建的属性上“win out”。
对象休息是对象扩展的双重对象,因为它们可以提取在解构元素时不会被拾取的任何额外属性:
let obj = { x: 1, y: 1, z: 1 };let { z, ...obj1 } = obj;obj1; // {x: number, y:number};
在TypeScript 2.1之前支持此功能,但仅在定位ES6/ES2015时。TypeScript 2.1为ES3和ES5运行时提供了功能,这意味着无论您使用何种环境,您都可以自由地利用它。
注意:首先,我们需要确保我们的运行时具有全局可用的ECMAScript兼容性Promise。这可能涉及为Promise获取一个polyfill,或者依赖一个你可能在你所定位的运行时间。我们还需要确保TypeScript知道Promise是存在的,通过将lib标志设置为类似于"dom", "es2015"或"dom", "es2015.promise", "es5"的东西。
{ "compilerOptions": { "lib": ["dom", "es2015.promise", "es5"] }}
function delay(milliseconds: number) { return new Promise<void>(resolve => { setTimeout(resolve, milliseconds); });}async function dramaticWelcome() { console.log("Hello"); for (let i = 0; i < 3; i++) { await delay(500); console.log("."); } console.log("World!");}dramaticWelcome();
编译和运行输出应该会导致ES3/ES5引擎上的正确行为。
TypeScript注入了一些辅助函数,例如用于继承的__extends,在对象文字中用于扩展操作的__assign和JSX元素以及用于异步函数的__awaiter。
以前有两种选择:
这两个选项还有待改进;将帮助器捆绑在每个文件中对于试图保持其包装尺寸较小的客户来说是一个痛点。并且不包括帮助器,意味着客户必须维护自己的帮助程序库。
TypeScript 2.1允许在项目中将这些文件包含在一个单独的模块中,编译器将根据需要向它们发出导入。
首先,安装tslib实用程序库:
npm install tslib
其次,使用--importHelpers命令编译文件:
tsc --module commonjs --importHelpers a.ts
因此,给定以下输入,生成的.js文件将包含导入到tslib并使用其中的__assign帮助程序而不是内联它。
export const o = { a: 1, name: "o" };export const copy = { ...o };
"use strict";var tslib_1 = require("tslib");exports.o = { a: 1, name: "o" };exports.copy = tslib_1.__assign({}, exports.o);
传统上,TypeScript对于如何导入模块过于严格。这是为了避免拼写错误并阻止用户错误地使用模块。
但是,在很多时候,您可能只想导入可能没有自含.d.ts文件的现有模块。以前这是一个错误。从TypeScript 2.1开始,这现在变得更加容易。
使用TypeScript 2.1,您可以导入JavaScript模块而无需类型声明。如果存在类型声明(例如,declare module "foo" { ... },或node_modules/@types/foo)仍然具有优先权。
对没有声明文件的模块的导入仍将被标记为--noImplicitAny下的错误。
// Succeeds if `node_modules/asdf/index.js` existsimport { x } from "asdf";
TypeScript 2.1支持三个新的目标值--target ES2016,--target ES2017和--target ESNext。
使用target --target ES2016将指示编译器不要转换特定于ES2016的功能,例如**运算符。
同样,--target ES2017将指示编译器不要转换特定于ES2017的特性,如async/ await。
--target ESNext针对最新支持的ES提议功能。
以前,如果TypeScript无法确定变量的类型,则会选择any类型。
let x; // implicitly 'any'let y = []; // implicitly 'any[]'let z: any; // explicitly 'any'.
使用TypeScript 2.1,而不仅仅是选择any,TypeScript将根据您最后分配的内容推断类型。
仅在设置--noImplicitAny时才启用此选项。
let x;// You can still assign anything you want to 'x'.x = () => 42;// After that last assignment, TypeScript 2.1 knows that 'x' has type '() => number'.let y = x();// Thanks to that, it will now tell you that you can't add a number to a function!console.log(x + y);// ~~~~~// Error! Operator '+' cannot be applied to types '() => number' and 'number'.// TypeScript still allows you to assign anything you want to 'x'.x = "Hello world!";// But now it also knows that 'x' is a 'string'!x.toLowerCase();
现在也对空数组进行了相同类型的跟踪。
声明为没有类型注释且初始值为[]的变量被视为隐式any[]变量。然而,每个后续的x.push(value),x.unshift(value)或者x[n] = value操作根据添加到的元素来演变变量的类型。
function f1() { let x = []; x.push(5); x[1] = "hello"; x.unshift(true); return x; // (string | number | boolean)[]}function f2() { let x = null; if (cond()) { x = []; while (cond()) { x.push("hello"); } } return x; // string[] | null}
这样做的一个很大好处是,在运行--noImplicitAny时你会看到更少的隐式any错误。仅当编译器无法知道没有类型注释的变量类型时,才会报告隐式any错误。
function f3() { let x = []; // Error: Variable 'x' implicitly has type 'any[]' in some locations where its type cannot be determined. x.push(5); function g() { x; // Error: Variable 'x' implicitly has an 'any[]' type. }}
字符串,数字和布尔文字类型(例如"abc",1和true)仅在存在显式类型注释时才推断。从TypeScript 2.1开始,始终为const变量和readonly属性推断文字类型。
为没有类型注释的const变量或readonly属性推断的类型是文字初始值设定项的类型。为具有初始值设定项且没有类型注释的let变量,var变量,参数或非readonly属性推断的类型是初始化程序的扩展文字类型。对于字符串文字类型的加宽类型是string,number对于数字文字类型,boolean对于true或false,以及包含枚举文字类型的枚举。
const c1 = 1; // Type 1const c2 = c1; // Type 1const c3 = "abc"; // Type "abc"const c4 = true; // Type trueconst c5 = cond ? 1 : "abc"; // Type 1 | "abc"let v1 = 1; // Type numberlet v2 = c2; // Type numberlet v3 = c3; // Type stringlet v4 = c4; // Type booleanlet v5 = c5; // Type number | string
可以通过显式类型注释来控制文字类型扩展。具体来说,当为没有类型注释的const位置推断出文字类型的表达式时,该const变量将推断出一个加宽的文字类型。但是,当const位置具有显式文字类型注释时,该const变量将获得非加宽文字类型。
const c1 = "hello"; // Widening type "hello"let v1 = c1; // Type stringconst c2: "hello" = "hello"; // Type "hello"let v2 = c2; // Type "hello"
在ES2015中,返回对象的构造函数隐式地将this值替换为super()的任何调用者。因此,有必要捕获super()的任何潜在返回值,并且使用this替换它。此更改允许使用Custom Elements,该元素利用此特性用用户编写的构造函数初始化浏览器分配的元素。
class Base { x: number; constructor() { // return a new object other than `this` return { x: 1, }; }}class Derived extends Base { constructor() { super(); this.x = 2; }}
输出:
var Derived = (function (_super) { __extends(Derived, _super); function Derived() { var _this = _super.call(this) || this; _this.x = 2; return _this; } return Derived;}(Base));
这种变化导致扩展内置类(如,Error,Array,Map,等)的行为中断。
通常,一个项目有多个输出目标,例如ES5和ES2015,调试和生产,CommonJS和System;这两个目标之间只有少数配置选项发生变化,维护多个tsconfig.json文件可能很麻烦。
TypeScript 2.1支持使用extends继承配置,其中:
configs/base.json:
{ "compilerOptions": { "noImplicitAny": true, "strictNullChecks": true }}
tsconfig.json:
{ "extends": "./configs/base", "files": [ "main.ts", "supplemental.ts" ]}
tsconfig.nostrictnull.json:
{ "extends": "./tsconfig", "compilerOptions": { "strictNullChecks": false }}
使用--alwaysStrict调用编译器的原因:
模块在严格模式下自动解析。对于非模块代码,建议使用新标志。
在 TypeScript 3.1 中,元组和数组上的映射对象类型现在产生新的元组/数组,而不是创建一个新的类型,其中的成员,比如push()
,pop()
和length
将被转换。例如:
type MapToPromise<T> = { [K in keyof T]: Promise<T[K]> };type Coordinate = [number, number]type PromiseCoordinate = MapToPromise<Coordinate>; // [Promise<number>, Promise<number>]
MapToPromise
使用一种类型T
,当该类型是一个元组,例如Coordinate
,它只转换数字属性。在[number, number]
中,有两个数字命名的属性:0
和1
。当给出这样的元组时,MapToPromise
将创建一个新的元组,其中0
和1
属性是原始类型的Promise
s。因此,结果类型PromiseCoordinate
最终会得到类型[Promise<number>, Promise<number>]
。
TypeScript 3.1 能够在函数声明和const
-declared functons 上定义属性,只需在同一范围内分配这些函数的属性即可。这允许我们编写规范的JavaScript代码而不求助于namespace
。例如:
function readImage(path: string, callback: (err: any, image: Image) => void) { // ...}readImage.sync = (path: string) => { const contents = fs.readFileSync(path); return decodeImageSync(contents);}
在这里,我们有一个以非阻塞异步方式读取图像的readImage
函数。除了readImage
外,我们在readImage
提供了一个方便的函数,名为readImage.sync
。
虽然ECMAScript导出通常是提供此功能的更好方式,但这种新的支持允许使用此样式编写的代码“仅仅工作” TypeScript。另外,这种属性声明方法让我们在React无状态功能组件(SFCs)上表达常用模式,例如defaultProps
和propTypes
。
export const FooComponent => ({ name }) => ( <div>Hello! I am {name}</div>);FooComponent.defaultProps = { name: "(anonymous)",};
更具体地说,同态映射类型类似于上面的形式。
来自社区的反馈以及相关的经验告诉我们,利用最新的TypeScript功能同时也容纳旧版本的用户是很困难的。TypeScript引入了一项名为typesVersions
的新功能,以帮助满足这些方案。
在 TypeScript 3.1 中使用节点模块分辨率时,当 TypeScript 破解打开package.json
文件以确定需要读取哪些文件时,它首先会查看名为typesVersions
的新字段。带有typesVersions
字段的package.json
可能如下所示:
{ "name": "package-name", "version": "1.0", "types": "./index.d.ts", "typesVersions": { ">=3.1": { "*": ["ts3.1/*"] } }}
这package.json
告诉TypeScript检查当前版本的TypeScript是否正在运行。如果它是3.1或更高版本,它会计算出您相对于包导入的路径,并从包的ts3.1
文件夹中读取。这就是{ "*": ["ts3.1/*"] }
表达的意思 - 如果你现在熟悉路径映射,它的工作原理就是这样的。
因此,在上面的示例中,如果我们从"package-name"
中导入,在TypeScript 3.1中运行时,我们将尝试从[...]/node_modules/package-name/ts3.1/index.d.ts
(和其他相关路径)解析。如果我们从package-name/foo
导入,我们将尝试寻找[...]/node_modules/package-name/ts3.1/foo.d.ts
和[...]/node_modules/package-name/ts3.1/foo/index.d.ts
。
如果我们在这个例子中没有在 TypeScript 3.1 中运行怎么办?好吧,如果typesVersions
中没有匹配的字段,TypeScript会回退到types
字段,因此 TypeScript 3.0 及更早版本将被重定向到[...]/node_modules/package-name/index.d.ts
。
TypeScript决定编译器和语言版本是否匹配的方式是使用Node的semver范围。
typesVersions
可以支持多个字段,其中每个字段名称由要匹配的范围指定。
{ "name": "package-name", "version": "1.0", "types": "./index.d.ts", "typesVersions": { ">=3.2": { "*": ["ts3.2/*"] }, ">=3.1": { "*": ["ts3.1/*"] } }}
由于范围可能会重叠,因此确定应用哪种重定向是特定于订单的。这意味着在上面的示例中,即使>=3.2
和>=3.1
匹配器都支持 TypeScript 3.2 及更高版本,反转顺序也可能有不同的行为,因此上述示例将不等同于以下示例:
{ "name": "package-name", "version": "1.0", "types": "./index.d.ts", "typesVersions": { // NOTE: this doesn't work! ">=3.1": { "*": ["ts3.1/*"] }, ">=3.2": { "*": ["ts3.2/*"] } }}
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean
(其它语言中也一样)。
let isDone: boolean = false;
和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number
。 除了支持十进制和十六进制字面量,Typescript还支持ECMAScript 2015中引入的二进制和八进制字面量。
let decLiteral: number = 6;let hexLiteral: number = 0xf00d;let binaryLiteral: number = 0b1010;let octalLiteral: number = 0o744;
JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string
表示文本数据类型。 和JavaScript一样,可以使用双引号( "
)或单引号('
)表示字符串。
let name: string = "bob";name = "smith";
你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( `
),并且以${ expr }
这种形式嵌入表达式
let name: string = `Gene`;let age: number = 37;let sentence: string = `Hello, my name is ${ name }.I'll be ${ age + 1 } years old next month.`;
这与下面定义sentence
的方式效果相同:
let sentence: string = "Hello, my name is " + name + ".
" + "I'll be " + (age + 1) + " years old next month.";
TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上[]
,表示由此类型元素组成的一个数组:
let list: number[] = [1, 2, 3];
第二种方式是使用数组泛型,Array<元素类型>
:
let list: Array<number> = [1, 2, 3];
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为string
和number
类型的元组。
// Declare a tuple typelet x: [string, number];// Initialize itx = ['hello', 10]; // OK// Initialize it incorrectlyx = [10, 'hello']; // Error
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OKconsole.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toStringx[6] = true; // Error, 布尔不是(string | number)类型
联合类型是高级主题,我们会在以后的章节里讨论它。
enum
类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Color {Red, Green, Blue};let c: Color = Color.Green;
默认情况下,从0
开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1
开始编号:
enum Color {Red = 1, Green, Blue};let c: Color = Color.Green;
或者,全部都采用手动赋值:
enum Color {Red = 1, Green = 2, Blue = 4};let c: Color = Color.Green;
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
enum Color {Red = 1, Green, Blue};let colorName: string = Color[2];alert(colorName);
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any
类型来标记这些变量:
let notSure: any = 4;notSure = "maybe a string instead";notSure = false; // okay, definitely a boolean
在对现有代码进行改写的时候,any
类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object
有相似的作用,就像它在其它语言中那样。 但是 Object
类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
let notSure: any = 4;notSure.ifItExists(); // okay, ifItExists might exist at runtimenotSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)let prettySure: Object = 4;prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
当你只知道一部分数据的类型时,any
类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:
let list: any[] = [1, true, "free"];list[1] = 100;
某种程度上来说,void
类型像是与any
类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void
:
function warnUser(): void { alert("This is my warning message");}
声明一个void
类型的变量没有什么大用,因为你只能为它赋予undefined
和null
:
let unusable: void = undefined;
TypeScript里,undefined
和null
两者各自有自己的类型分别叫做undefined
和null
。 和 void
相似,它们的本身的类型用处不是很大:
// Not much else we can assign to these variables!let u: undefined = undefined;let n: null = null;
默认情况下null
和undefined
是所有类型的子类型。 就是说你可以把 null
和undefined
赋值给number
类型的变量。
然而,当你指定了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们各自。 这能避免很多常见的问题。 也许在某处你想传入一个 string
或null
或undefined
,你可以使用联合类型string | null | undefined
。 再次说明,稍后我们会介绍联合类型。
注意:我们鼓励尽可能地使用
--strictNullChecks
,但在本手册里我们假设这个标记是关闭的。
never
类型表示的是那些永不存在的值的类型。 例如, never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never
类型,当它们被永不为真的类型保护所约束时。
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never
的子类型或可以赋值给never
类型(除了never
本身之外)。 即使 any
也不可以赋值给never
。
下面是一些返回never
类型的函数:
// 返回never的函数必须存在无法达到的终点function error(message: string): never { throw new Error(message);}// 推断的返回值类型为neverfunction fail() { return error("Something failed");}// 返回never的函数必须存在无法达到的终点function infiniteLoop(): never { while (true) { }}
有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";let strLength: number = (<string>someValue).length;
另一个为as
语法:
let someValue: any = "this is a string";let strLength: number = (someValue as string).length;
两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as
语法断言是被允许的。
let
你可能已经注意到了,我们使用let
关键字来代替大家所熟悉的JavaScript关键字var
。 let
关键字是JavaScript的一个新概念,TypeScript实现了它。 我们会在以后详细介绍它,很多常见的问题都可以通过使用 let
来解决,所以尽可能地使用let
来代替var
吧。
let
和const
是JavaScript里相对较新的变量声明方式。 像我们之前提到过的, let
在很多方面与var
是相似的,但是可以帮助大家避免在JavaScript里常见一些问题。 const
是对let
的一个增强,它能阻止对一个变量再次赋值。
因为TypeScript是JavaScript的超集,所以它本身就支持let
和const
。 下面我们会详细说明这些新的声明方式以及为什么推荐使用它们来代替 var
。
如果你之前使用JavaScript时没有特别在意,那么这节内容会唤起你的回忆。 如果你已经对 var
声明的怪异之处了如指掌,那么你可以轻松地略过这节。
var
声明一直以来我们都是通过var
关键字定义JavaScript变量。
var a = 10;
大家都能理解,这里定义了一个名为a
值为10
的变量。
我们也可以在函数内部定义变量:
function f() { var message = "Hello, world!"; return message;}
并且我们也可以在其它函数内部访问相同的变量。
function f() { var a = 10; return function g() { var b = a + 1; return b; }}var g = f();g(); // returns 11;
上面的例子里,g
可以获取到f
函数里定义的a
变量。 每当 g
被调用时,它都可以访问到f
里的a
变量。 即使当g
在f
已经执行完后才被调用,它仍然可以访问及修改a
。
function f() { var a = 1; a = 2; var b = g(); a = 3; return b; function g() { return a; }}f(); // returns 2
对于熟悉其它语言的人来说,var
声明有些奇怪的作用域规则。 看下面的例子:
function f(shouldInitialize: boolean) { if (shouldInitialize) { var x = 10; } return x;}f(true); // returns '10'f(false); // returns 'undefined'
有些读者可能要多看几遍这个例子。 变量 x
是定义在*if
语句里面*,但是我们却可以在语句的外面访问它。 这是因为 var
声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),包含它的代码块对此没有什么影响。 有些人称此为* var
作用域或函数作用域*。 函数参数也使用函数作用域。
这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:
function sumMatrix(matrix: number[][]) { var sum = 0; for (var i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (var i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum;}
这里很容易看出一些问题,里层的for
循环会覆盖变量i
,因为所有i
都引用相同的函数作用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。
快速的猜一下下面的代码会返回什么:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i);}
介绍一下,setTimeout
会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。
好吧,看一下结果:
10101010101010101010
很多JavaScript程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:
0123456789
还记得我们上面讲的变量获取吗?
每当
g
被调用时,它都可以访问到f
里的a
变量。
让我们花点时间考虑在这个上下文里的情况。 setTimeout
在若干毫秒后执行一个函数,并且是在for
循环结束后。 for
循环结束后,i
的值为10
。 所以当函数被调用的时候,它会打印出 10
!
一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i
的值:
for (var i = 0; i < 10; i++) { // capture the current state of 'i' // by invoking a function with its current value (function(i) { setTimeout(function() { console.log(i); }, 100 * i); })(i);}
这种奇怪的形式我们已经司空见惯了。 参数 i
会覆盖for
循环里的i
,但是因为我们起了同样的名字,所以我们不用怎么改for
循环体里的代码。
let
声明现在你已经知道了var
存在一些问题,这恰好说明了为什么用let
语句来声明变量。 除了名字不同外, let
与var
的写法一致。
let hello = "Hello!";
主要的区别不在语法上,而是语义,我们接下来会深入研究。
当用let
声明一个变量,它使用的是词法作用域或块作用域。 不同于使用 var
声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for
循环之外是不能访问的。
function f(input: boolean) { let a = 100; if (input) { // Still okay to reference 'a' let b = a + 1; return b; } // Error: 'b' doesn't exist here return b;}
这里我们定义了2个变量a
和b
。 a
的作用域是f
函数体内,而b
的作用域是if
语句块里。
在catch
语句里声明的变量也具有同样的作用域规则。
try { throw "oh no!";}catch (e) { console.log("Oh well.");}// Error: 'e' doesn't exist hereconsole.log(e);
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 时间死区。 它只是用来说明我们不能在 let
语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。
a++; // illegal to use 'a' before it's declared;let a;
注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。
function foo() { // okay to capture 'a' return a;}// 不能在'a'被声明前调用'foo'// 运行时应该抛出错误foo();let a;
关于时间死区的更多信息,查看这里Mozilla Developer Network.
我们提过使用var
声明时,它不在乎你声明多少次;你只会得到1个。
function f(x) { var x; var x; if (true) { var x; }}
在上面的例子里,所有x
的声明实际上都引用一个相同的x
,并且这是完全有效的代码。 这经常会成为bug的来源。 好的是, let
声明就不会这么宽松了。
let x = 10;let x = 20; // 错误,不能在1个作用域里多次声明`x`
并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。
function f(x) { let x = 100; // error: interferes with parameter declaration}function g() { let x = 100; var x = 100; // error: can't have both declarations of 'x'}
并不是说块级作用域变量不能在函数作用域内声明。 而是块级作用域变量需要在不用的块里声明。
function f(condition, x) { if (condition) { let x = 100; return x; } return x;}f(false, 0); // returns 0f(true, 0); // returns 100
在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用 let
重写之前的sumMatrix
函数。
function sumMatrix(matrix: number[][]) { let sum = 0; for (let i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (let i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum;}
这个版本的循环能得到正确的结果,因为内层循环的i
可以屏蔽掉外层循环的i
。
通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好打算一下。
在我们最初谈及获取用var
声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。 直观地讲,每次进入一个作用域时,它创建了一个变量的 环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
function theCityThatAlwaysSleeps() { let getCity; if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity();}
因为我们已经在city
的环境里获取到了city
,所以就算if
语句执行结束后我们仍然可以访问它。
回想一下前面setTimeout
的例子,我们最后需要使用立即执行的函数表达式来获取每次for
循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。 这样做挺痛苦的,但是幸运的是,你不必在TypeScript里这样做了。
当let
声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout
例子里我们仅使用let
声明就可以了。
for (let i = 0; i < 10 ; i++) { setTimeout(function() {console.log(i); }, 100 * i);}
会输出与预料一致的结果:
0123456789
const
声明const
声明是声明变量的另一种方式。
const numLivesForCat = 9;
它们与let
声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let
相同的作用域规则,但是不能对它们重新赋值。
这很好理解,它们引用的值是不可变的。
const numLivesForCat = 9;const kitty = { name: "Aurora", numLives: numLivesForCat,}// Errorkitty = { name: "Danielle", numLives: numLivesForCat};// all "okay"kitty.name = "Rory";kitty.name = "Kitty";kitty.name = "Cat";kitty.numLives--;
除非你使用特殊的方法去避免,实际上const
变量的内部状态是可修改的。 幸运的是,TypeScript允许你将对象的成员设置成只读的。 接口一章有详细说明。
let
vs. const
现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。
使用最小特权原则,所有变量除了你计划去修改的都应该使用const
。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const
也可以让我们更容易的推测数据的流动。
另一方面,用户很喜欢let
的简洁性。 这个手册大部分地方都使用了 let
。
跟据你的自己判断,如果合适的话,与团队成员商议一下。有趣的是,TypeScript允许您指定对象的成员为只读。关于接口的章节有详细说明。
解构
Another TypeScript已经可以解析其它 ECMAScript 2015 特性了。 完整列表请参见 the article on the Mozilla Developer Network。 本章,我们将给出一个简短的概述。
最简单的解构莫过于数组的解构赋值了:
let input = [1, 2];let [first, second] = input;console.log(first); // outputs 1console.log(second); // outputs 2
这创建了2个命名变量 first
和 second
。 相当于使用了索引,但更为方便:
first = input[0];second = input[1];
解构作用于已声明的变量会更好:
// swap variables[first, second] = [second, first];
作用于函数参数:
function f([first, second]: [number, number]) { console.log(first); console.log(second);}f(input);
你可以使用...name
语法创建一个剩余变量列表:
let [first, ...rest] = [1, 2, 3, 4];console.log(first); // outputs 1console.log(rest); // outputs [ 2, 3, 4 ]
当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:
let [first] = [1, 2, 3, 4];console.log(first); // outputs 1
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4];
你也可以解构对象:
let o = { a: "foo", b: 12, c: "bar"}let {a, b} = o;
这通过 o.a
and o.b
创建了 a
和 b
。 注意,如果你不需要 c
你可以忽略它。
就像数组解构,你可以用没有声明的赋值:
({a, b} = {a: "baz", b: 101});
注意,我们需要用括号将它括起来,因为Javascript通常会将以 {
起始的语句解析为一个块。
你也可以给属性以不同的名字:
let {a: newName1, b: newName2} = o;
这里的语法开始变得混乱。 你可以将 a: newName1
读做 "a
作为 newName1
"。 方向是从左到右,好像你写成了以下样子:
let newName1 = o.a;let newName2 = o.b;
令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。
let {a, b}: {a: string, b: number} = o;
默认值可以让你在属性为 undefined 时使用缺省值:
function keepWholeObject(wholeObject: {a: string, b?: number}) { let {a, b = 1001} = wholeObject;}
现在,即使 b
为 undefined , keepWholeObject
函数的变量 wholeObject
的属性 a
和 b
都会有值。
解构也能用于函数声明。 看以下简单的情况:
type C = {a: string, b?: number}function f({a, b}: C): void { // ...}
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要知道在设置默认值之前设置其类型。
function f({a, b} = {a: "", b: 0}): void { // ...}f(); // ok, default to {a: "", b: 0}
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C
的定义有一个 b
可选属性:
function f({a, b = 0} = {a: ""}): void { // ...}f({a: "yes"}) // ok, default b = 0f() // ok, default to {a: ""}, which then defaults b = 0f({}) // error, 'a' is required if you supply an argument
要小心使用解构。 从前面的例子可以看出,就算是最简单的解构也会有很多问题。 尤其当存在深层嵌套解构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解,也是令人难以理解的。 解构表达式要尽量保持小而简单。 你自己也可以直接使用解构将会生成的赋值表达式。
TypeScript的核心原则之一是对值所具有的shape进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
下面通过一个简单示例来观察接口是如何工作的:
function printLabel(labelledObj: { label: string }) { console.log(labelledObj.label);}let myObj = { size: 10, label: "Size 10 Object" };printLabel(myObj);
类型检查器会查看printLabel
的调用。 printLabel
有一个参数,并要求这个对象参数有一个名为label
类型为string
的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候TypeScript却并不会这么宽松,我们下面会稍做讲解。
下面我们重写上面的例子,这次使用接口来描述:必须包含一个label
属性且类型为string
:
interface LabelledValue { label: string;}function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label);}let myObj = {size: 10, label: "Size 10 Object"};printLabel(myObj);
LabelledValue
接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label
属性且类型为string
的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel
的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。
还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
下面是应用了“option bags”的例子:
interface SquareConfig { color?: string; width?: number;}function createSquare(config: SquareConfig): {color: string; area: number} { let newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare;}let mySquare = createSquare({color: "black"});
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?
符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将 createSquare
里的color
属性名拼错,就会得到一个错误提示:
interface SquareConfig { color?: string; width?: number;}function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare = {color: "white", area: 100}; if (config.color) { // Error: Property 'collor' does not exist on type 'SquareConfig' newSquare.color = config.collor; // Type-checker can catch the mistyped name here } if (config.width) { newSquare.area = config.width * config.width; } return newSquare;}let mySquare = createSquare({color: "black"});
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly
来指定只读属性:
interface Point { readonly x: number; readonly y: number;}
你可以通过赋值一个对象字面量来构造一个Point
。 赋值后, x
和y
再也不能被改变了。
let p1: Point = { x: 10, y: 20 };p1.x = 5; // error!
TypeScript具有ReadonlyArray<T>
类型,它与Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];let ro: ReadonlyArray<number> = a;ro[0] = 12; // error!ro.push(5); // error!ro.length = 100; // error!a = ro; // error!
上面代码的最后一行,可以看到就算把整个ReadonlyArray
赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:
a = ro as number[];
readonly
vs const
最简单判断该用readonly
还是const
的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用const
,若做为属性则使用readonly
。
我们在第一个例子里使用了接口,TypeScript让我们传入{ size: number; label: string; }
到仅期望得到{ label: string; }
的函数里。 我们已经学过了可选属性,并且知道他们在“option bags”模式里很有用。
然而,天真地将这两者结合的话就会像在JavaScript里那样搬起石头砸自己的脚。 比如,拿 createSquare
例子来说:
interface SquareConfig { color?: string; width?: number;}function createSquare(config: SquareConfig): { color: string; area: number } { // ...}let mySquare = createSquare({ colour: "red", width: 100 });
注意传入createSquare
的参数拼写为*colour
*而不是color
。 在JavaScript里,这会默默地失败。
你可能会争辩这个程序已经正确地类型化了,因为width
属性是兼容的,不存在color
属性,而且额外的colour
属性是无意义的。
然而,TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
// error: 'colour' not expected in type 'SquareConfig'let mySquare = createSquare({ colour: "red", width: 100 });
绕开这些检查非常简单。 最简便的方法是使用类型断言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig
带有上面定义的类型的color
和width
属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:
interface SquareConfig { color?: string; width?: number; [propName: string]: any;}
我们稍后会讲到索引签名,但在这我们要表示的是SquareConfig
可以有任意数量的属性,并且只要它们不是color
和width
,那么就无所谓它们的类型是什么。
还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为squareOptions
不会经过额外属性检查,所以编译器不会报错。
let squareOptions = { colour: "red", width: 100 };let mySquare = createSquare(squareOptions);
要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的bug。 就是说你遇到了额外类型检查出的错误,比如选择包,你应该去审查一下你的类型声明。 在这里,如果支持传入 color
或colour
属性到createSquare
,你应该修改SquareConfig
定义来体现出这一点。
接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
interface SearchFunc { (source: string, subString: string): boolean;}
这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。
let mySearch: SearchFunc;mySearch = function(source: string, subString: string) { let result = source.search(subString); if (result == -1) { return false; } else { return true; }}
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:
let mySearch: SearchFunc;mySearch = function(src: string, sub: string): boolean { let result = src.search(sub); if (result == -1) { return false; } else { return true; }}
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,Typescript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc
类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是 false
和true
)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与SearchFunc
接口中的定义不匹配。
let mySearch: SearchFunc;mySearch = function(src, sub) { let result = src.search(sub); if (result == -1) { return false; } else { return true; }}
与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]
或ageMap["daniel"]
。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:
interface StringArray { [index: number]: string;}let myArray: StringArray;myArray = ["Bob", "Fred"];let myStr: string = myArray[0];
上面例子里,我们定义了StringArray
接口,它具有索引签名。 这个索引签名表示了当用 number
去索引StringArray
时会得到string
类型的返回值。
共有支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
class Animal { name: string;}class Dog extends Animal { breed: string;}// Error: indexing with a 'string' will sometimes get you a Dog!interface NotOkay { [x: number]: Animal; [x: string]: Dog;}
字符串索引签名能够很好的描述dictionary
模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property
和obj["property"]
两种形式都可以。 下面的例子里, name
的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
interface NumberDictionary { [index: string]: number; length: number; // 可以,length是number类型 name: string // 错误,`name`的类型不是索引类型的子类型}
最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray { readonly [index: number]: string;}let myArray: ReadonlyStringArray = ["Alice", "Bob"];myArray[2] = "Mallory"; // error!
你不能设置myArray[2]
,因为索引签名是只读的。
与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface { currentTime: Date;}class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { }}
你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime
方法一样:
interface ClockInterface { currentTime: Date; setTime(d: Date);}class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { }}
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
interface ClockConstructor { new (hour: number, minute: number);}class Clock implements ClockConstructor { currentTime: Date; constructor(h: number, m: number) { }}
这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor
为构造函数所用和ClockInterface
为实例方法所用。 为了方便我们定义一个构造函数 createClock
,它用传入的类型创建实例。
interface ClockConstructor { new (hour: number, minute: number): ClockInterface;}interface ClockInterface { tick();}function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface { return new ctor(hour, minute);}class DigitalClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("beep beep"); }}class AnalogClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("tick tock"); }}let digital = createClock(DigitalClock, 12, 17);let analog = createClock(AnalogClock, 7, 32);
因为createClock
的第一个参数是ClockConstructor
类型,在createClock(AnalogClock, 7, 32)
里,会检查AnalogClock
是否符合构造函数签名。
和类一样,接口也可以相互扩展。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface Shape { color: string;}interface Square extends Shape { sideLength: number;}let square = <Square>{};square.color = "blue";square.sideLength = 10;
一个接口可以继承多个接口,创建出多个接口的合成接口。
interface Shape { color: string;}interface PenStroke { penWidth: number;}interface Square extends Shape, PenStroke { sideLength: number;}let square = <Square>{};square.color = "blue";square.sideLength = 10;square.penWidth = 5.0;
先前我们提过,接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
一个例子就是,一个接口可以同时做为函数和对象使用,并带有额外的属性。
interface Counter { (start: number): string; interval: number; reset(): void;}function getCounter(): Counter { let counter = <Counter>function (start: number) { }; counter.interval = 123; counter.reset = function () { }; return counter;}let c = getCounter();c(10);c.reset();c.interval = 5.0;
在使用JavaScript第三方库的时候,你可能需要像上面那样去完整地定义类型。
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
这是很有用的,当你有一个很深层次的继承,但是只想你的代码只是针对拥有特定属性的子类起作用的时候。子类除了继承自基类外与基类没有任何联系。 例:
class Control { private state: any;}interface SelectableControl extends Control { select(): void;}class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
select() { }
}
class Location {
}
在上面的例子里,SelectableControl
包含了Control
的所有成员,包括私有成员state
。 因为 state
是私有成员,所以只能够是Control
的子类们才能实现SelectableControl
接口。 因为只有 Control
的子类才能够拥有一个声明于Control
的私有成员state
,这对私有成员的兼容性是必需的。
在Control
类内部,是允许通过SelectableControl
的实例来访问私有成员state
的。 实际上,SelectableControl
就像Control
一样,并拥有一个select
方法。 Button
和TextBox
类是SelectableControl
的子类(因为它们都继承自Control
并有select
方法),但Image
和Location
类并不是这样的。
传统的JavaScript的程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来从ECMAScript 2015,也就是ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。使用TypeScript,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript的版本。
下面看一个使用类的例子:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; }}let greeter = new Greeter("world");
如果你使用过C#或Java,你会对这种语法非常熟悉。我们声明一个 Greeter
类。这个类有3个成员:一个叫做greeting
的属性,一个构造函数和一个greet
方法。
你会注意到,我们在引用任何一个类成员的时候都用了this
。它表示我们访问的是类的成员。
最后一行,我们使用new
构造了Greeter
类的一个实例。它会调用之前定义的构造函数,创建一个 Greeter
类型的新对象,并执行构造函数初始化它。
在TypeScript里,我们可以使用常用的面向对象模式。当然,基于类的程序设计中最基本的模式是允许使用继承来扩展现有的类。
看下面的例子:
class Animal { name:string; constructor(theName: string) { this.name = theName; } move(distanceInMeters: number = 0) { console.log(`${this.name} moved ${distanceInMeters}m.`); }}class Snake extends Animal { constructor(name: string) { super(name); } move(distanceInMeters = 5) { console.log("Slithering..."); super.move(distanceInMeters); }}class Horse extends Animal { constructor(name: string) { super(name); } move(distanceInMeters = 45) { console.log("Galloping..."); super.move(distanceInMeters); }}let sam = new Snake("Sammy the Python");let tom: Animal = new Horse("Tommy the Palomino");sam.move();tom.move(34);
这个例子展示了TypeScript中继承的一些特征,它们与其它语言类似。 我们使用 extends
关键字来创建子类。你可以看到Horse
和Snake
类是基类Animal
的子类,并且可以访问其属性和方法。
包含构造函数的派生类必须调用super()
,它会执行基类的构造方法。
这个例子演示了如何在子类里可以重写父类的方法。 Snake
类和Horse
类都创建了move
方法,它们重写了从Animal
继承来的move
方法,使得move
方法根据不同的类而具有不同的功能。 注意,即使 tom
被声明为Animal
类型,但因为它的值是Horse
,tom.move(34)
会调用Horse
里的重写方法:
Slithering...Sammy the Python moved 5m.Galloping...Tommy the Palomino moved 34m.
public
在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public
来做修饰;例如,C#要求必须明确地使用public
指定成员是可见的。 在TypeScript里,成员都默认为 public
。
你也可以明确的将一个成员标记成public
。 我们可以用下面的方式来重写上面的 Animal
类:
class Animal { public name: string; public constructor(theName: string) { this.name = theName; } public move(distanceInMeters: number) { console.log(`${this.name} moved ${distanceInMeters}m.`); }}
private
当成员被标记成private
时,它就不能在声明它的类的外部访问。比如:
class Animal { private name: string; constructor(theName: string) { this.name = theName; }}new Animal("Cat").name; // Error: 'name' is private;
TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
然而,当我们比较带有private
或protected
成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private
成员,那么只有当另外一个类型中也存在这样一个private
成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected
成员也使用这个规则。
下面来看一个例子,更好地说明了这一点:
class Animal { private name: string; constructor(theName: string) { this.name = theName; }}class Rhino extends Animal { constructor() { super("Rhino"); }}class Employee { private name: string; constructor(theName: string) { this.name = theName; }}let animal = new Animal("Goat");let rhino = new Rhino();let employee = new Employee("Bob");animal = rhino;animal = employee; // Error: Animal and Employee are not compatible
这个例子中有Animal
和Rhino
两个类,Rhino
是Animal
类的子类。 还有一个 Employee
类,其类型看上去与Animal
是相同的。 我们创建了几个这些类的实例,并相互赋值来看看会发生什么。 因为 Animal
和Rhino
共享了来自Animal
里的私有成员定义private name: string
,因此它们是兼容的。 然而 Employee
却不是这样。当把Employee
赋值给Animal
的时候,得到一个错误,说它们的类型不兼容。 尽管 Employee
里也有一个私有成员name
,但它明显不是Animal
里面定义的那个。
protected
protected
修饰符与private
修饰符的行为很相似,但有一点不同,protected
成员在派生类中仍然可以访问。例如:
class Person { protected name: string; constructor(name: string) { this.name = name; }}class Employee extends Person { private department: string; constructor(name: string, department: string) { super(name) this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; }}let howard = new Employee("Howard", "Sales");console.log(howard.getElevatorPitch());console.log(howard.name); // error
注意,我们不能在Person
类外使用name
,但是我们仍然可以通过Employee
类的实例方法访问,因为Employee
是由Person
派生而来的。
构造函数也可以被标记成protected
。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如,
class Person { protected name: string; protected constructor(theName: string) { this.name = theName; }}// Employee can extend Personclass Employee extends Person { private department: string; constructor(name: string, department: string) { super(name); this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; }}let howard = new Employee("Howard", "Sales");let john = new Person("John"); // Error: The 'Person' constructor is protected
你可以使用readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Octopus { readonly name: string; readonly numberOfLegs: number = 8; constructor (theName: string) { this.name = theName; }}let dad = new Octopus("Man with the 8 strong legs");dad.name = "Man with the 3-piece suit"; // error! name is readonly.
在上面的例子中,我们不得不定义一个受保护的成员name
和一个构造函数参数theName
在Person
类里,并且立刻给name
和theName
赋值。 这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Animal
类的修改版,使用了参数属性:
class Animal { constructor(private name: string) { } move(distanceInMeters: number) { console.log(`${this.name} moved ${distanceInMeters}m.`); }}
注意看我们是如何舍弃了theName
,仅在构造函数里使用private name: string
参数来创建和初始化name
成员。 我们把声明和赋值合并至一处。
参数属性通过给构造函数参数添加一个访问限定符来声明。 使用 private
限定一个参数属性会声明并初始化一个私有成员;对于public
和protected
来说也是一样。
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
下面来看如何把一个简单的类改写成使用get
和set
。 首先,我们从一个没有使用存取器的例子开始。
class Employee { fullName: string;}let employee = new Employee();employee.fullName = "Bob Smith";if (employee.fullName) { console.log(employee.fullName);}
我们可以随意的设置fullName
,这是非常方便的,但是这也可能会带来麻烦。
下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对 fullName
的直接访问改成了可以检查密码的set
方法。 我们也加了一个 get
方法,让上面的例子仍然可以工作。
let passcode = "secret passcode";class Employee { private _fullName: string; get fullName(): string { return this._fullName; } set fullName(newName: string) { if (passcode && passcode == "secret passcode") { this._fullName = newName; } else { console.log("Error: Unauthorized update of employee!"); } }}let employee = new Employee();employee.fullName = "Bob Smith";if (employee.fullName) { alert(employee.fullName);}
我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。
对于存取器有下面几点需要注意的:
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get
不带有set
的存取器自动被推断为readonly
。 这在从代码生成 .d.ts
文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用 static
定义origin
,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin
前面加上类名。 如同在实例属性上使用 this.
前缀来访问属性一样,这里我们使用Grid.
来访问静态属性。
class Grid { static origin = {x: 0, y: 0}; calculateDistanceFromOrigin(point: {x: number; y: number;}) { let xDist = (point.x - Grid.origin.x); let yDist = (point.y - Grid.origin.y); return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale; } constructor (public scale: number) { }}let grid1 = new Grid(1.0); // 1x scalelet grid2 = new Grid(5.0); // 5x scaleconsole.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法。
abstract class Animal { abstract makeSound(): void; move(): void { console.log('roaming the earch...'); }}
抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract
关键字并且可以包含访问修饰符。
abstract class Department { constructor(public name: string) { } printName(): void { console.log('Department name: ' + this.name); } abstract printMeeting(): void; // 必须在派生类中实现}class AccountingDepartment extends Department { constructor() { super('Accounting and Auditing'); // constructors in derived classes must call super() } printMeeting(): void { console.log('The Accounting Department meets each Monday at 10am.'); } generateReports(): void { console.log('Generating accounting reports...'); }}let department: Department; // ok to create a reference to an abstract typedepartment = new Department(); // error: cannot create an instance of an abstract classdepartment = new AccountingDepartment(); // ok to create and assign a non-abstract subclassdepartment.printName();department.printMeeting();department.generateReports(); // error: method doesn't exist on declared abstract type
当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的 实例的类型。
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; }}let greeter: Greeter;greeter = new Greeter("world");console.log(greeter.greet());
这里,我们写了let greeter: Greeter
,意思是Greeter
类的实例的类型是Greeter
。 这对于用过其它面向对象语言的程序员来讲已经是老习惯了。
我们也创建了一个叫做构造函数的值。这个函数会在我们使用 new
创建类实例的时候被调用。下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:
let Greeter = (function () { function Greeter(message) { this.greeting = message; } Greeter.prototype.greet = function () { return "Hello, " + this.greeting; }; return Greeter;})();let greeter;greeter = new Greeter("world");console.log(greeter.greet());
上面的代码里,let Greeter
将被赋值为构造函数。我们当调用 new
并执行了这个函数后,便会得到一个类的实例。这个构造函数也包含了类的所有静态属性。换个角度说,我们可以认为类具有 实例部分与静态部分这两个部分。
让我们稍微改写一下这个例子,看看它们之前的区别:
class Greeter { static standardGreeting = "Hello, there"; greeting: string; greet() { if (this.greeting) { return "Hello, " + this.greeting; } else { return Greeter.standardGreeting; } }}let greeter1: Greeter;greeter1 = new Greeter();console.log(greeter1.greet());let greeterMaker: typeof Greeter = Greeter;greeterMaker.standardGreeting = "Hey there!";let greeter2: Greeter = new greeterMaker();console.log(greeter2.greet());
这个例子里,greeter1
与之前看到的一样。我们实例化 Greeter
类,并使用这个对象。与我们之前看到的一样。
再之后,我们直接使用类。我们创建³³了一个叫做 greeterMaker
的变量。这个变量保存了这个类或者说保存了类构造函数。然后我们使用 typeof Greeter
,意思是取Greeter类的类型,而不是实例的类型。或者更确切的说, “我告诉 Greeter
标识符的类型”,也就是构造函数的类型。这个类型包含了类的所有静态成员和构造函数。之后,就和前面一样,在我们 greeterMaker
上使用new
,创建³³ Greeter
的实例。
如上一节里所讲的,类定义会创建两个东西:类的实例类型和一个构造函数。因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
class Point { x: number; y: number;}interface Point3d extends Point { z: number;}let point3d: Point3d = {x: 1, y: 2, z: 3};
函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。
和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数。 你可以随意选择适合应用程序的方式,不论是定义一系列API函数还是只使用一次的函数。
通过下面的例子可以迅速回想起这两种JavaScript中的函数:
// Named functionfunction add(x, y) { return x + y;}// Anonymous functionlet myAdd = function(x, y) { return x + y; };
在JavaScript里,函数可以使用函数体外部的变量。 当函数这么做时,我们说它‘捕获’了这些变量。 至于为什么可以这样做以及其中的利弊超出了本文的范围,但是深刻理解这个机制对学习JavaScript和TypeScript会很有帮助。
let z = 100;function addToZ(x, y) { return x + y + z;}
让我们为上面那个函数添加类型:
function add(x: number, y: number): number { return x + y;}let myAdd = function(x: number, y: number): number { return x+y; };
我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。
现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。
let myAdd: (x:number, y:number)=>number = function(x: number, y: number): number { return x+y; };
函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。 我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。 这个名字只是为了增加可读性。 我们也可以这么写:
let myAdd: (baseValue:number, increment:number) => number = function(x: number, y: number): number { return x + y; };
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用( =>
)符号,使之清晰明了。 如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void
而不能留空。
函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。
尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:
// myAdd has the full function typelet myAdd = function(x: number, y: number): number { return x + y; };// The parameters `x` and `y` have the type numberlet myAdd: (baseValue:number, increment:number) => number = function(x, y) { return x + y; };
这叫做“按上下文归类”,是类型推论的一种。 它帮助我们更好地为程序指定类型。
TypeScript里的每个函数参数都是必须的。 这不是指不能传递 null
或undefined
作为参数,而是说编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
function buildName(firstName: string, lastName: string) { return firstName + " " + lastName;}let result1 = buildName("Bob"); // error, too few parameterslet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result3 = buildName("Bob", "Adams"); // ah, just right
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ?
实现可选参数的功能。 比如,我们想让last name是可选的:
function buildName(firstName: string, lastName?: string) { if (lastName) return firstName + " " + lastName; else return firstName;}let result1 = buildName("Bob"); // works correctly nowlet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result3 = buildName("Bob", "Adams"); // ah, just right
可选参数必须跟在必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面。
在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined
时。 它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"Smith"
。
function buildName(firstName: string, lastName = "Smith") { return firstName + " " + lastName;}let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result4 = buildName("Bob", "Adams"); // ah, just right
在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型。
function buildName(firstName: string, lastName?: string) { // ...}
和
function buildName(firstName: string, lastName = "Smith") { // ...}
共享同样的类型(firstName: string, lastName?: string) => string
。 默认参数的默认值消失了,只保留了它是一个可选参数的信息。
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined
值来获得默认值。 例如,我们重写最后一个例子,让 firstName
是带默认值的参数:
function buildName(firstName = "Will", lastName: string) { return firstName + " " + lastName;}let result1 = buildName("Bob"); // error, too few parameterslet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments
来访问所有传入的参数。
在TypeScript里,你可以把所有参数收集到一个变量里:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" ");}let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...
)后面给定的名字,你可以在函数体内使用这个数组。
这个省略号也会在带有剩余参数的函数类型定义上使用到:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" ");}let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
学习使用JavaScript里this
就好比一场成年礼。 由于TypeScript是JavaScript的超集,TypeScript程序员也需要弄清this
工作机制并且当有bug的时候能够找出错误所在。 幸运的是,TypeScript能通知你错误地使用了 this
的地方。 如果你想了解JavaScript里的 this
是如何工作的,那么首先阅读Yehuda Katz写的Understanding JavaScript Function Invocation and "this"。 Yehuda的文章详细的阐述了 this
的内部工作原理,因此我们这里只做简单介绍。
this
和箭头函数JavaScript里,this
的值在函数被调用的时候才会指定。 这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。 但众所周知,这不是一件很简单的事,尤其是在返回一个函数或将函数当做参数传递的时候。
下面看一个例子:
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }}let cardPicker = deck.createCardPicker();let pickedCard = cardPicker();alert("card: " + pickedCard.card + " of " + pickedCard.suit);
可以看到createCardPicker
是个函数,并且它又返回了一个函数。 如果我们尝试运行这个程序,会发现它并没有弹出对话框而是报错了。 因为 createCardPicker
返回的函数里的this
被设置成了window
而不是deck
对象。 因为我们只是独立的调用了 cardPicker()
。 顶级的非方法式调用会将 this
视为window
。 (注意:在严格模式下, this
为undefined
而不是window
)。
为了解决这个问题,我们可以在函数被返回时就绑好正确的this
。 这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的 this
值,而不是调用时的值:
我们把函数表达式变为使用lambda表达式( () => {} )。 这样就会在函数创建的时候就指定了‘this’值,而不是在函数调用的时候。
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { // NOTE: the line below is now an arrow function, allowing us to capture 'this' right here return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }}let cardPicker = deck.createCardPicker();let pickedCard = cardPicker();alert("card: " + pickedCard.card + " of " + pickedCard.suit);
更好事情是,TypeScript会警告你犯了一个错误,如果你给编译器设置了--noImplicitThis
标记。 它会指出this.suits[pickedSuit]
里的this
的类型为any
。
this
参数不幸的是,this.suits[pickedSuit]
的类型依旧为any
。 这是因为 this
来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的 this
参数。 this
参数是个假的参数,它出现在参数列表的最前面:
function f(this: void) { // make sure `this` is unusable in this standalone function}
让我们往例子里添加一些接口,Card
和 Deck
,让类型重用能够变得清晰简单些:
interface Card { suit: string; card: number;}interface Deck { suits: string[]; cards: number[]; createCardPicker(this: Deck): () => Card;}let deck: Deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), // NOTE: The function now explicitly specifies that its callee must be of type Deck createCardPicker: function(this: Deck) { return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }}let cardPicker = deck.createCardPicker();let pickedCard = cardPicker();alert("card: " + pickedCard.card + " of " + pickedCard.suit);
现在TypeScript知道createCardPicker
期望在某个Deck
对象上调用。 也就是说 this
是Deck
类型的,而非any
,因此--noImplicitThis
不会报错了。
this
参数在回调函数里你可以也看到过在回调函数里的this
报错,当你将一个函数传递到某个库函数里稍后会被调用时。 因为当回调被调用的时候,它们会被当成一个普通函数调用, this
将为undefined
。 稍做改动,你就可以通过 this
参数来避免错误。 首先,库函数的作者要指定 this
的类型:
interface UIElement { addClickListener(onclick: (this: void, e: Event) => void): void;}
this: void
表示 addClickListener
期望 onclick
是一个不需要 this
类型的函数。 其次,用 this
注释您的调用代码:
class Handler { info: string; onClickBad(this: Handler, e: Event) { // oops, used this here. using this callback would crash at runtime this.info = e.message; };}let h = new Handler();uiElement.addClickListener(h.onClickBad); // error!
指定了this
类型后,你显式声明onClickBad
必须在Handler
的实例上调用。 然后TypeScript会检测到addClickListener
要求函数带有this: void
。 改变 this
类型来修复这个错误:
class Handler { info: string; onClickGood(this: void, e: Event) { // can't use this here because it's of type void! console.log('clicked!'); }}let h = new Handler();uiElement.addClickListener(h.onClickGood);
因为onClickGood
指定了this
类型为void
,因此传递addClickListener
是合法的。 当然了,这也意味着不能使用 this.info
. 如果你两者都想要,你不得不使用箭头函数了:
class Handler { info: string; onClickGood = (e: Event) => { this.info = e.message }}
这是可行的因为箭头函数不会捕获this
,所以你总是可以把它们传给期望this: void
的函数。 缺点是每个Handler
对象都会创建一个箭头函数。 另一方面,方法只会被创建一次,添加到 Handler
的原型链上。 它们在不同 Handler
对象间是共享的。
JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。
let suits = ["hearts", "spades", "clubs", "diamonds"];function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card else if (typeof x == "number") { let pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; }}let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];let pickedCard1 = myDeck[pickCard(myDeck)];alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);let pickedCard2 = pickCard(15);alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
pickCard
方法根据传入参数的不同会返回两种不同的类型。 如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。 如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。
方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载 pickCard
函数。
let suits = ["hearts", "spades", "clubs", "diamonds"];function pickCard(x: {suit: string; card: number; }[]): number;function pickCard(x: number): {suit: string; card: number; };function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card else if (typeof x == "number") { let pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; }}let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];let pickedCard1 = myDeck[pickCard(myDeck)];alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);let pickedCard2 = pickCard(15);alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
这样改变后,重载的pickCard
函数在调用的时候会进行正确的类型检查。
为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
注意,function pickCard(x): any
并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard
会产生错误。
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像C#和Java这样的语言中,可以使用泛型
来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是echo
命令。
不用泛型的话,这个函数可能是下面这样:
function identity(arg: number): number { return arg;}
或者,我们使用any
类型来定义函数:
function identity(arg: any): any { return arg;}
虽然使用any
类型后这个函数已经能接收任何类型的arg参数,但是却丢失了一些信息:传入的类型与返回的类型应该是相同的。 如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function identity<T>(arg: T): T { return arg;}
我们给identity添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 之后我们再次使用了 T
当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
我们把这个版本的identity
函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any
,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。
我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string'
这里我们明确的指定了T
是string
类型,并做为一个参数传给函数,使用了<>
括起来而不是()
。
第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output = identity("myString"); // type of output will be 'string'
注意我们没必要使用尖括号(<>
)来明确地传入类型;编译器可以查看myString
的值,然后把T
设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
使用泛型创建像identity
这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。
看下之前identity
例子:
function identity<T>(arg: T): T { return arg;}
如果我们想同时打印出arg
的长度。 我们很可能会这样做:
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn't have .length return arg;}
如果这么做,编译器会报错说我们使用了arg
的.length
属性,但是没有地方指明arg
具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length
属性的。
现在假设我们想操作T
类型的数组而不直接是T
。由于我们操作的是数组,所以.length
属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:
function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); // Array has a .length, so no more error return arg;}
你可以这样理解loggingIdentity
的类型:泛型函数loggingIdentity
,接收类型参数T
,和函数arg
,它是个元素类型是T
的数组,并返回元素类型是T
的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时T
的的类型为number
。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。
我们也可以这样实现上面的例子:
function loggingIdentity<T>(arg: Array<T>): Array<T> { console.log(arg.length); // Array has a .length, so no more error return arg;}
使用过其它语言的话,你可能对这种语法已经很熟悉了。 在下一节,会介绍如何创建自定义泛型像 Array<T>
一样。
我们来到了泛型接口;让我们创建一个泛型接口来与 identities()
一起使用:
interface Identities<V, W> { id1: V, id2: W}
我在这里使用 V 和 W 作为类型变量来表示任何字母(或有效的字母和数字名称的组合)都是可以的——除了用于常规目的之外,它们的名称没有任何意义。
现在,我们可以将这个接口应用为 identities()
的返回类型,并稍稍修改返回类型去迎合它。我们还可以 console.log
这些参数和它们的类型,以便进一步说明:
function identities<T, U> (arg1: T, arg2: U): Identities<T, U> { console.log(arg1 + ": " + typeof (arg1)); console.log(arg2 + ": " + typeof (arg2)); let identities: Identities<T, U> = { id1: arg1, id2: arg2 }; return identities;}
我们现在 identities()
所做的是将类型 T 和 U 传递到函数和 Identities
接口中,使我们能够定义与参数类型相关的返回类型。
注意:如果编译 TS 项目并查找泛型,则不会找到任何泛型。由于在 Javascript 中不支持泛型,所以在编译器生成的构建中不会看到泛型。泛型纯粹是用于编译时的开发安全网,它将确保代码的类型安全抽象。
泛型类看上去与泛型接口差不多。 泛型类使用( <>
)括起泛型类型,跟在类名后面。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T;}let myGenericNumber = new GenericNumber<number>();myGenericNumber.zeroValue = 0;myGenericNumber.add = function(x, y) { return x + y; };
GenericNumber
类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number
类型。 也可以使用字符串或其它更复杂的类型。
let stringNumeric = new GenericNumber<string>();stringNumeric.zeroValue = "";stringNumeric.add = function(x, y) { return x + y; };alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。
我们在类那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在loggingIdentity
例子中,我们想访问arg
的length
属性,但是编译器并不能证明每种类型都有length
属性,所以就报错了。
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn't have .length return arg;}
相比于操作any
所有类型,我们想要限制函数去处理任意带有.length
属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。
为此,我们定义一个接口来描述约束条件。 创建一个包含 .length
属性的接口,使用这个接口和extends
关键字还实现约束:
interface Lengthwise { length: number;}function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // Now we know it has a .length property, so no more error return arg;}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
我们需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity({length: 10, value: 3});
你可以声明一个类型参数,且它被另一个类型参数所约束。比如,
function find<T, U extends Findable<T>>(n: T, s: U) { // ...}find (giraffe, myAnimals);
在 TypeScript 使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,
function create<T>(c: {new(): T; }): T { return new c();}
一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。
class BeeKeeper { hasMask: boolean;}class ZooKeeper { nametag: string;}class Animal { numLegs: number;}class Bee extends Animal { keeper: BeeKeeper;}class Lion extends Animal { keeper: ZooKeeper;}function findKeeper<A extends Animal, K> (a: {new(): A; prototype: {keeper: K}}): K { return a.prototype.keeper;}findKeeper(Lion).nametag; // typechecks!
使用枚举我们可以定义一些有名字的数字常量。 枚举通过 enum
关键字来定义。
enum Direction { Up = 1, Down, Left, Right}
一个枚举类型可以包含零个或多个枚举成员。 枚举成员具有一个数字值,它可以是 常数或是计算得出的值 当满足如下条件时,枚举成员被当作是常数:
0
。+
, -
, ~
一元运算符应用于常数枚举表达式+
, -
, *
, /
, %
, <<
, >>
, >>>
, &
, |
, ^
二元运算符,常数枚举表达式做为其一个操作对象 若常数枚举表达式求值后为 NaN
或Infinity
,则会在编译阶段报错。所有其它情况的枚举成员被当作是需要计算得出的值。
enum FileAccess { // constant members None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write // computed member G = "123".length}
枚举是在运行时真正存在的一个对象。 其中一个原因是因为这样可以从枚举值到枚举名进行反向映射。
enum Enum { A}let a = Enum.A;let nameOfA = Enum[Enum.A]; // "A"
编译成:
var Enum;(function (Enum) { Enum[Enum["A"] = 0] = "A";})(Enum || (Enum = {}));var a = Enum.A;var nameOfA = Enum[Enum.A]; // "A"
生成的代码中,枚举类型被编译成一个对象,它包含双向映射(name
-> value
)和(value
-> name
)。 引用枚举成员总会生成一次属性访问并且永远不会内联。 在大多数情况下这是很好的并且正确的解决方案。 然而有时候需求却比较严格。 当访问枚举值时,为了避免生成多余的代码和间接引用,可以使用常数枚举。 常数枚举是在enum
关键字前使用const
修饰符。
const enum Enum { A = 1, B = A * 2}
常数枚举只能使用常数枚举表达式并且不同于常规的枚举的是它们在编译阶段会被删除。 常数枚举成员在使用的地方被内联进来。 这是因为常数枚举不可能有计算成员。
const enum Directions { Up, Down, Left, Right}let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
生成后的代码为:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
外部枚举用来描述已经存在的枚举类型的形状。
declare enum Enum { A = 1, B, C = 2}
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
这节介绍TypeScript里的类型推论。即,类型是在哪里如何被推断的。
TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子
let x = 3;
变量x
的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。
大多数情况下,类型推论是直截了当地。 后面的小节,我们会浏览类型推论时的细微差别。
当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如,
let x = [0, 1, null];
为了推断x
的类型,我们必须考虑所有元素的类型。 这里有两种选择: number
和null
。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如:
let zoo = [new Rhino(), new Elephant(), new Snake()];
这里,我们想让zoo被推断为Animal[]
类型,但是这个数组里没有对象是Animal
类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
如果没有找到最佳通用类型的话,类型推论的结果是空对象类型,{}
。 因为这个类型没有任何成员,所以访问其成员的时候会报错。
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:
window.onmousedown = function(mouseEvent) { console.log(mouseEvent.buton); //<- Error};
这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown
函数的类型来推断右边函数表达式的类型。 因此,就能推断出 mouseEvent
参数的类型了。 如果函数表达式不是在上下文类型的位置,mouseEvent
参数的类型需要指定为any
,这样也不会报错了。
如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。 重写上面的例子:
window.onmousedown = function(mouseEvent: any) { console.log(mouseEvent.buton); //<- Now, no error is given};
这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了,因为这里不会使用到上下文类型。
上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:
function createZoo(): Animal[] { return [new Rhino(), new Elephant(), new Snake()];}
这个例子里,最佳通用类型有4个候选者:Animal
,Rhino
,Elephant
和Snake
。 当然, Animal
会被做为最佳通用类型。
TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。) 看下面的例子:
interface Named { name: string;}class Person { name: string;}let p: Named;// OK, because of structural typingp = new Person();
在使用基于名义类型的语言,比如C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Named接口。
TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。
TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。
TypeScript结构化类型系统的基本规则是,如果x
要兼容y
,那么y
至少具有与x
相同的属性。比如:
interface Named { name: string;}let x: Named;// y's inferred type is { name: string; location: string; }let y = { name: 'Alice', location: 'Seattle' };x = y;
这里要检查y
是否能赋值给x
,编译器检查x
中的每个属性,看是否能在y
中也找到对应属性。 在这个例子中,y
必须包含名字是name
的string
类型成员。y
满足条件,因此赋值正确。
检查函数参数时使用相同的规则:
function greet(n: Named) { alert('Hello, ' + n.name);}greet(y); // OK
注意,y
有个额外的location
属性,但这不会引发错误。 只有目标类型(这里是 Named
)的成员会被一一检查是否兼容。
这个比较过程是递归进行的,检查每个成员及子成员。
相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。 下面我们从两个简单的函数入手,它们仅是参数列表略有不同:
let x = (a: number) => 0;let y = (b: number, s: string) => 0;y = x; // OKx = y; // Error
要查看x
是否能赋值给y
,首先看它们的参数列表。 x
的每个参数必须能在y
里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里, x
的每个参数在y
中都能找到对应的参数,所以允许赋值。
第二个赋值错误,因为y
有个必需的第二个参数,但是x
并没有,所以不允许赋值。
你可能会疑惑为什么允许忽略
参数,像例子y = x
中那样。 原因是忽略额外的参数在JavaScript里是很常见的。 例如, Array#forEach
给回调函数传3个参数:数组元素,索引和整个数组。 尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:
let items = [1, 2, 3];// Don't force these extra argumentsitems.forEach((item, index, array) => console.log(item));// Should be OK!items.forEach((item) => console.log(item));
下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数:
let x = () => ({name: 'Alice'});let y = () => ({name: 'Alice', location: 'Seattle'});x = y; // OKy = x; // Error because x() lacks a location property
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。例如:
enum EventType { Mouse, Keyboard }interface Event { timestamp: number; }interface MouseEvent extends Event { x: number; y: number }interface KeyEvent extends Event { keyCode: number }function listenEvent(eventType: EventType, handler: (n: Event) => void) { /* ... */}// Unsound, but useful and commonlistenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));// Undesirable alternatives in presence of soundnesslistenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));// Still disallowed (clear error). Type safety enforced for wholly incompatible typeslistenEvent(EventType.Mouse, (e: number) => console.log(e));
比较函数兼容性的时候,可选参数与必须参数是可交换的。 原类型上额外的可选参数并不会造成错误,目标类型的可选参数没有对应的参数也不是错误。
当一个函数有剩余参数时,它被当做无限个可选参数。
这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded
。
有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:
function invokeLater(args: any[], callback: (...args: any[]) => void) { /* ... Invoke callback with 'args' ... */}// Unsound - invokeLater "might" provide any number of argumentsinvokeLater([1, 2], (x, y) => console.log(x + ', ' + y));// Confusing (x and y are actually required) and undiscoverableinvokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,
enum Status { Ready, Waiting };enum Color { Red, Blue, Green };let status = Status.Ready;status = Color.Green; //error
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
class Animal { feet: number; constructor(name: string, numFeet: number) { }}class Size { feet: number; constructor(numFeet: number) { }}let a: Animal;let s: Size;a = s; //OKs = a; //OK
私有成员会影响兼容性判断。 当类的实例用来检查兼容时,如果它包含一个私有成员,那么目标类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,
interface Empty<T> {}let x: Empty<number>;let y: Empty<string>;x = y; // okay, y matches structure of x
上面代码里,x
和y
是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:
interface NotEmpty<T> { data: T;}let x: NotEmpty<number>;let y: NotEmpty<string>;x = y; // error, x and y are not compatible
在这里,泛型类型在使用时就好比不是一个泛型类型。
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any
比较。 然后用结果类型进行比较,就像上面第一个例子。
比如,
let identity = function<T>(x: T): T { // ...}let reverse = function<U>(y: U): U { // ...}identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any
目前为止,我们使用了兼容性
,它在语言规范里没有定义。 在TypeScript里,有两种类型的兼容性:子类型与赋值。 它们的不同点在于,赋值扩展了子类型兼容,允许给 any
赋值或从any
取值和允许数字赋值给枚举类型或枚举类型赋值给数字。
语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的甚至在implements
和extends
语句里。 更多信息,请参阅 TypeScript语言规范.
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable
同时是Person
和Serializable
和Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子:
function extend<T, U>(first: T, second: U): T & U { let result = <T & U>{}; for (let id in first) { (<any>result)[id] = (<any>first)[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (<any>result)[id] = (<any>second)[id]; } } return result;}class Person { constructor(public name: string) { }}interface Loggable { log(): void;}class ConsoleLogger implements Loggable { log() { // ... }}var jim = extend(new Person("Jim"), new ConsoleLogger());var n = jim.name;jim.log();
联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入 number
或string
类型的参数。 例如下面的函数:
/** * Takes a string and adds "padding" to the left. * If 'padding' is a string, then 'padding' is appended to the left side. * If 'padding' is a number, then that number of spaces is added to the left side. */function padLeft(value: string, padding: any) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value; } if (typeof padding === "string") { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`);}padLeft("Hello world", 4); // returns " Hello world"
padLeft
存在一个问题,padding
参数的类型指定成了any
。 这就是说我们可以传入一个既不是 number
也不是string
类型的参数,但是TypeScript却不报错。
let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错
在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft
原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。
代替any
, 我们可以使用联合类型做为padding
的参数:
/** * Takes a string and adds "padding" to the left. * If 'padding' is a string, then 'padding' is appended to the left side. * If 'padding' is a number, then that number of spaces is added to the left side. */function padLeft(value: string, padding: string | number) { // ...}let indentedString = padLeft("Hello world", true); // errors during compilation
联合类型表示一个值可以是几种类型之一。 我们用竖线( |
)分隔每个类型,所以number | string | boolean
表示一个值可以是number
,string
,或boolean
。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird { fly(); layEggs();}interface Fish { swim(); layEggs();}function getSmallPet(): Fish | Bird { // ...}let pet = getSmallPet();pet.layEggs(); // okaypet.swim(); // errors
这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是 A | B
,我们能够确定的是它包含了A
和B
中共有的成员。 这个例子里, Bird
具有一个fly
成员。 我们不能确定一个 Bird | Fish
类型的变量是否有fly
方法。 如果变量在运行时是 Fish
类型,那么调用pet.fly()
就出错了。
联合类型非常适合这样的情形,可接收的值有不同的类型。 当我们想明确地知道是否拿到 Fish
时会怎么做? JavaScript里常用来区分2个可能值的方法是检查它们是否存在。 像之前提到的,我们只能访问联合类型的所有类型中共有的成员。
let pet = getSmallPet();// 每一个成员访问都会报错if (pet.swim) { pet.swim();}else if (pet.fly) { pet.fly();}
为了让这段代码工作,我们要使用类型断言:
let pet = getSmallPet();if ((<Fish>pet).swim) { (<Fish>pet).swim();}else { (<Bird>pet).fly();}
可以注意到我们使用了多次类型断言。 如果我们只要检查过一次类型,就能够在后面的每个分支里清楚 pet
的类型的话就好了。
TypeScript里的类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型断言:
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined;}
在这个例子里,pet is Fish
就是类型谓词。 谓词是 parameterName is Type
这种形式,parameterName
必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用isFish
时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
// 'swim' 和 'fly' 调用都没有问题了if (isFish(pet)) { pet.swim();}else { pet.fly();}
注意TypeScript不仅知道在if
分支里pet
是Fish
类型; 它还清楚在 else
分支里,一定不是Fish
类型,一定是Bird
类型。
typeof
类型保护现在我们回过头来看看怎么使用联合类型书写padLeft
代码。 我们可以像下面这样利用类型断言来写:
function isNumber(x: any): x is number { return typeof x === "number";}function isString(x: any): x is string { return typeof x === "string";}function padLeft(value: string, padding: string | number) { if (isNumber(padding)) { return Array(padding + 1).join(" ") + value; } if (isString(padding)) { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`);}
然而,必须要定义一个函数来判断类型是否是原始类型,这太痛苦了。 幸运的是,现在我们不必将 typeof x === "number"
抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。
function padLeft(value: string, padding: string | number) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value; } if (typeof padding === "string") { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`);}
这些*typeof
类型保护*只有两种形式能被识别:typeof v === "typename"
和typeof v !== "typename"
,"typename"
必须是"number"
,"string"
,"boolean"
或"symbol"
。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
instanceof
类型保护如果你已经阅读了typeof
类型保护并且对JavaScript里的instanceof
操作符熟悉的话,你可能已经猜到了这节要讲的内容。
instanceof
类型保护是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:
interface Padder { getPaddingString(): string}class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) { } getPaddingString() { return Array(this.numSpaces + 1).join(" "); }}class StringPadder implements Padder { constructor(private value: string) { } getPaddingString() { return this.value; }}function getRandomPadder() { return Math.random() < 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder(" ");}// 类型为SpaceRepeatingPadder | StringPadderlet padder: Padder = getRandomPadder();if (padder instanceof SpaceRepeatingPadder) { padder; // 类型细化为'SpaceRepeatingPadder'}if (padder instanceof StringPadder) { padder; // 类型细化为'StringPadder'}
instanceof
的右侧要求是一个构造函数,TypeScript将细化为:
prototype
属性的类型,如果它的类型不为any
的话以此顺序。
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string;type NameResolver = () => string;type NameOrResolver = Name | NameResolver;function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); }}
起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Container<T> = { value: T };
我们也可以使用类型别名来在属性里引用自己:
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>;}
然而,类型别名不能够出现在声名语句的右侧:
type LinkedList<T> = T & { next: LinkedList<T> };interface Person { name: string;}var people: LinkedList<Person>;var s = people.name;var s = people.next.name;var s = people.next.next.name;var s = people.next.next.next.name;
然而,类型别名不能出现在声明右侧的任何地方。
type Yikes = Array<Yikes>; // error
像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced
上,显示它返回的是Interface
,但悬停在aliased
上时,显示的却是对象字面量类型。
type Alias = { num: number }interface Interface { num: number;}declare function aliased(arg: Alias): Alias;declare function interfaced(arg: Interface): Interface;
另一个重要区别是类型别名不能被extends
和implements
(自己也不能extends
和implements
其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
type Easing = "ease-in" | "ease-out" | "ease-in-out";class UIElement { animate(dx: number, dy: number, easing: Easing) { if (easing === "ease-in") { // ... } else if (easing === "ease-out") { } else if (easing === "ease-in-out") { } else { // error! should not pass null or undefined. } }}let button = new UIElement();button.animate(0, 0, "ease-in");button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
字符串字面量类型还可以用于区分函数重载:
function createElement(tagName: "img"): HTMLImageElement;function createElement(tagName: "input"): HTMLInputElement;// ... more overloads ...function createElement(tagName: string): Element { // ... code goes here ...}
你可以合并字符串字面量类型,联合类型,类型保护和类型别名来创建一个叫做可辨识联合的高级模式,它也称做标签联合或代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有4个要素:
interface Square { kind: "square"; size: number;}interface Rectangle { kind: "rectangle"; width: number; height: number;}interface Circle { kind: "circle"; radius: number;}
首先我们声明了将要联合的接口。 每个接口都有 kind
属性但有不同的字符器字面量类型。 kind
属性称做可辨识的特征或标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:
type Shape = Square | Rectangle | Circle;
现在我们使用可辨识联合:
function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; }}
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle
到Shape
,我们同时还需要更新area
:
type Shape = Square | Rectangle | Circle | Triangle;function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } // should error here - we didn't handle case "triangle"}
有两种方式可以实现。 首先是启用 --strictNullChecks
并且指定一个返回值类型:
function area(s: Shape): number { // error: returns number | undefined switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; }}
因为switch
没有包涵所有情况,所以TypeScript认为这个函数有时候会返回undefined
。 如果你明确地指定了返回值类型为 number
,那么你会看到一个错误,因为实际上返回值的类型为number | undefined
。 然而,这种方法存在些微妙之处且 --strictNullChecks
对旧代码支持不好。
第二种方法使用never
类型,编译器用它来进行完整性检查:
function assertNever(x: never): never { throw new Error("Unexpected object: " + x);}function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; default: return assertNever(s); // error here if there are missing cases }}
这里,assertNever
检查s
是否为never
类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s
将具有一个赶写的类型,因此你会得到一个错误。 这种方式需要你定义一个额外的函数。
this
类型多态的this
类型表示的是某个包含类或接口的子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回 this
类型:
class BasicCalculator { public constructor(protected value: number = 0) { } public currentValue(): number { return this.value; } public add(operand: number): this { this.value += operand; return this; } public multiply(operand: number): this { this.value *= operand; return this; } // ... other operations go here ...}let v = new BasicCalculator(2) .multiply(5) .add(1) .currentValue();
由于这个类使用了this
类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。
class ScientificCalculator extends BasicCalculator { public constructor(value = 0) { super(value); } public sin() { this.value = Math.sin(this.value); return this; } // ... other operations go here ...}let v = new ScientificCalculator(2) .multiply(5) .sin() .add(1) .currentValue();
如果没有this
类型,ScientificCalculator
就不能够在继承BasicCalculator
的同时还保持接口的连贯性。 multiply
将会返回BasicCalculator
,它并没有sin
方法。 然而,使用 this
类型,multiply
会返回this
,在这里就是ScientificCalculator
。
自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的列表:
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,迭代器只允许在Array
类型上使用。 在非数组值上使用 for..of
语句会得到一个错误,就算这些非数组值已经实现了Symbol.iterator
属性。
编译器会生成一个简单的for
循环做为for..of
循环,比如:
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 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
的文件都被当成一个模块。
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export
关键字来导出。
export interface StringValidator { isAcceptable(s: string): boolean;}
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 };
我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
export class ParseIntBasedZipCodeValidator { isAcceptable(s: string) { return s.length === 5 && parseInt(s).toString() === s; }}// 导出原先的验证器但做了重命名export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"
。
export * from "./StringValidator"; // exports interface StringValidatorexport * from "./LettersOnlyValidator"; // exports class LettersOnlyValidatorexport * 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。
declare let $: JQuery;export default $;
import $ from "JQuery";$("button.continue").html( "Next Step..." );
类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。
export default class ZipCodeValidator { static numberRegexp = /^[0-9]+$/; isAcceptable(s: string) { return s.length === 5 && ZipCodeValidator.numberRegexp.test(s); }}
import validator from "./ZipCodeValidator";let myValidator = new validator();
或者
const numberRegexp = /^[0-9]+$/;export default function (s: string) { return s.length === 5 && numberRegexp.test(s);}
import validate from "./StaticZipCodeValidator";let strings = ["Hello", "98052", "101"];// Use function validatestrings.forEach(s => { console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);});
default
导出也可以是一个值
export default "123";
import num from "./OneTwoThree";console.log(num); // "123"
export =
和 import = require()
CommonJS和AMD都有一个exports
对象的概念,它包含了一个模块的所有导出内容。
它们也支持把exports
替换为一个自定义对象。 默认导出就好比这样一个功能;然而,它们却并不相互兼容。 TypeScript模块支持 export =
语法以支持传统的CommonJS和AMD的工作流模型。
export =
语法定义一个模块的导出对象。 它可以是类,接口,命名空间,函数或枚举。
若要导入一个使用了export =
的模块时,必须使用TypeScript提供的特定语法import let = require("module")
。
let numberRegexp = /^[0-9]+$/;class ZipCodeValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); }}export = ZipCodeValidator;
import zip = require("./ZipCodeValidator");// Some samples to trylet strings = ["Hello", "98052", "101"];// Validators to uselet validator = new zip();// Show whether each string passed each validatorstrings.forEach(s => { console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);});
根据编译时指定的模块目标参数,编译器会生成相应的供Node.js (CommonJS),Require.js (AMD),isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。 想要了解生成代码中define
,require
和 register
的意义,请参考相应模块加载器的文档。
下面的例子说明了导入导出语句里使用的名字是怎么转换为相应的模块加载器代码的。
import m = require("mod");export let t = m.something + 1;
define(["require", "exports", "./mod"], function (require, exports, mod_1) { exports.t = mod_1.something + 1;});
let mod_1 = require("./mod");exports.t = mod_1.something + 1;
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { let 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) { let mod_1 = require("./mod"); exports.t = mod_1.something + 1;});
System.register(["./mod"], function(exports_1) { let mod_1; let t; return { setters:[ function (mod_1_1) { mod_1 = mod_1_1; }], execute: function() { exports_1("t", t = mod_1.something + 1); } }});
import { something } from "./mod";export let t = something + 1;
下面我们来整理一下前面的验证器实现,每个模块只有一个命名的导出。
为了编译,我们必需要在命令行上指定一个模块目标。对于Node.js来说,使用--module commonjs
; 对于Require.js来说,使用``--module amd`。比如:
tsc --module commonjs Test.ts
编译完成后,每个模块会生成一个单独的.js
文件。 好比使用了reference标签,编译器会根据 import
语句编译相应的文件。
export interface StringValidator { isAcceptable(s: string): boolean;}
import { StringValidator } from "./Validation";const lettersRegexp = /^[A-Za-z]+$/;export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); }}
import { StringValidator } from "./Validation";const numberRegexp = /^[0-9]+$/;export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); }}
import { StringValidator } from "./Validation";import { ZipCodeValidator } from "./ZipCodeValidator";import { LettersOnlyValidator } from "./LettersOnlyValidator";// Some samples to trylet strings = ["Hello", "98052", "101"];// Validators to uselet validators: { [s: string]: StringValidator; } = {};validators["ZIP code"] = new ZipCodeValidator();validators["Letters only"] = new LettersOnlyValidator();// Show whether each string passed each validatorstrings.forEach(s => { for (let name in validators) { console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`); }});
有时候,你只想在某种条件下才加载某个模块。 在TypeScript里,使用下面的方式来实现它和其它的高级加载场景,我们可以直接调用模块加载器并且可以保证类型完全。
编译器会检测是否每个模块都会在生成的JavaScript中用到。 如果一个模块标识符只在类型注解部分使用,并且完全没有在表达式中使用时,就不会生成 require
这个模块的代码。 省略掉没有用到的引用对性能提升是很有益的,并同时提供了选择性加载模块的能力。
这种模式的核心是import id = require("...")
语句可以让我们访问模块导出的类型。 模块加载器会被动态调用(通过 require
),就像下面if
代码块里那样。 它利用了省略引用的优化,所以模块只在被需要时加载。 为了让这个模块工作,一定要注意 import
定义的标识符只能在表示类型处使用(不能在会转换成JavaScript的地方)。
为了确保类型安全性,我们可以使用typeof
关键字。 typeof
关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型。
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("...")) { /* ... */ }}
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("...")) { /* ... */ } });}
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编写的类库的类型,我们需要声明类库所暴露出的API。
我们叫它声明因为它不是“外部程序”的具体实现。 它们通常是在 .d.ts
文件里定义的。 如果你熟悉C/C++,你可以把它们当做 .h
文件。 让我们看一些例子。
在Node.js里大部分工作是通过加载一个或多个模块实现的。 我们可以使用顶级的 export
声明来为每个模块都定义一个.d.ts
文件,但最好还是写在一个大的.d.ts
文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用 module
关键字并且把名字用引号括起来,方便之后import
。 例如:
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;}
现在我们可以/// <reference>
node.d.ts
并且使用import url = require("url");
加载模块。
/// <reference path="node.d.ts"/>import * as URL from "url";let myUrl = URL.parse("http://www.typescriptlang.org");
假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。
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或Isomorphic模块为代表。 这些库可以通过导入的形式或全局变量的形式访问。 例如:
export const 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);
用户应该更容易地使用你模块导出的内容。 嵌套层次过多会变得难以处理,因此仔细考虑一下如何组织你的代码。
从你的模块中导出一个命名空间就是一个增加嵌套的例子。 虽然命名空间有时候有它们的用处,在使用模块的时候它们额外地增加了一层。 这对用户来说是很不便的并且通常是多余的。
导出类的静态方法也有同样的问题 - 这个类本身就增加了一层嵌套。 除非它能方便表述或便于清晰使用,否则请考虑直接导出一个辅助方法。
class
或 function
,使用 export default
就像“在顶层上导出”帮助减少用户使用的难度,一个默认的导出也能起到这个效果。 如果一个模块就是为了导出特定的内容,那么你应该考虑使用一个默认导出。 这会令模块的导入和使用变得些许简单。 比如:
export default class SomeType { constructor() { ... }}
export default function getThing() { return 'thing'; }
import t from "./MyClass";import f from "./MyFunc";let x = new t();console.log(f());
对用户来说这是最理想的。他们可以随意命名导入模块的类型(本例为t
)并且不需要多余的(.)来找到相关对象。
export class SomeType { /* ... */ }export function someFunc() { /* ... */ }
相反地,当导入的时候:
import { SomeType, SomeFunc } from "./MyThings";let x = new SomeType();let y = someFunc();
export class Dog { ... }export class Cat { ... }export class Tree { ... }export class Flower { ... }
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 handelChar(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.handelChar(input[i]); } console.log(`result of '${input}' is '${c.getResult()}'`);}
这是使用导出的test
函数来测试计算器。
import { Calculator, test } from "./Calculator";let c = new Calculator();test(c, "1+2*33/11="); // prints 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(); if (base <= 0 || base > ProgrammerCalculator.digits.length) { throw new Error("base has to be within 0 to 16 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 Calculatorexport { ProgrammerCalculator as Calculator };// Also, export the helper functionexport { test } from "./Calculator";
新的ProgrammerCalculator
模块导出的API与原先的Calculator
模块很相似,但却没有改变原模块里的对象。 下面是测试ProgrammerCalculator类的代码:
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
-- 两个类型的名字相同,但命名空间不同。 然而,这对于模块来说却不是一个问题。 在一个模块里,没有理由两个对象拥有同一个名字。 从模块的使用角度来说,使用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的情况。
更多关于模块和命名空间的资料查看[命名空间和模块](./Namespaces and Modules.md)
以下均为模块结构上的危险信号。重新检查以确保你没有在对模块使用命名空间:
export namespace Foo { ... }
(删除Foo
并把所有内容向上层移动一层)export class
或export function
(考虑使用export default
)export namespace Foo {
(不要以为这些会合并到一个Foo
中!)关于术语的一点说明: 请务必注意一点,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 trylet strings = ["Hello", "98052", "101"];// Validators to uselet validators: { [s: string]: StringValidator; } = {};validators["ZIP code"] = new ZipCodeValidator();validators["Letters only"] = new LettersOnlyValidator();// Show whether each string passed each validatorstrings.forEach(s => { for (let name in validators) { console.log(""" + s + "" " + (validators[name].isAcceptable(s) ? " 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 trylet strings = ["Hello", "98052", "101"];// Validators to uselet validators: { [s: string]: Validation.StringValidator; } = {};validators["ZIP code"] = new Validation.ZipCodeValidator();validators["Letters only"] = new Validation.LettersOnlyValidator();// Show whether each string passed each validatorstrings.forEach(s => { for (let name in validators) { console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`); }});
当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。
现在,我们把Validation
命名空间分割成多个文件。 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。 因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。 我们的测试代码保持不变。
namespace Validation { export interface StringValidator { isAcceptable(s: string): boolean; }}
/// <reference path="Validation.ts" />namespace Validation { const lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } }}
/// <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); } }}
/// <reference path="Validation.ts" />/// <reference path="LettersOnlyValidator.ts" />/// <reference path="ZipCodeValidator.ts" />// Some samples to trylet strings = ["Hello", "98052", "101"];// Validators to uselet validators: { [s: string]: Validation.StringValidator; } = {};validators["ZIP code"] = new Validation.ZipCodeValidator();validators["Letters only"] = new Validation.LettersOnlyValidator();// Show whether each string passed each validatorstrings.forEach(s => { 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文件按正确的顺序引进来,比如:
<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
会生成与原始符号不同的引用,所以改变别名的var
值并不会影响原始变量的值。
为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API。 由于大部分程序库只提供少数的顶级对象,命名空间是用来表示它们的一个好办法。
我们称其为声明是因为它不是外部程序的具体实现。 我们通常在 .d.ts
里写这些声明。 如果你熟悉C/C++,你可以把它们当做 .h
文件。 让我们看一些例子。
流行的程序库D3在全局对象d3
里定义它的功能。 因为这个库通过一个 <script>
标签加载(不是通过模块加载器),它的声明文件使用内部模块来定义它的类型。 为了让TypeScript编译器识别它的类型,我们使用外部命名空间声明。 比如,我们可以像下面这样写:
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 let d3: D3.Base;
关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说
module X {
相当于现在推荐的写法namespace X {
)。
这篇文章将概括介绍在TypeScript里使用模块与命名空间来组织代码的方法。 我们也会谈及命名空间和模块的高级使用场景,和在使用它们的过程中常见的陷阱。
查看模块章节了解关于模块的更多信息。 查看 命名空间章节了解关于命名空间的更多信息。
命名空间是位于全局命名空间下的一个普通的带有名字的JavaScript对象。 这令命名空间十分容易使用。 它们可以在多文件中同时使用,并通过 --outFile
结合在一起。 命名空间是帮你组织Web应用不错的方式,你可以把所有依赖都放在HTML页面的 <script>
标签里。
但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中。
像命名空间一样,模块可以包含代码和声明。 不同的是模块可以 声明它的依赖。
模块会把依赖添加到模块加载器上(例如CommonJs / Require.js)。 对于小型的JS应用来说可能没必要,但是对于大型应用,这一点点的花费会带来长久的模块化和可维护性上的便利。 模块也提供了更好的代码重用,更强的封闭性以及更好的使用工具进行优化。
对于Node.js应用来说,模块是默认并推荐的组织代码的方式。
从ECMAScript 2015开始,模块成为了语言内置的部分,应该会被所有正常的解释引擎所支持。 因此,对于新项目来说推荐使用模块做为组织代码的方式。
这部分我们会描述常见的命名空间和模块的使用陷阱和如何去避免它们。
/// <reference>
一个常见的错误是使用/// <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例子中引用 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以上的版本能够使用outFile
当目标为amd
或system
。
这节假设你已经了解了模块的一些基本知识 请阅读 模块文档了解更多信息。
模块解析就是指编译器所要依据的一个流程,用它来找出某个导入操作所引用的具体值。 假设有一个导入语句import { a } from "moduleA"
; 为了去检查任何对 a
的使用,编译器需要准确的知道它表示什么,并且会需要检查它的定义moduleA
。
这时候,编译器会想知道“moduleA
的shape是怎样的?” 这听上去很简单, moduleA
可能在你写的某个.ts
/.tsx
文件里或者在你的代码所依赖的.d.ts
里。
首先,编译器会尝试定位表示导入模块的文件。 编译会遵循下列二种策略之一: Classic或Node。 这些策略会告诉编译器到 哪里去查找moduleA
。
如果它们失败了并且如果模块名是非相对的(且是在"moduleA"
的情况下),编译器会尝试定位一个外部模块声明。 我们接下来会讲到非相对导入。
最后,如果编译器还是不能解析这个模块,它会记录一个错误。 在这种情况下,错误可能为 error TS2307: Cannot find module 'moduleA'.
根据模块引用是相对的还是非相对的,模块导入会以不同的方式解析。
相对导入是以/
,./
或../
开头的。 下面是一些例子:
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
所有其它形式的导入被当作非相对的。 下面是一些例子:
import * as $ from "jQuery";
import { Component } from "angular2/core";
相对导入解析时是相对于导入它的文件来的,并且不能解析为一个外部模块声明。 你应该为你自己写的模块使用相对导入,这样能确保它们在运行时的相对位置。
共有两种可用的模块解析策略:Node和Classic。 你可以使用 --moduleResolution
标记为指定使用哪个。 默认值为 Node。
这种策略以前是TypeScript默认的解析策略。 现在,它存在的理由主要是为了向后兼容。
相对导入的模块是相对于导入它的文件进行解析的。 因此 /root/src/folder/A.ts
文件里的import { b } from "./moduleB"
会使用下面的查找流程:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
对于非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。
比如:
有一个对moduleB
的非相对导入import { b } from "moduleB"
,它是在/root/src/folder/A.ts
文件里,会以如下的方式来定位"moduleB"
:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
这个解析策略试图在运行时模仿Node.js模块解析机制。 完整的Node.js解析算法可以在 Node.js module documentation找到。
为了理解TypeScript编译依照的解析步骤,先弄明白Node.js模块是非常重要的。 通常,在Node.js里导入是通过require
函数调用进行的。 Node.js会根据 require
的是相对路径还是非相对路径做出不同的行为。
相对路径很简单。 例如,假设有一个文件路径为 /root/src/moduleA.js
,包含了一个导入var x = require("./moduleB");
Node.js以下面的顺序解析这个导入:
将/root/src/moduleB.js
视为文件,检查是否存在。
将/root/src/moduleB
视为目录,检查是否它包含package.json
文件并且其指定了一个"main"
模块。 在我们的例子里,如果Node.js发现文件 /root/src/moduleB/package.json
包含了{ "main": "lib/mainModule.js" }
,那么Node.js会引用/root/src/moduleB/lib/mainModule.js
。
将/root/src/moduleB
视为目录,检查它是否包含index.js
文件。 这个文件会被隐式地当作那个文件夹下的"main"模块。
你可以阅读Node.js文档了解更多详细信息:file modules 和 folder modules。
但是,非相对模块名的解析是个完全不同的过程。 Node会在一个特殊的文件夹 node_modules
里查找你的模块。node_modules
可能与当前文件在同一级目录下,或者在上层目录里。 Node会向上级目录遍历,查找每个node_modules
直到它找到要加载的模块。
还是用上面例子,但假设/root/src/moduleA.js
里使用的是非相对路径导入var x = require("moduleB");
。 Node则会以下面的顺序去解析 moduleB
,直到有一个匹配上。
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json
(如果指定了"main"
属性)/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json
(如果指定了"main"
属性)/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json
(如果指定了"main"
属性)/node_modules/moduleB/index.js
注意Node.js在步骤(4)和(7)会向上跳一级目录。
你可以阅读Node.js文档了解更多详细信息:loading modules from node_modules
。
TypeScript是模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件。 因此,TypeScript在Node解析逻辑基础上增加了TypeScript源文件的扩展名( .ts
,.tsx
和.d.ts
)。 同时,TypeScript在 package.json
里使用字段"typings"
来表示类似"main"
的意义 - 编译器会使用它来找到要使用的"main"定义文件。
比如,有一个导入语句import { b } from "./moduleB"
在/root/src/moduleA.ts
里,会以下面的流程来定位"./moduleB"
:
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json
(如果指定了"typings"
属性)/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
回想一下Node.js先查找moduleB.js
文件,然后是合适的package.json
,再之后是index.js
。
类似地,非相对的导入会遵循Node.js的解析逻辑,首先查找文件,然后是合适的文件夹。 因此/src/moduleA.ts
文件里的import { b } from "moduleB"
会以下面的查找顺序解析:
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json
(如果指定了"typings"
属性)/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json
(如果指定了"typings"
属性)/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json
(如果指定了"typings"
属性)/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
不要被这里步骤的数量吓到 - TypeScript只是在步骤(8)和(15)向上跳了两次目录。 这并不比Node.js里的流程复杂。
--noResolve
正常来讲编译器会在开始编译之前解析模块导入。 每当它成功地解析了对一个文件 import
,这个文件被会加到一个文件列表里,以供编译器稍后处理。
--noResolve
编译选项告诉编译器不要添加任何不是在命令行上传入的文件到编译列表。 编译器仍然会尝试解析模块,但是只要没有指定这个文件,那么它就不会被包含在内。
比如
import * as A from "moduleA" // OK, moduleA passed on the command-lineimport * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
tsc app.ts moduleA.ts --noResolve
使用--noResolve
编译app.ts
:
moduleA
,因为它在命令行上指定了。moduleB
,因为没有在命令行上传递。exclude
列表里的模块还会被编译器使用tsconfig.json
将文件夹转变一个“工程” 如果不指定任何 “exclude”
或“files”
,文件夹里的所有文件包括tsconfig.json
和所有的子目录都会在编译列表里。 如果你想利用 “exclude”
排除某些文件,甚至你想指定所有要编译的文件列表,请使用“files”
。
有些是被tsconfig.json
自动加入的。 它不会涉及到上面讨论的模块解析。 如果编译器识别出一个文件是模块导入目标,它就会加到编译列表里,不管它是否被排除了。
因此,要从编译列表中排除一个文件,你需要在排除它的同时,还要排除所有对它进行import
或使用了/// <reference path="..." />
指令的文件。
介绍
TypeScript中有些独特的概念可以在类型层面上描述JavaScript对象的模型。 这其中尤其独特的一个例子是“声明合并”的概念。 理解了这个概念,将有助于操作现有的JavaScript代码。 同时,也会有助于理解更多高级抽象的概念。
对本文件来讲,“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并;不局限于两个声明。
Typescript中的声明会创建以下三种实体之一:命名空间,类型或值。 创建命名空间的声明会新建一个命名空间,它包含了用(.)符号来访问时使用的名字。 创建类型的声明是:用声明的模型创建一个类型并绑定到给定的名字上。 最后,创建值的声明会创建在JavaScript输出中看到的值。
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | X | X | |
Class | X | X | |
Enum | X | X | |
Interface | X | ||
Type Alias | X | ||
Function | X | ||
Variable | X |
理解每个声明创建了什么,有助于理解当声明合并时有哪些东西被合并了。
最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。
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;}
与接口相似,同名的命名空间也会合并其成员。 命名空间会创建出命名空间和值,我们需要知道这两者都是怎么合并的。
对于命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口。
对于命名空间里值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。
Animals
声明合并示例:
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, haveMuscles is not visible here }}
因为haveMuscles
并没有导出,只有animalsHaveMuscles
函数共享了原始未合并的命名空间可以访问这个变量。 doAnimalsHaveMuscles
函数虽是合并命名空间的一部分,但是访问不了未导出的成员。
命名空间可以与其它类型的声明进行合并。 只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。 Typescript使用这个功能去实现一些JavaScript里的设计模式。
这让我们可以表示内部类。
class Album { label: Album.AlbumLabel;}namespace Album { export class AlbumLabel { }}
合并规则与上面合并命名空间
小节里讲的规则一致,我们必须导出AlbumLabel
类,好让合并的类能访问。 合并结果是一个类并带有一个内部类。 你也可以使用命名空间为类增加一些静态属性。
除了内部类的模式,你在JavaScript里,创建一个函数稍后扩展它增加一些属性也是很常见的。 Typescript使用声明合并来达到这个目的并保证类型安全。
function buildLabel(name: string): string { return buildLabel.prefix + name + buildLabel.suffix;}namespace buildLabel { export let suffix = ""; export let prefix = "Hello, ";}alert(buildLabel("Sam Smith"));
相似的,命名空间可以用来扩展枚举型:
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; } }}
TypeScript并非允许所有的合并。 目前,类不能与其它类或变量合并。 想要了解如何模仿类的合并,请参考TypeScript的混入。
虽然JavaScript不支持合并,但你可以为导入的对象打补丁以更新它们。让我们考察一下这个玩具性的示例:
// observable.jsexport class Observable<T> { // ... implementation left as an exercise for the reader ...}// map.jsimport { Observable } from "./observable";Observable.prototype.map = function (f) { // ... another exercise for the reader}
它也可以很好地工作在TypeScript中, 但编译器对 Observable.prototype.map
一无所知。 你可以使用扩展模块来将它告诉编译器:
// observable.ts stays the same// map.tsimport { Observable } from "./observable";declare module "./observable" { interface Observable<T> { map<U>(f: (x: T) => U): Observable<U>; }}Observable.prototype.map = function (f) { // ... another exercise for the reader}// consumer.tsimport { Observable } from "./observable";import "./map";let o: Observable<number>;o.map(x => x.toFixed());
模块名的解析和用import
/export
解析模块标识符的方式是一致的。 更多信息请参考 Modules。 当这些声明在扩展中合并时,就好像在原始位置被声明了一样。但是,你不能在扩展中声明新的顶级声明--仅可以扩展模块中已经存在的声明。
你也以在模块内部添加声明到全局作用域中。
// observable.tsexport class Observable<T> { // ... still no implementation ...}declare global { interface Array<T> { toObservable(): Observable<T>; }}Array.prototype.toObservable = function () { // ...}
全局扩展与模块扩展的行为和限制是相同的。
JSX是一种嵌入式的类似XML的语法。 它可以被转换成合法的JavaScript,尽管转换的语义是依据不同的实现而定的。 JSX因 React框架而流行,但是也被其它应用所使用。 TypeScript支持内嵌,类型检查和将JSX直接编译为JavaScript。
想要使用JSX必须做两件事:
.tsx
扩展名jsx
选项TypeScript具有两种JSX模式:preserve
和react
。 这些模式只在代码生成阶段起作用 - 类型检查并不受影响。 在 preserve
模式下生成代码中会保留JSX以供后续的转换操作使用(比如:Babel)。 另外,输出文件会带有.jsx
扩展名。 react
模式会生成React.createElement
,在使用前不需要再进行转换操作了,输出文件的扩展名为.js
。
模式 | 输入 | 输出 | 输出文件扩展名 |
---|---|---|---|
preserve | <div /> | <div /> | .jsx |
react | <div /> | React.createElement("div") | .js |
你可以通过在命令行里使用--jsx
标记或tsconfig.json里的选项来指定模式。
注意:
React
标识符是写死的硬代码,所以你必须保证React(大写的R)是可用的。 Note: The identifierReact
is hard-coded, so you must make React available with an uppercase R.
as
操作符回想一下怎么写类型断言:
var foo = <foo>bar;
这里我们断言bar
变量是foo
类型的。 因为TypeScript也使用尖括号来表示类型断言,JSX的语法带来了解析的困难。因此,TypeScript在 .tsx
文件里禁用了使用尖括号的类型断言。
为了弥补.tsx
里的这个功能,新加入了一个类型断言符号:as
。 上面的例子可以很容易地使用 as
操作符改写:
var foo = bar as foo;
as
操作符在.ts
和.tsx
里都可用,并且与其它类型断言行为是等价的。
为了理解JSX的类型检查,你必须首先理解固有元素与基于值的元素之间的区别。 假设有这样一个JSX表达式 <expr />
,expr
可能引用环境自带的某些东西(比如,在DOM环境里的div
或span
)或者是你自定义的组件。 这是非常重要的,原因有如下两点:
React.createElement("div")
),然而由你自定义的组件却不会生成(React.createElement(MyComponent)
)。TypeScript使用与React相同的规范 来区别它们。 固有元素总是以一个小写字母开头,基于值的元素总是以一个大写字母开头。
固有元素使用特殊的接口JSX.IntrinsicElements
来查找。 默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。 然而,如果接口存在,那么固有元素的名字需要在 JSX.IntrinsicElements
接口的属性里查找。 例如:
declare namespace JSX { interface IntrinsicElements { foo: any }}<foo />; // 正确<bar />; // 错误
在上例中,<foo />
没有问题,但是<bar />
会报错,因为它没在JSX.IntrinsicElements
里指定。
注意:你也可以在
JSX.IntrinsicElements
上指定一个用来捕获所有字符串索引:declare namespace JSX { interface IntrinsicElements { [elemName: string]: any; }}
基于值的元素会简单的在它所在的作用域里按标识符查找。
import MyComponent from "./myComponent";<MyComponent />; // 正确<SomeOtherComponent />; // 错误
可以限制基于值的元素的类型。 然而,为了这么做我们需要引入两个新的术语: 元素类的类型和元素实例的类型。
现在有<Expr />
,元素类的类型为Expr
的类型。 所以在上面的例子里,如果 MyComponent
是ES6的类,那么它的类类型就是这个类。 如果 MyComponent
是个工厂函数,类类型为这个函数。
一旦建立起了类类型,实例类型就确定了,为类类型调用签名的返回值与构造签名的联合类型。 再次说明,在ES6类的情况下,实例类型为这个类的实例的类型,并且如果是工厂函数,实例类型为这个函数返回值类型。
class MyComponent { render() {}}// 使用构造签名var myComponent = new MyComponent();// 元素类的类型 => MyComponent// 元素实例的类型 => { render: () => void }function MyFactoryFunction() { return { render: () => { } }}// 使用调用签名var myComponent = MyFactoryFunction();// 元素类的类型 => FactoryFunction// 元素实例的类型 => { render: () => void }
元素的实例类型很有趣,因为它必须赋值给JSX.ElementClass
或抛出一个错误。 默认的 JSX.ElementClass
为{}
,但是它可以被扩展用来限制JSX的类型以符合相应的接口。
declare namespace JSX { interface ElementClass { render: any; }}class MyComponent { render() {}}function MyFactoryFunction() { return { render: () => {} }}<MyComponent />; // 正确<MyFactoryFunction />; // 正确class NotAValidComponent {}function NotAValidFactoryFunction() { return {};}<NotAValidComponent />; // 错误<NotAValidFactoryFunction />; // 错误
属性类型检查的第一步是确定元素属性类型。 这在固有元素和基于值的元素之间稍有不同。
对于固有元素,这是JSX.IntrinsicElements
属性的类型。
declare namespace JSX { interface IntrinsicElements { foo: { bar?: boolean } }}// `foo`的元素属性类型为`{bar?: boolean}`<foo bar />;
对于基于值的元素,就稍微复杂些。 它取决于先前确定的在元素实例类型上的某个属性的类型。 至于该使用哪个属性来确定类型取决于 JSX.ElementAttributesProperty
。 它应该使用单一的属性来定义。 这个属性名之后会被使用。
declare namespace JSX { interface ElementAttributesProperty { props; // 指定用来使用的属性名 }}class MyComponent { // 在元素实例类型上指定属性 props: { foo?: string; }}// `MyComponent`的元素属性类型为`{foo?: string}`<MyComponent foo="bar" />
元素属性类型用于的JSX里进行属性的类型检查。 支持可选属性和必须属性。
declare namespace JSX { interface IntrinsicElements { foo: { requiredProp: string; optionalProp?: number } }}<foo requiredProp="bar" />; // 正确<foo requiredProp="bar" optionalProp={0} />; // 正确<foo />; // 错误, 缺少 requiredProp<foo requiredProp={0} />; // 错误, requiredProp 应该是字符串<foo requiredProp="bar" unknownProp />; // 错误, unknownProp 不存在<foo requiredProp="bar" some-unknown-prop />; // 正确, `some-unknown-prop`不是个合法的标识符
注意:如果一个属性名不是个合法的JS标识符(像
data-*
属性),并且它没出现在元素属性类型里时不会当做一个错误。
延展操作符也可以使用:
var props = { requiredProp: 'bar' };<foo {...props} />; // 正确var badProps = {};<foo {...badProps} />; // 错误
默认地JSX表达式结果的类型为any。 你可以自定义这个类型,通过指定
JSX.Element`接口。 然而,不能够从接口里检索元素,属性或JSX的子元素的类型信息。 它是一个黑盒。
JSX允许你使用{ }
标签来内嵌表达式。
var a = <div> {['foo', 'bar'].map(i => <span>{i / 2}</span>)}</div>
上面的代码产生一个错误,因为你不能用数字来除以一个字符串。 输出如下,若你使用了 preserve
选项:
var a = <div> {['foo', 'bar'].map(function (i) { return <span>{i / 2}</span>; })}</div>
要想一起使用JSX和React,你应该使用React类型定义。 这些类型声明定义了 JSX
合适命名空间来使用React。
/// <reference path="react.d.ts" />interface Props { foo: string;}class MyComponent extends React.Component<Props, {}> { render() { return <span>{this.props.foo}</span> }}<MyComponent foo="bar" />; // 正确<MyComponent foo={0} />; // 错误
随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第一阶段,但在TypeScript里已做为一项实验性特性予以支持。
注意 装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json
里启用experimentalDecorators
编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true }}
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用@expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
例如,有一个@sealed
装饰器,我们会这样定义sealed
函数:
function sealed(target) { // do something with "target" ...}
注意 后面类装饰器小节里有一个更加详细的例子。
如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
我们可以通过下面的方式来写一个装饰器工厂函数:
function color(value: string) { // 这是一个装饰器工厂 return function (target) { // 这是装饰器 // do something with "target" and "value"... }}
注意 下面方法装饰器小节里有一个更加详细的例子。
多个装饰器可以同时应用到一个声明上,就像下面的示例:
@f @g x
@f@gx
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合f和g时,复合的结果(f ∘ g)(x)等同于f(g(x))。
同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
如果我们使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:
function f() { console.log("f(): evaluated"); return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("f(): called"); }}function g() { console.log("g(): evaluated"); return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("g(): called"); }}class C { @f() @g() method() {}}
在控制台里会打印出如下结果:
f(): evaluatedg(): evaluatedg(): calledf(): called
类中不同声明上的装饰器将按以下规定的顺序应用:
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts
),也不能用在任何外部上下文中(比如declare
的类)。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
注意 如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中 不会为你做这些。
下面是使用类装饰器(@sealed
)的例子,应用在Greeter
类:
@sealedclass Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; }}
我们可以这样定义@sealed
装饰器:
function sealed(constructor: Function) { Object.seal(constructor); Object.seal(constructor.prototype);}
当@sealed
被执行的时候,它将密封此类的构造函数和原型。(注:参见Object.seal)
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts
),重载或者任何外部上下文(比如declare
的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
注意 如果代码输出目标版本小于
ES5
,Property Descriptor将会是undefined
。
如果方法装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
返回值会被忽略。
下面是一个方法装饰器(@enumerable
)的例子,应用于Greeter
类的方法上:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } @enumerable(false) greet() { return "Hello, " + this.greeting; }}
我们可以用下面的函数声明来定义@enumerable
装饰器:
function enumerable(value: boolean) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.enumerable = value; };}
这里的@enumerable(false)
是一个装饰器工厂。 当装饰器 @enumerable(false)
被调用时,它会修改属性描述符的enumerable
属性。
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare
的类)里。
注意 TypeScript不允许同时装饰一个成员的
get
和set
访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get
和set
访问器,而不是分开声明的。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
注意 如果代码输出目标版本小于
ES5
,Property Descriptor将会是undefined
。
如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
返回值会被忽略。
下面是使用了访问器装饰器(@configurable
)的例子,应用于Point
类的成员上:
class Point { private _x: number; private _y: number; constructor(x: number, y: number) { this._x = x; this._y = y; } @configurable(false) get x() { return this._x; } @configurable(false) get y() { return this._y; }}
我们可以通过如下函数声明来定义@configurable
装饰器:
function configurable(value: boolean) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.configurable = value; };}
属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare
的类)里。
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
注意 属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。 因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
如果属性装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
,返回值会被忽略。
如果访问符装饰器返回一个值,它会被用作方法的属性描述符。
我们可以用它来记录这个属性的元数据,如下例所示:
class Greeter { @format("Hello, %s") greeting: string; constructor(message: string) { this.greeting = message; } greet() { let formatString = getFormat(this, "greeting"); return formatString.replace("%s", this.greeting); }}
然后定义@format
装饰器和getFormat
函数:
import "reflect-metadata";const formatMetadataKey = Symbol("format");function format(formatString: string) { return Reflect.metadata(formatMetadataKey, formatString);}function getFormat(target: any, propertyKey: string) { return Reflect.getMetadata(formatMetadataKey, target, propertyKey);}
这个@format("Hello, %s")
装饰器是个 装饰器工厂。 当 @format("Hello, %s")
被调用时,它添加一条这个属性的元数据,通过reflect-metadata
库里的Reflect.metadata
函数。 当 getFormat
被调用时,它读取格式的元数据。
注意 这个例子需要使用
reflect-metadata
库。 查看 元数据了解reflect-metadata
库更详细的信息。
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare
的类)里。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
注意 参数装饰器只能用来监视一个方法的参数是否被传入。
参数装饰器的返回值会被忽略。
下例定义了参数装饰器(@required
)并应用于Greeter
类方法的一个参数:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } @validate greet(@required name: string) { return "Hello " + name + ", " + this.greeting; }}
然后我们使用下面的函数定义 @required
和 @validate
装饰器:
import "reflect-metadata";const requiredMetadataKey = Symbol("required");function required(target: Object, propertyKey: string | symbol, parameterIndex: number) { let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; existingRequiredParameters.push(parameterIndex); Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);}function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) { let method = descriptor.value; descriptor.value = function () { let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName); if (requiredParameters) { for (let parameterIndex of requiredParameters) { if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) { throw new Error("Missing required argument."); } } } return method.apply(this, arguments); }}
@required
装饰器添加了元数据实体把参数标记为必需的。 @validate
装饰器把greet
方法包裹在一个函数里在调用原先的函数前验证函数参数。
注意 这个例子使用了
reflect-metadata
库。 查看 元数据了解reflect-metadata
库的更多信息。
一些例子使用了reflect-metadata
库来支持实验性的metadata API。 这个库还不是ECMAScript (JavaScript)标准的一部分。 然而,当装饰器被ECMAScript官方标准采纳后,这些扩展也将被推荐给ECMAScript以采纳。
你可以通过npm安装这个库:
npm i reflect-metadata --save
TypeScript支持为带有装饰器的声明生成元数据。 你需要在命令行或 tsconfig.json
里启用emitDecoratorMetadata
编译器选项。
Command Line:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true }}
当启用后,只要reflect-metadata
库被引入了,设计阶段添加的类型信息可以在运行时使用。
如下例所示:
import "reflect-metadata";class Point { x: number; y: number;}class Line { private _p0: Point; private _p1: Point; @validate set p0(value: Point) { this._p0 = value; } get p0() { return this._p0; } @validate set p1(value: Point) { this._p1 = value; } get p1() { return this._p1; }}function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) { let set = descriptor.set; descriptor.set = function (value: T) { let type = Reflect.getMetadata("design:type", target, propertyKey); if (!(value instanceof type)) { throw new TypeError("Invalid type."); } }}
TypeScript编译器可以通过@Reflect.metadata
装饰器注入设计阶段的类型信息。 你可以认为它相当于下面的TypeScript:
class Line { private _p0: Point; private _p1: Point; @validate @Reflect.metadata("design:type", Point) set p0(value: Point) { this._p0 = value; } get p0() { return this._p0; } @validate @Reflect.metadata("design:type", Point) set p1(value: Point) { this._p1 = value; } get p1() { return this._p1; }}
注意 装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes)。
除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。 你可能在Scala等语言里对mixins及其特性已经很熟悉了,但它在JavaScript中也是很流行的。
下面的代码演示了如何在TypeScript里使用混入。 后面我们还会解释这段代码是怎么工作的。
// Disposable Mixinclass Disposable { isDisposed: boolean; dispose() { this.isDisposed = true; }}// Activatable Mixinclass Activatable { isActive: boolean; activate() { this.isActive = true; } deactivate() { this.isActive = false; }}class SmartObject implements Disposable, Activatable { constructor() { setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500); } interact() { this.activate(); } // Disposable isDisposed: boolean = false; dispose: () => void; // Activatable isActive: boolean = false; activate: () => void; deactivate: () => void;}applyMixins(SmartObject, [Disposable, Activatable]);let smartObj = new SmartObject();setTimeout(() => smartObj.interact(), 1000);////////////////////////////////////////// In your runtime library somewhere////////////////////////////////////////function applyMixins(derivedCtor: any, baseCtors: any[]) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { derivedCtor.prototype[name] = baseCtor.prototype[name]; }); });}
代码里首先定义了两个类,它们将做为mixins。 可以看到每个类都只定义了一个特定的行为或功能。 稍后我们使用它们来创建一个新类,同时具有这两种功能。
// Disposable Mixinclass Disposable { isDisposed: boolean; dispose() { this.isDisposed = true; }}// Activatable Mixinclass Activatable { isActive: boolean; activate() { this.isActive = true; } deactivate() { this.isActive = false; }}
下面创建一个类,结合了这两个mixins。 下面来看一下具体是怎么操作的:
class SmartObject implements Disposable, Activatable {
首先应该注意到的是,没使用extends
而是使用implements
。 把类当成了接口,仅使用Disposable和Activatable的类型而非其实现。 这意味着我们需要在类里面实现接口。 但是这是我们在用mixin时想避免的。
我们可以这么做来达到目的,为将要mixin进来的属性方法创建出占位属性。 这告诉编译器这些成员在运行时是可用的。 这样就能使用mixin带来的便利,虽说需要提前定义一些占位属性。
// DisposableisDisposed: boolean = false;dispose: () => void;// ActivatableisActive: boolean = false;activate: () => void;deactivate: () => void;
最后,把mixins混入定义的类,完成全部实现部分。
applyMixins(SmartObject, [Disposable, Activatable]);
最后,创建这个帮助函数,帮我们做混入操作。 它会遍历mixins上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。
function applyMixins(derivedCtor: any, baseCtors: any[]) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { derivedCtor.prototype[name] = baseCtor.prototype[name]; }) });}
三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。
三斜线指令仅可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。 如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。
/// <reference path="..." />
/// <reference path="..." />
指令是三斜线指令中最常见的一种。 它用于声明文件间的 依赖。
三斜线引用告诉编译器在编译过程中要引入的额外的文件。
当使用--out
或--outFile
时,它也可以做为调整输出内容顺序的一种方法。 文件在输出文件内容中的位置与经过预处理后的输入顺序一致。
编译器会对输入文件进行预处理来解析所有三斜线引用指令。 在这个过程中,额外的文件会加到编译过程中。
这个过程会以一些根文件开始; 它们是在命令行中指定的文件或是在 tsconfig.json
中的"files"
列表里的文件。 这些根文件按指定的顺序进行预处理。 在一个文件被加入列表前,它包含的所有三斜线引用都要被处理,还有它们包含的目标。 三斜线引用以它们在文件里出现的顺序,使用深度优先的方式解析。
一个三斜线引用路径是相对于包含它的文件的,如果不是根文件。
引用不存在的文件会报错。 一个文件用三斜线指令引用自己会报错。
--noResolve
如果指定了--noResolve
编译选项,三斜线引用会被忽略;它们不会增加新文件,也不会改变给定文件的顺序。
/// <reference no-default-lib="true"/>
这个指令把一个文件标记成默认库。 你会在 lib.d.ts
文件和它不同的变体的顶端看到这个注释。
这个指令告诉编译器在编译过程中不要包含这个默认库(比如,lib.d.ts
)。 这与在命令行上使用 --noLib
相似。
还要注意,当传递了--skipDefaultLibCheck
时,编译器只会忽略检查带有/// <reference no-default-lib="true"/>
的文件。
/// <amd-module />
默认情况下生成的AMD模块都是匿名的。 但是,当一些工具需要处理生成的模块时会产生问题,比如 r.js
。
amd-module
指令允许给编译器传入一个可选的模块名:
///<amd-module name='NamedModule'/>export class C {}
这会将NamedModule
传入到AMD define
函数里:
define("NamedModule", ["require", "exports"], function (require, exports) { var C = (function () { function C() { } return C; })(); exports.C = C;});
/// <amd-dependency />
注意:这个指令被废弃了。使用
import "moduleName";
语句代替。
/// <amd-dependency path="x" />
告诉编译器有一个非TypeScript模块依赖需要被注入,做为目标模块require
调用的一部分。
amd-dependency
指令也可以带一个可选的name
属性;它允许我们为amd-dependency传入一个可选名字:
/// <amd-dependency path="legacy/moduleA" name="moduleA"/>declare var moduleA:MyTypemoduleA.callStuff()
生成的JavaScript代码:
define(["require", "exports", "legacy/moduleA"], function (require, exports, moduleA) { moduleA.callStuff()});
这篇指南的目的是教你如何书写高质量的TypeScript声明文件。
在这篇指南里,我们假设你对TypeScript已经有了基本的了解。 如果没有,请先阅读 TypeScript手册 来了解一些基本知识,尤其是类型和命名空间部分。
这篇指南被分成了以下章节。
结构一节将帮助你了解常见库的格式以及如何为每种格式书写正确的声明文件。 如果你在编辑一个已经存在的文件,那么你可能不需要阅读此章节。 如果你在书写新的声明文件,那么你必须阅读此章节以理解库的不同格式是如何影响声明文件的书写的。
声明文件里有很多常见的错误是很容易避免的。规范一节指出了常见的错误, 描述了如何发现它们, 与怎样去修复。 每个人都要阅读这个章节以了解如何避免常见错误。
很多时候,我们只能通过一些示例来了解第三方库是如何工作的,同时我们需要为这样的库书写声明文件。举例一节展示了很多常见的API模式以及如何为它们书写声明文件。 这篇指南是针对TypeScript初学者的,他们可能还不了解TypeScript里的所有语言结构。
对于那些对声明文件底层工作机制感兴趣的老手们,深入一节解释了很多高级书写声明文件的高级概念, 以及展示了如何利用这些概念来创建整洁和直观的声明文件。
在模版一节里,你能找到一些声明文件,它们可以帮助你快速开始 当你在书写一个新声明文件的时候。 参考结构这篇文档来找到应该使用哪个模版文件。
发布一节讲解了如何发布声明文件为npm包,及如何管理包的依赖。
对于JavaScript库的使用者来讲,使用一节提供了一些简单步骤来定位与安装相应的声明文件。
一般来讲,你组织声明文件的方式取决于库是如何被使用的。 在JavaScript里提供了很多库的使用方法,这就需要你书写声明文件去匹配它们。 这篇指南涵盖了如何识别常见库的模式,与怎么样书写符合相应模式的声明文件。
针对每种主要的库的组织模式,在模版一节都有对应的文件。 你可以利用它们帮助你快速上手。
首先,我们先看一下TypeScript声明文件能够表示的库的类型。 这里会简单展示每种类型的库的使用方式,如何去书写,还有一些真实案例。
识别库的类型是书写声明文件的第一步。 我们将会给出一些提示,关于怎样通过库的 使用方法及其源码来识别库的类型。 根据库的文档及组织结构不同,这两种方式可能一个会比另外的那个简单一些。 我们推荐你使用任意你喜欢的方式。
全局库是指能在全局命名空间下访问的(例如:不需要使用任何形式的import
)。 许多库都是简单的暴露出一个或多个全局变量。 比如,如果你使用过 jQuery,$
变量可以被够简单的引用:
$(() => { console.log('hello!'); } );
你经常会在全局库的指南文档上看到如何在HTML里用脚本标签引用库:
<script src="http://a.great.cdn.for/someLib.js"></script>
目前,大多数流行的全局访问型库实际上都以UMD库的形式进行书写(见后文)。 UMD库的文档很难与全局库文档两者之间难以区分。 在书写全局声明文件前,一定要确认一下库是否真的不是UMD。
全局库的代码通常都十分简单。 一个全局的“Hello, world”库可能是这样的:
function createGreeting(s) { return "Hello, " + s;}
或这样:
window.createGreeting = function(s) { return "Hello, " + s;}
当你查看全局库的源代码时,你通常会看到:
var
语句或function
声明window.someName
document
或window
是存在的你不会看到:
require
或define
var fs = require("fs");
define(...)
调用require
或导入这个库由于把一个全局库转变成UMD库是非常容易的,所以很少流行的库还再使用全局的风格。 然而,小型的且需要DOM(或 没有依赖)的库可能还是全局类型的。
模版文件global.d.ts
定义了myLib
库作为例子。 一定要阅读 "防止命名冲突"补充说明。
一些库只能工作在模块加载器的环境下。 比如,像 express
只能在Node.js里工作所以必须使用CommonJS的require
函数加载。
ECMAScript 2015(也就是ES2015,ECMAScript 6或ES6),CommonJS和RequireJS具有相似的导入一个模块的表示方法。 例如,对于JavaScript CommonJS (Node.js),有下面的代码
var fs = require("fs");
对于TypeScript或ES6,import
关键字也具有相同的作用:
import fs = require("fs");
你通常会在模块化库的文档里看到如下说明:
var someLib = require('someLib');
或
define(..., ['someLib'], function(someLib) {});
与全局模块一样,你也可能会在UMD模块的文档里看到这些例子,因此要仔细查看源码和文档。
模块库至少会包含下列具有代表性的条目之一:
require
或define
import * as a from 'b';
or export c;
这样的声明exports
或module.exports
它们极少包含:
window
或global
的赋值许多流行的Node.js库都是这种模块化的,例如express
,gulp
和 request
。
UMD模块是指那些既可以作为模块使用(通过导入)又可以作为全局(在没有模块加载器的环境里)使用的模块。 许多流行的库,比如 Moment.js,就是这样的形式。 比如,在Node.js或RequireJS里,你可以这样写:
import moment = require("moment");console.log(moment.format());
然而在纯净的浏览器环境里你也可以这样写:
console.log(moment.format());
UMD模块会检查是否存在模块加载器环境。 这是非常形容观察到的模块,它们会像下面这样:
(function (root, factory) { if (typeof define === "function" && define.amd) { define(["libName"], factory); } else if (typeof module === "object" && module.exports) { module.exports = factory(require("libName")); } else { root.returnExports = factory(root.libName); }}(this, function (b) {
如果你在库的源码里看到了typeof define
,typeof window
,或typeof module
这样的测试,尤其是在文件的顶端,那么它几乎就是一个UMD库。
UMD库的文档里经常会包含通过require
“在Node.js里使用”例子, 和“在浏览器里使用”的例子,展示如何使用<script>
标签去加载脚本。
大多数流行的库现在都能够被当成UMD包。 比如 jQuery,Moment.js,lodash和许多其它的。
针对模块有三种可用的模块, module.d.ts
, module-class.d.ts
and module-function.d.ts
.
使用module-function.d.ts
,如果模块能够作为函数调用。
var x = require("foo");// Note: calling 'x' as a functionvar y = x(42);
一定要阅读补充说明: “ES6模块调用签名的影响”
使用module-class.d.ts
如果模块能够使用new
来构造:
var x = require("bar");// Note: using 'new' operator on the imported variablevar y = new x("hello");
相同的补充说明作用于这些模块。
如果模块不能被调用或构造,使用module.d.ts
文件。
一个模块插件可以改变一个模块的结构(UMD或模块)。 例如,在Moment.js里, moment-range
添加了新的range
方法到monent
对象。
对于声明文件的目标,我们会写相同的代码不论被改变的模块是一个纯粹的模块还是UMD模块。
使用module-plugin.d.ts
模版。
一个全局插件是全局代码,它们会改变全局对象的结构。 对于 全局修改的模块,在运行时存在冲突的可能。
比如,一些库往Array.prototype
或String.prototype
里添加新的方法。
全局通常很容易地从它们的文档识别出来。
你会看到像下面这样的例子:
var x = "hello, world";// Creates new methods on built-in typesconsole.log(x.startsWithHello());var y = [1, 2, 3];// Creates new methods on built-in typesconsole.log(y.reverseAndSort());
使用global-plugin.d.ts
模版。
当一个全局修改的模块被导入的时候,它们会改变全局作用域里的值。 比如,存在一些库它们添加新的成员到String.prototype
当导入它们的时候。 这种模式很危险,因为可能造成运行时的冲突, 但是我们仍然可以为它们书写声明文件。
全局修改的模块通常可以很容易地从它们的文档识别出来。 通常来讲,它们与全局插件相似,但是需要 require
调用来激活它们的效果。
你可能会看到像下面这样的文档:
// 'require' call that doesn't use its return valuevar unused = require("magic-string-time");/* or */require("magic-string-time");var x = "hello, world";// Creates new methods on built-in typesconsole.log(x.startsWithHello());var y = [1, 2, 3];// Creates new methods on built-in typesconsole.log(y.reverseAndSort());
使用global-modifying-module.d.ts
模版。
可能会有以下几种依赖。
如果你的库依赖于某个全局库,使用/// <reference types="..." />
指令:
/// <reference types="someLib" />function getThing(): someLib.thing;
如果你的库依赖于模块,使用import
语句:
import * as moment from "moment";function getThing(): moment;
如果你的全局库依赖于某个UMD模块,使用/// <reference types
指令:
/// <reference types="moment" />function getThing(): moment;
如果你的模块或UMD库依赖于一个UMD库,使用import
语句:
import * as someLib from 'someLib';
不要使用/// <reference
指令去声明UMD库的依赖!
注意,在书写全局声明文件时,允许在全局作用域里定义很多类型。 我们十分不建义这样做,当一个工程里有许多声明文件时,它会导致无法处理的命名冲突。
一个简单的规则是使用库定义的全局变量名来声明命名空间类型。 比如,库定义了一个全局的值 cats
,你可以这样写
declare namespace cats { interface KittySettings { }}
不要
// at top-levelinterface CatsKittySettings { }
这样也保证了库在转换成UMD的时候没有任何的破坏式改变,对于声明文件用户来说。
一些插件添加或修改已存在的顶层模块的导出部分。 当然这在CommonJS和其它加载器里是允许的,ES模块被当作是不可改变的因此这种模式就不可行了。 因为TypeScript是能不预知加载器类型的,所以没没在编译时保证,但是开发者如果要转到ES6模块加载器上应该注意这一点。
很多流行库,比如Express,暴露出自己作为可以调用的函数。 比如,典型的Express使用方法如下:
import exp = require("express");var app = exp();
在ES6模块加载器里,顶层的对象(这里以exp
导入)只能具有属性; 顶层的模块对象 永远不能被调用。 十分常见的解决方法是定义一个 default
导出到一个可调用的/可构造的对象; 一会模块加载器助手工具能够自己探测到这种情况并且使用 default
导出来替换顶层对象。
这篇指南的目的是教你如何书写高质量的TypeScript声明文件。 我们在这里会展示一些API的文档,还有它们的使用示例, 并且阐述了如何为它们书写声明文件。
这些例子是按复杂度递增的顺序组织的。
文档
全局变量
foo
包含了存在组件总数。
代码
console.log("Half the number of widgets is " + (foo / 2));
声明
使用declare var
声明变量。 如果变量是只读的,那么可以使用 declare const
。 你还可以使用 declare let
如果变量拥有块级作用域。
/** 组件总数 */declare var foo: number;
文档
用一个字符串参数调用
greet
函数向用户显示一条欢迎信息。
代码
greet("hello, world");
声明
使用declare function
声明函数。
declare function greet(greeting: string): void;
文档
全局变量
myLib
包含一个makeGreeting
函数, 还有一个属性numberOfGreetings
指示目前为止欢迎数量。
代码
let result = myLib.makeGreeting("hello, world");console.log("The computed greeting is:" + result);let count = myLib.numberOfGreetings;
声明
使用declare namespace
描述用点表示法访问的类型或值。
declare namespace myLib { function makeGreeting(s: string): string; let numberOfGreetings: number;}
文档
getWidget
函数接收一个数字,返回一个组件,或接收一个字符串并返回一个组件数组。
代码
let x: Widget = getWidget(43);let arr: Widget[] = getWidget("all of them");
声明
declare function getWidget(n: number): Widget;declare function getWidget(s: string): Widget[];
文档
当指定一个欢迎词时,你必须传入一个
GreetingSettings
对象。 这个对象具有以下几个属性:
- greeting:必需的字符串
- duration: 可靠的时长(毫秒表示)
- color: 可选字符串,比如‘#ff00ff’
代码
greet({ greeting: "hello world", duration: 4000});
声明
使用interface
定义一个带有属性的类型。
interface GreetingSettings { greeting: string; duration?: number; color?: string;}declare function greet(setting: GreetingSettings): void;
文档
在任何需要欢迎词的地方,你可以提供一个
string
,一个返回string
的函数或一个Greeter
实例。
代码
function getGreeting() { return "howdy";}class MyGreeter extends Greeter { }greet("hello");greet(getGreeting);greet(new MyGreeter());
声明
你可以使用类型别名来定义类型的短名:
type GreetingLike = string | (() => string) | Greeting;declare function greet(g: GreetingLike): void;
文档
greeter
对象能够记录到文件或显示一个警告。 你可以为.log(...)
提供LogOptions和为.alert(...)
提供选项。
代码
const g = new Greeter("Hello");g.log({ verbose: true });g.alert({ modal: false, title: "Current Greeting" });
声明
使用命名空间组织类型。
declare namespace GreetingLib { interface LogOptions { verbose?: boolean; } interface AlertOptions { modal: boolean; title?: string; color?: string; }}
你也可以在一个声明中创建嵌套的命名空间:
declare namespace GreetingLib.Options { // Refer to via GreetingLib.Options.Log interface Log { verbose?: boolean; } interface Alert { modal: boolean; title?: string; color?: string; }}
文档
你可以通过实例化
Greeter
对象来创建欢迎词,或者继承Greeter
对象来自定义欢迎词。
代码
const myGreeter = new Greeter("hello, world");myGreeter.greeting = "howdy";myGreeter.showGreeting();class SpecialGreeter extends Greeter { constructor() { super("Very special greetings"); }}
声明
使用declare class
描述一个类或像类一样的对象。 类可以有属性和方法,就和构造函数一样。
declare class Greeter { constructor(greeting: string); greeting: string; showGreeting(): void;}
Number
,String
,Boolean
和Object
不要使用如下类型Number
,String
,Boolean
或Object
。 这些类型指的是非原始的装盒对象,它们几乎没在JavaScript代码里正确地使用过。
/* 错误 */function reverse(s: String): String;
应该使用类型number
,string
,and boolean
。
/* OK */function reverse(s: string): string;
如果你就要使用Object
类型,考虑使用any
代替。 目前在TypeScript里无法指定一个对象“不是一个原始值”。
不要定义一个从来没使用过其类型参数的泛型类型。 了解详情 TypeScript FAQ page。
不要为返回值被忽略的回调函数设置一个any
类型的返回值类型:
/* 错误 */function fn(x: () => any) { x();}
应该给返回值被忽略的回调函数设置void
类型的返回值类型:
/* OK */function fn(x: () => void) { x();}
为什么:使用void
相对安全,因为它防止了你不小心使用x
的返回值:
function fn(x: () => void) { var k = x(); // oops! meant to do something else k.doSomething(); // error, but would be OK if the return type had been 'any'}
不要在回调函数里使用可选参数除非你真的要这么做:
/* 错误 */interface Fetcher { getObject(done: (data: any, elapsedTime?: number) => void): void;}
这里有一种特殊的意义:done
回调函数可能以1个参数或2个参数调用。 代码大概的意思是说这个回调函数不在乎是否有 elapsedTime
参数, 但是不需要把这个参数当成可选参数来达到此目的 -- 因为总是允许提供一个接收较少参数的回调函数。
应该写出回调函数的非可选参数:
/* OK */interface Fetcher { getObject(done: (data: any, elapsedTime: number) => void): void;}
不要因为回调函数参数个数不同而写不同的重载:
/* 错误 */declare function beforeAll(action: () => void, timeout?: number): void;declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;
应该只使用最大参数个数写一个重载:
/* OK */declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;
为什么:回调函数总是可以忽略某个参数的,因此没必要为参数少的情况写重载。 参数少的回调函数首先允许错误类型的函数被传入,因为它们匹配第一个重载。
不要把一般的重载放在精确的重载前面:
/* 错误 */declare function fn(x: any): any;declare function fn(x: HTMLElement): number;declare function fn(x: HTMLDivElement): string;var myElem: HTMLDivElement;var x = fn(myElem); // x: any, wat?
应该排序重载令精确的排在一般的之前:
/* OK */declare function fn(x: HTMLDivElement): string;declare function fn(x: HTMLElement): number;declare function fn(x: any): any;var myElem: HTMLDivElement;var x = fn(myElem); // x: string, :)
为什么:TypeScript会选择第一个匹配到的重载当解析函数调用的时候。 当前面的重载比后面的“普通”,那么后面的被隐藏了不会被调用。
不要为仅在末尾参数不同时写不同的重载:
/* 错误 */interface Example { diff(one: string): number; diff(one: string, two: string): number; diff(one: string, two: string, three: boolean): number;}
应该尽可能使用可选参数:
/* OK */interface Example { diff(one: string, two?: string, three?: boolean): number;}
注意这在所有重载都有相同类型的返回值时会不好用。
为什么:有两种生要的原因。
TypeScript解析签名兼容性时会查看是否某个目标签名能够使用源的参数调用, 且允许外来参数。 下面的代码暴露出一个bug,当签名被正确的使用可选参数书写时:
function fn(x: (a: string, b: number, c: number) => void) { }var x: Example;// When written with overloads, OK -- used first overload// When written with optionals, correctly an errorfn(x.diff);
第二个原因是当使用了TypeScript“严格检查null”特性时。 因为没有指定的参数在JavaScript里表示为undefined
,通常显示地为可选参数传入一个undefined
。 这段代码在严格null模式下可以工作:
var x: Example;// When written with overloads, incorrectly an error because of passing 'undefined' to 'string'// When written with optionals, correctly OKx.diff("something", true ? undefined : "hour");
不要为仅在某个位置上的参数类型不同的情况下定义重载:
/* WRONG */interface Moment { utcOffset(): number; utcOffset(b: number): Moment; utcOffset(b: string): Moment;}
应该尽可能使用联合类型:
/* OK */interface Moment { utcOffset(): number; utcOffset(b: number|string): Moment;}
注意我们没有让b
成为可选的,因为签名的返回值类型不同。
为什么:This is important for people who are "passing through" a value to your function:
function fn(x: string): void;function fn(x: number): void;function fn(x: number|string) { // When written with separate overloads, incorrectly an error // When written with union types, correctly OK return moment().utcOffset(x);}
组织模块以提供你想要的API形式保持一致是比较难的。 比如,你可能想要这样一个模块,可以用或不用 new
来创建不同的类型, 在不同层级上暴露出不同的命名类型, 且模块对象上还带有一些属性。
阅读这篇指定后,你就会了解如果书写复杂的暴露出友好API的声明文件。 这篇指定针对于模块(UMD)库,因为它们的选择具有更高的可变性。
如果你理解了一些关于TypeScript是如何工作的核心概念, 那么你就能够为任何结构书写声明文件。
如果你正在阅读这篇指南,你可能已经大概了解TypeScript里的类型指是什么。 明确一下, 类型通过以下方式引入:
type sn = number | string;
)interface I { x: number[]; }
)class C { }
)enum E { A, B, C }
)import
声明以上每种声明形式都会创建一个新的类型名称。
与类型相比,你可能已经理解了什么是值。 值是运行时名字,可以在表达式里引用。 比如 let x = 5;
创建一个名为x
的值。
同样,以下方式能够创建值:
let
,const
,和var
声明namespace
或module
声明enum
声明class
声明import
声明function
声明类型可以存在于命名空间里。 比如,有这样的声明 let x: A.B.C
, 我们就认为 C
类型来自A.B
命名空间。
这个区别虽细微但很重要 -- 这里,A.B
不是必需的类型或值。
一个给定的名字A
,我们可以找出三种不同的意义:一个类型,一个值或一个命名空间。 要如何去解析这个名字要看它所在的上下文是怎样的。 比如,在声明 let m: A.A = A;
, A
首先被当做命名空间,然后做为类型名,最后是值。 这些意义最终可能会指向完全不同的声明!
这看上去另人迷惑,但是只要我们不过度的重载这还是很方便的。 下面让我们来看看一些有用的组合行为。
眼尖的读者可能会注意到,比如,class
同时出现在类型和值列表里。 class C { }
声明创建了两个东西: 类型C
指向类的实例结构, 值C
指向类构造函数。 枚举声明拥有相似的行为。
假设我们写了模块文件foo.d.ts
:
export var SomeVar: { a: SomeType };export interface SomeType { count: number;}
这样使用它:
import * as foo from './foo';let x: foo.SomeType = foo.SomeVar.a;console.log(x.count);
这可以很好地工作,但是我们知道SomeType
和SomeVar
很相关 因此我们想让他们有相同的名字。 我们可以使用组合通过相同的名字 Bar
表示这两种不同的对象(值和对象):
export var Bar: { a: Bar };export interface Bar { count: number;}
这提供了解构使用的机会:
import { Bar } from './foo';let x: Bar = Bar.a;console.log(x.count);
再次地,这里我们使用Bar
做为类型和值。 注意我们没有声明 Bar
值为Bar
类型 -- 它们是独立的。
有一些声明能够通过多个声明组合。 比如, class C { }
和interface C { }
可以同时存在并且都可以做为C
类型的属性。
只要不产生冲突就是合法的。 一个普通的规则是值总是会和同名的其它值产生冲突除非它们在不同命名空间里, 类型冲突则发生在使用类型别名声明的情况下( type s = string
), 命名空间永远不会发生冲突。
让我们看看如何使用。
interface
添加我们可以使用一个interface
往别一个interface
声明里添加额外成员:
interface Foo { x: number;}// ... elsewhere ...interface Foo { y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
这同样作用于类:
class Foo { x: number;}// ... elsewhere ...interface Foo { y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
注意我们不能使用接口往类型别名里添加成员(type s = string;
)
namespace
添加namespace
声明可以用来添加新类型,值和命名空间,只要不出现冲突。
比如,我们可能添加静态成员到一个类:
class C {}// ... elsewhere ...namespace C { export let x: number;}let y = C.x; // OK
注意在这个例子里,我们添加一个值到C
的静态部分(它的构造函数)。 这里因为我们添加了一个 值,且其它值的容器是另一个值 (类型包含于命名空间,命名空间包含于另外的命名空间)。
我们还可以给类添加一个命名空间类型:
class C {}// ... elsewhere ...namespace C { export interface D { }}let y: C.D; // OK
在这个例子里,直到我们写了namespace
声明才有了命名空间C
。 做为命名空间的 C
不会与类创建的值C
或类型C
相互冲突。
最后,我们可以进行不同的合并通过namespace
声明。 Finally, we could perform many different merges usingnamespace
declarations. This isn't a particularly realistic example, but shows all sorts of interesting behavior:
namespace X { export interface Y { } export class Z { }}// ... elsewhere ...namespace X { export var Y: number; export namespace Z { export class C { } }}type X = string;
在这个例子里,第一个代码块创建了以下名字与含义:
X
(因为namespace
声明包含一个值,Z
)X
(因为namespace
声明包含一个值,Z
)X
里的类型Y
X
里的类型Z
(类的实例结构)X
的一个属性值Z
(类的构造函数)第二个代码块创建了以下名字与含义:
Y
(number
类型),它是值X
的一个属性Z
Z
,它是值X
的一个属性X.Z
命名空间下的类型C
X.Z
的一个属性值C
X
export =
或import
一个重要的原则是export
和import
声明会导出或导入目标的所有含义。
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the global-modifying module template file. You should rename it to index.d.ts *~ and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ Note: If your global-modifying module is callable or constructable, you'll *~ need to combine the patterns here with those in the module-class or module-function *~ template files */declare global { /*~ Here, declare things that go in the global namespace, or augment *~ existing declarations in the global namespace */ interface String { fancyFormat(opts: StringFormatOptions): string; }}/*~ If your module exports types or values, write them as usual */export interface StringFormatOptions { fancinessLevel: number;}/*~ For example, declaring a method on the module (in addition to its global side effects) */export function doSomething(): void;/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */export { };
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This template shows how to write a global plugin. *//*~ Write a declaration for the original type and add new members. *~ For example, this adds a 'toBinaryString' method with to overloads to *~ the built-in number type. */interface Number { toBinaryString(opts?: MyLibrary.BinaryFormatOptions): string; toBinaryString(callback: MyLibrary.BinaryFormatCallback, opts?: MyLibrary.BinaryFormatOptions): string;}/*~ If you need to declare several types, place them inside a namespace *~ to avoid adding too many things to the global namespace. */declare namespace MyLibrary { type BinaryFormatCallback = (n: number) => string; interface BinaryFormatOptions { prefix?: string; padding: number; }}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ If this library is callable (e.g. can be invoked as myLib(3)), *~ include those call signatures here. *~ Otherwise, delete this section. */declare function myLib(a: string): string;declare function myLib(a: number): number;/*~ If you want the name of this library to be a valid type name, *~ you can do so here. *~ *~ For example, this allows us to write 'var x: myLib'; *~ Be sure this actually makes sense! If it doesn't, just *~ delete this declaration and add types inside the namespace below. */interface myLib { name: string; length: number; extras?: string[];}/*~ If your library has properties exposed on a global variable, *~ place them here. *~ You should also place types (interfaces and type alias) here. */declare namespace myLib { //~ We can write 'myLib.timeout = 50;' let timeout: number; //~ We can access 'myLib.version', but not change it const version: string; //~ There's some class we can create via 'let c = new myLib.Cat(42)' //~ Or reference e.g. 'function f(c: myLib.Cat) { ... } class Cat { constructor(n: number); //~ We can read 'c.age' from a 'Cat' instance readonly age: number; //~ We can invoke 'c.purr()' from a 'Cat' instance purr(): void; } //~ We can declare a variable as //~ 'var s: myLib.CatSettings = { weight: 5, name: "Maru" };' interface CatSettings { weight: number; name: string; tailLength?: number; } //~ We can write 'const v: myLib.VetID = 42;' //~ or 'const v: myLib.VetID = "bob";' type VetID = string | number; //~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v);' function checkCat(c: Cat, s?: VetID);}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module template file for class modules. *~ You should rename it to index.d.ts and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ Note that ES6 modules cannot directly export class objects. *~ This file should be imported using the CommonJS-style: *~ import x = require('someLibrary'); *~ *~ Refer to the documentation to understand common *~ workarounds for this limitation of ES6 modules. *//*~ If this module is a UMD module that exposes a global variable 'myClassLib' when *~ loaded outside a module loader environment, declare that global here. *~ Otherwise, delete this declaration. */export as namespace myClassLib;/*~ This declaration specifies that the class constructor function *~ is the exported object from the file */export = MyClass;/*~ Write your module's methods and properties in this class */declare class MyClass { constructor(someParam?: string); someProperty: string[]; myMethod(opts: MyClass.MyClassMethodOptions): number;}/*~ If you want to expose types from your module as well, you can *~ place them in this block. */declare namespace MyClass { export interface MyClassMethodOptions { width?: number; height?: number; }}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module template file for function modules. *~ You should rename it to index.d.ts and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ Note that ES6 modules cannot directly export callable functions. *~ This file should be imported using the CommonJS-style: *~ import x = require('someLibrary'); *~ *~ Refer to the documentation to understand common *~ workarounds for this limitation of ES6 modules. *//*~ If this module is a UMD module that exposes a global variable 'myFuncLib' when *~ loaded outside a module loader environment, declare that global here. *~ Otherwise, delete this declaration. */export as namespace myFuncLib;/*~ This declaration specifies that the function *~ is the exported object from the file */export = MyFunction;/*~ This example shows how to have multiple overloads for your function */declare function MyFunction(name: string): MyFunction.NamedReturnType;declare function MyFunction(length: number): MyFunction.LengthReturnType;/*~ If you want to expose types from your module as well, you can *~ place them in this block. Often you will want to describe the *~ shape of the return type of the function; that type should *~ be declared in here, as this example shows. */declare namespace MyFunction { export interface LengthReturnType { width: number; height: number; } export interface NamedReturnType { firstName: string; lastName: string; } /*~ If the module also has properties, declare them here. For example, *~ this declaration says that this code is legal: *~ import f = require('myFuncLibrary'); *~ console.log(f.defaultName); */ export const defaultName: string; export let defaultLength: number;}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module plugin template file. You should rename it to index.d.ts *~ and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ On this line, import the module which this module adds to */import * as m from 'someModule';/*~ You can also import other modules if needed */import * as other from 'anotherModule';/*~ Here, declare the same module as the one you imported above */declare module 'someModule' { /*~ Inside, add new function, classes, or variables. You can use *~ unexported types from the original module if needed. */ export function theNewMethod(x: m.foo): other.bar; /*~ You can also add new properties to existing interfaces from *~ the original module by writing interface augmentations */ export interface SomeModuleOptions { someModuleSetting?: string; } /*~ New types can also be declared and will appear as if they *~ are in the original module */ export interface MyModulePluginOptions { size: number; }}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module template file. You should rename it to index.d.ts *~ and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ If this module is a UMD module that exposes a global variable 'myLib' when *~ loaded outside a module loader environment, declare that global here. *~ Otherwise, delete this declaration. */export as namespace myLib;/*~ If this module has methods, declare them as functions like so. */export function myMethod(a: string): string;export function myOtherMethod(a: number): number;/*~ You can declare types that are available via importing the module */export interface someType { name: string; length: number; extras?: string[];}/*~ You can declare properties of the module using const, let, or var */export const myField: number;/*~ If there are types, properties, or methods inside dotted names *~ of the module, declare them inside a 'namespace'. */export namespace subProp { /*~ For example, given this definition, someone could write: *~ import { subProp } from 'yourModule'; *~ subProp.foo(); *~ or *~ import * as yourMod from 'yourModule'; *~ yourMod.subProp.foo(); */ export function foo(): void;}
现在我们已经按照指南里的步骤写好一个声明文件,是时候把它发布到npm了。 有两种主要方式用来发布声明文件到npm:
如果你能控制要使用你发布的声明文件的那个npm包的话,推荐第一种方式。 这样的话,你的声明文件与JavaScript总是在一起传递。
如果你的包有一个主.js
文件,你还是需要在package.json
里指定主声明文件。 设置 types
属性指向捆绑在一起的声明文件。 比如:
{ "name": "awesome", "author": "Vandelay Industries", "version": "1.0.0", "main": "./lib/main.js", "types": "./lib/main.d.ts"}
注意"typings"
与"types"
具有相同的意义,也可以使用它。
同样要注意的是如果主声明文件名是index.d.ts
并且位置在包的根目录里(与index.js
并列),你就不需要使用"types"
属性指定了。
所有的依赖是由npm管理的。 确保所依赖的声明包都在 package.json
的"dependencies"
里指明了 比如,假设我们写了一个包它依赖于Browserify和TypeScript。
{ "name": "browserify-typescript-extension", "author": "Vandelay Industries", "version": "1.0.0", "main": "./lib/main.js", "types": "./lib/main.d.ts", "dependencies": [ "browserify@latest", "@types/browserify@latest", "typescript@next" ]}
这里,我们的包依赖于browserify
和typescript
包。 browserify
没有把它的声明文件捆绑在它的npm包里,所以我们需要依赖于@types/browserify
得到它的声明文件。 typescript
相反,它把声明文件放在了npm包里,因此我们不需要依赖额外的包。
我们的包要从这两个包里暴露出声明文件,因此browserify-typescript-extension
的用户也需要这些依赖。 正因此,我们使用 "dependencies"
而不是"devDependencies"
,否则用户将需要手动安装那些包。 如果我们只是在写一个命令行应用,并且我们的包不会被当做一个库使用的话,那么我就可以使用devDependencies
。
/// <reference path="..." />
不要在声明文件里使用/// <reference path="..." />
。
/// <reference path="../typescript/lib/typescriptServices.d.ts" />....
应该使用/// <reference types="..." />
代替
/// <reference types="typescript" />....
务必阅读[使用依赖](./Library Structures.md#consuming-dependencies)一节了解详情。
如果你的类型声明依赖于另一个包:
在发布声明文件包之后,确保在DefinitelyTyped外部包列表里面添加一条引用。 这可以让查找工具知道你的包提供了自己的声明文件。
@types下面的包是从DefinitelyTyped里自动发布的,通过 types-publisher工具。 如果想让你的包发布为@types包,提交一个pull request到https://github.com/DefinitelyTyped/DefinitelyTyped。 在这里查看详细信息contribution guidelines page。
在TypeScript 2.0,获取、使用和查找声明文件变得十分容易。 这篇文章将详细说明怎么做这三件事。
在TypeScript 2.0以上的版本,获取类型声明文件只需要使用npm。
比如,获取lodash库的声明文件,只需使用下面的命令:
npm install --save @types/lodash
下载完后,就可以直接在TypeScript里使用lodash了。 不论是在模块里还是全局代码里使用。
比如,你已经npm install
安装了类型声明,你可以使用导入:
import * as _ from "lodash";_.padStart("Hello TypeScript!", 20, " ");
或者如果你没有使用模块,那么你只需使用全局的变量_
。
_.padStart("Hello TypeScript!", 20, " ");
大多数情况下,类型声明包的名字总是与它们在npm
上的包的名字相同,但是有@types/
前缀, 但如果你需要的话,你可以在 https://aka.ms/types这里查找你喜欢的库。
注意:如果你要找的声明文件不存在,你可以贡献一份,这样就方便了下一位要使用它的人。 查看DefinitelyTyped 贡献指南页了解详情。
如果一个目录下存在一个tsconfig.json
文件,那么它意味着这个目录是TypeScript项目的根目录。tsconfig.json
文件中指定了用来编译这个项目的根文件和编译选项。 一个项目可以通过以下方式之一来编译:
tsc
,编译器会从当前目录开始去查找tsconfig.json
文件,逐级向上搜索父目录。tsc
,且使用命令行参数--project
(或-p
)指定一个包含tsconfig.json
文件的目录。当命令行上指定了输入文件时,tsconfig.json
文件会被忽略。
tsconfig.json
示例文件:
"files"
属性{ "compilerOptions": { "module": "commonjs", "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, "outFile": "../../built/local/tsc.js", "sourceMap": true }, "files": [ "core.ts", "sys.ts", "types.ts", "scanner.ts", "parser.ts", "utilities.ts", "binder.ts", "checker.ts", "emitter.ts", "program.ts", "commandLineParser.ts", "tsc.ts", "diagnosticInformationMap.generated.ts" ]}
使用"include"
和"exclude"
属性
{ "compilerOptions": { "module": "commonjs", "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, "outFile": "../../built/local/tsc.js", "sourceMap": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules", "**/*.spec.ts" ]}
"compilerOptions"
可以被忽略,这时编译器会使用默认值。在这里查看完整的[编译器选项](./Compiler Options.md)列表。
"files"
指定一个包含相对或绝对文件路径的列表。 "include"
和"exclude"
属性指定一个文件glob匹配模式列表。 支持的glob通配符有:
*
匹配0或多个字符(不包括目录分隔符)?
匹配一个任意字符(不包括目录分隔符)**/
递归匹配任意子目录如果一个glob模式里的某部分只包含*
或.*
,那么仅有支持的文件扩展名类型被包含在内(比如默认.ts
,.tsx
,和.d.ts
, 如果allowJs
设置能true
还包含.js
和.jsx
)。
如果"files"
和"include"
都没有被指定,编译器默认包含当前目录和子目录下所有的TypeScript文件(.ts
,.d.ts
和 .tsx
),排除在"exclude"
里指定的文件。JS文件(.js
和.jsx
)也被包含进来如果allowJs
被设置成true
。 如果指定了 "files"
或"include"
,编译器会将它们结合一并包含进来。 使用 "outDir"
指定的目录下的文件永远会被编译器排除,除非你明确地使用"files"
将其包含进来(这时就算用exclude
指定也没用)。
使用"include"
引入的文件可以使用"exclude"
属性过滤。 然而,通过 "files"
属性明确指定的文件却总是会被包含在内,不管"exclude"
如何设置。 如果没有特殊指定, "exclude"
默认情况下会排除node_modules
,bower_components
,和jspm_packages
目录。
任何被"files"或
"include"指定的文件所引用的文件也会被包含进来。
A.ts引用了
B.ts,因此
B.ts不能被排除,除非引用它的
A.ts在
"exclude"`列表中。
tsconfig.json
文件可以是个空文件,那么所有默认的文件(如上面所述)都会以默认配置选项编译。
在命令行上指定的编译选项会覆盖在tsconfig.json
文件里的相应选项。
compileOnSave
在最顶层设置compileOnSave
标记,可以让IDE在保存文件的时候根据tsconfig.json
重新生成文件。
{ "compileOnSave": true, "compilerOptions": { "noImplicitAny" : true }}
要想支持这个特性需要Visual Studio 2015, TypeScript1.8.4以上并且安装atom-typescript插件。
到这里查看模式: http://json.schemastore.org/tsconfig.
选项 | 类型 | 默认值 | 描述 |
---|---|---|---|
--allowJs | boolean | true | 允许编译javascript文件。 |
--allowSyntheticDefaultImports | boolean | module === "system" | 允许从没有设置默认导出的模块中默认导入。这并不影响代码的显示,仅为了类型检查。 |
--allowUnreachableCode | boolean | false | 不报告执行不到的代码错误。 |
--allowUnusedLabels | boolean | false | 不报告未使用的标签错误。 |
--baseUrl | string | 解析非相对模块名的基准目录。查看模块解析文档了解详情。 | |
--charset | string | "utf8" | 输入文件的字符集。 |
--declaration -d | boolean | false | 生成相应的'.d.ts'文件。 |
--declarationDir | string | 生成声明文件的输出路径。 | |
--diagnostics | boolean | false | 显示诊断信息。 |
--disableSizeLimit | boolean | false | 禁用JavaScript工程体积大小的限制 |
--emitBOM | boolean | false | 在输出文件的开头加入BOM头(UTF-8 Byte Order Mark)。 |
--emitDecoratorMetadata [1] | boolean | false | 给源码里的装饰器声明加上设计类型元数据。查看issue #2577了解更多信息。 |
--experimentalDecorators [1] | boolean | false | 实验性启用ES7装饰器支持。 |
--forceConsistentCasingInFileNames | boolean | false | 不允许不一致包装引用相同的文件。 |
--help -h | 打印帮助信息。 | ||
--inlineSourceMap | boolean | false | 生成单个sourcemaps文件,而不是将每sourcemaps生成不同的文件。 |
--inlineSources | boolean | false | 将代码与sourcemaps生成到一个文件中,要求同时设置了--inlineSourceMap 或--sourceMap 属性。 |
--init | 初始化TypeScript项目并创建一个tsconfig.json 文件。 | ||
--isolatedModules | boolean | false | 无条件地给没有解析的文件生成imports。 |
--jsx | string | "Preserve" | 在'.tsx'文件里支持JSX:'React' 或 'Preserve'。查看JSX。 |
--lib | string[] | 编译过程中需要引入的库文件的列表。 可能的值为: ► es5 ► es6 ► es2015 ► es7 ► es2016 ► es2017 dom webworker scripthost ► es2015.core ► es2015.collection ► es2015.generator ► es2015.iterable ► es2015.promise ► es2015.proxy ► es2015.reflect ► es2015.symbol ► es2015.symbol.wellknown ► es2016.array.include ► es2017.object ► es2017.sharedmemory 注意:如果 --lib 没有指定默认库。默认库是 ► For --target ES5: dom,es5,scripthost ► For --target ES6: dom,es6,dom.iterable,scripthost | |
--listEmittedFiles | boolean | false | 打印出编译后生成文件的名字。 |
--listFiles | boolean | false | 编译过程中打印文件名。 |
--locale | string | (platform specific) | 显示错误信息时使用的语言,比如:en-us。 |
--mapRoot | string | 为调试器指定指定sourcemap文件的路径,而不是使用生成时的路径。当.map 文件是在运行时指定的,并不同于js 文件的地址时使用这个标记。指定的路径会嵌入到sourceMap 里告诉调试器到哪里去找它们。 | |
--maxNodeModuleJsDepth | number | 0 | node_modules下的最大依赖深度搜索并加载JavaScript文件。仅适用于使用--allowJs 。 |
--module -m | string | target === 'ES6' ? 'ES6' : 'commonjs' | 指定生成哪个模块系统代码:'commonjs','amd','system',或 'umd'或'es2015'。只有'amd'和'system'能和--outFile 一起使用。当目标是ES5或以下的时候不能使用'es2015'。 |
--moduleResolution | string | module === 'amd' | 'system' | 'ES6' ? 'classic' : 'node' | 决定如何处理模块。或者是'node'对于Node.js/io.js,或者是'classic'(默认)。查看模块解析文档了解详情。 |
--newLine | string | (platform specific) | 当生成文件时指定行结束符:'CRLF'(dos)或 'LF' (unix)。 |
--noEmit | boolean | false | 不生成输出文件。 |
--noEmitHelpers | boolean | false | 不在输出文件中生成用户自定义的帮助函数代码,如__extends 。 |
--noEmitOnError | boolean | false | 报错时不生成输出文件。 |
--noFallthroughCasesInSwitch | boolean | false | 报告switch语句的fallthrough错误。(即,不允许switch的case语句贯穿) |
--noImplicitAny | boolean | false | 在表达式和声明上有隐含的'any'类型时报错。 |
--noImplicitReturns | boolean | false | 不是函数的所有返回路径都有返回值时报错。 |
--noImplicitThis | boolean | false | 当this 表达式的值为any 类型的时候,生成一个错误。 |
--noImplicitUseStrict | boolean | false | 模块输出中不包含'use strict'指令。 |
--noLib | boolean | false | 不包含默认的库文件(lib.d.ts)。 |
--noResolve | boolean | false | 不把/// <reference``> 或模块导入的文件加到编译文件列表。 |
--noUnusedLocals | boolean | false | 若有未使用的局部变量则抛错。 |
--noUnusedParameters | boolean | false | 若有未使用的参数则抛错。 |
--out | string | 弃用。使用 --outFile 代替。 | |
--outDir | string | 重定向输出目录。 | |
--outFile | string | 将输出文件合并为一个文件。合并的顺序是根据传入编译器的文件顺序和///<reference``> 和import 的文件顺序决定的。查看输出文件顺序文件了解详情。 | |
paths [2] | Object | 模块名到基于baseUrl 的路径映射的列表。查看模块解析文档了解详情。 | |
--preserveConstEnums | boolean | false | 保留const 和enum 声明。查看const enums documentation了解详情。 |
--pretty [1] | boolean | false | 给错误和消息设置样式,使用颜色和上下文。 |
--project -p | string | 编译指定目录下的项目。这个目录应该包含一个tsconfig.json 文件来管理编译。查看tsconfig.json文档了解更多信息。 | |
--reactNamespace | string | "React" | 当目标为生成'react' JSX时,指定createElement 和__spread 的调用对象 |
--removeComments | boolean | false | 删除所有注释,除了以/!* 开头的版权信息。 |
--rootDir | string | (common root directory is computed from the list of input files) | 仅用来控制输出的目录结构--outDir 。 |
rootDirs [2] | string[] | 根(root)文件夹列表,联给了代表运行时表示工程结构的内容。查看模块解析文档了解详情。 | |
--skipLibCheck | boolean | false | 不检查默认库文件(lib.d.ts )的正确性。 |
--skipDefaultLibCheck | boolean | false | 不检查用户定义的库文件(*.d.ts )的正确性。 |
--sourceMap | boolean | false | 生成相应的'.map'文件。 |
--sourceRoot | string | 指定TypeScript源文件的路径,以便调试器定位。当TypeScript文件的位置是在运行时指定时使用此标记。路径信息会被加到sourceMap 里。 | |
--strictNullChecks | boolean | false | 在严格的null 检查模式下,null 和undefined 值不包含在任何类型里,只允许用它们自己和any 来赋值(有个例外,undefined 可以赋值到void )。 |
--stripInternal [1] | boolean | false | 不对具有/** @internal */ JSDoc注解的代码生成代码。 |
--suppressExcessPropertyErrors [1] | boolean | false | 阻止对对象字面量的额外属性检查。 |
--suppressImplicitAnyIndexErrors | boolean | false | 阻止--noImplicitAny 对缺少索引签名的索引对象报错。查看issue #1232了解详情。 |
--target -t | string | "ES5" | 指定ECMAScript目标版本'ES3' (默认),'ES5',或'ES6'[1] |
--traceResolution | boolean | false | 生成模块解析日志信息 |
--types | string[] | 要包含的类型声明文件名列表。 | |
--typeRoots | string[] | 要包含的类型声明文件路径列表。 | |
--version -v | 打印编译器版本号。 | ||
--watch -w | 在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。 |
tsconfig.json
里使用,不能在命令行使用。编译选项可以在使用MSBuild的项目里通过MSBuild属性指定。
<PropertyGroup Condition="'$(Configuration)' == 'Debug'"> <TypeScriptRemoveComments>false</TypeScriptRemoveComments> <TypeScriptSourceMap>true</TypeScriptSourceMap></PropertyGroup><PropertyGroup Condition="'$(Configuration)' == 'Release'"> <TypeScriptRemoveComments>true</TypeScriptRemoveComments> <TypeScriptSourceMap>false</TypeScriptSourceMap></PropertyGroup><Import Project="$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets" Condition="Exists('$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets')" />
编译选项 | MSBuild属性名称 | 可用值 |
---|---|---|
--declaration | TypeScriptGeneratesDeclarations | 布尔值 |
--module | TypeScriptModuleKind | AMD , CommonJs , UMD 或 System |
--target | TypeScriptTarget | ES3 , ES5 , or ES6 |
--charset | TypeScriptCharset | |
--emitBOM | TypeScriptEmitBOM | 布尔值 |
--emitDecoratorMetadata | TypeScriptEmitDecoratorMetadata | 布尔值 |
--experimentalDecorators | TypeScriptExperimentalDecorators | 布尔值 |
--inlineSourceMap | TypeScriptInlineSourceMap | 布尔值 |
--inlineSources | TypeScriptInlineSources | 布尔值 |
--locale | 自动的 | 自动设置成PreferredUILang的值 |
--mapRoot | TypeScriptMapRoot | 文件路径 |
--newLine | TypeScriptNewLine | CRLF 或 LF |
--noEmitOnError | TypeScriptNoEmitOnError | 布尔值 |
--noEmitHelpers | TypeScriptNoEmitHelpers | 布尔值 |
--noImplicitAny | TypeScriptNoImplicitAny | 布尔值 |
--noUnusedLocals | TypeScriptNoUnusedLocals | 布尔值 |
--noUnusedParameters | TypeScriptNoUnusedParameters | 布尔值 |
--noLib | TypeScriptNoLib | 布尔值 |
--noResolve | TypeScriptNoResolve | 布尔值 |
--out | TypeScriptOutFile | 文件路径 |
--outDir | TypeScriptOutDir | 文件路径 |
--preserveConstEnums | TypeScriptPreserveConstEnums | 布尔值 |
--removeComments | TypeScriptRemoveComments | 布尔值 |
--rootDir | TypeScriptRootDir | 文件路径 |
--isolatedModules | TypeScriptIsolatedModules | 布尔值 |
--sourceMap | TypeScriptSourceMap | 文件路径 |
--sourceRoot | TypeScriptSourceRoot | 文件路径 |
--strictNullChecks | TypeScriptStrictNullChecks | 布尔值 |
--suppressImplicitAnyIndexErrors | TypeScriptSuppressImplicitAnyIndexErrors | 布尔值 |
--suppressExcessPropertyErrors | TypeScriptSuppressExcessPropertyErrors | 布尔值 |
--moduleResolution | TypeScriptModuleResolution | Classic or Node |
--experimentalAsyncFunctions | TypeScriptExperimentalAsyncFunctions | 布尔值 |
--jsx | TypeScriptJSXEmit | React or Preserve |
--reactNamespace | TypeScriptReactNamespace | string |
--skipDefaultLibCheck | TypeScriptSkipDefaultLibCheck | 布尔值 |
--allowUnusedLabels | TypeScriptAllowUnusedLabels | 布尔值 |
--noImplicitReturns | TypeScriptNoImplicitReturns | 布尔值 |
--noFallthroughCasesInSwitch | TypeScriptNoFallthroughCasesInSwitch | 布尔值 |
--allowUnreachableCode | TypeScriptAllowUnreachableCode | 布尔值 |
--forceConsistentCasingInFileNames | TypeScriptForceConsistentCasingInFileNames | 布尔值 |
--allowSyntheticDefaultImports | TypeScriptAllowSyntheticDefaultImports | 布尔值 |
--noImplicitUseStrict | TypeScriptNoImplicitUseStrict | 布尔值 |
--project | VS不支持 | |
--watch | VS不支持 | |
--diagnostics | VS不支持 | |
--listFiles | VS不支持 | |
--noEmit | VS不支持 | |
--allowJs | VS不支持 | |
VS特有选项 | TypeScriptAdditionalFlags | 任意编译选项 |
查找 C:Program Files (x86)MSBuildMicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets
文件。 可用的MSBuild XML标签与相应的tsc
编译选项的映射都在那里。
工程文件里的<TypeScriptToolsVersion>1.7</TypeScriptToolsVersion>
属性值表明了构建时使用的编译器的版本号(这个例子里是1.7) 这样就允许一个工程在不同的机器上使用固定的版本去编译。
如果没有指定TypeScriptToolsVersion
,则会使用机器上安装的最新版本的编译器去构建。
如果用户使用的是更新版本的TypeScript,则会在首次加载工程的时候看到一个提示升级工程的对话框。
如果你使用其它的构建工具(比如,gulp, grunt等等)并且使用VS做为开发和调试工具,那么在工程里设置<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
。 这样VS只会提供给你编辑的功能,而不会在你按F5的时候去构建。
npm install tsify
browserify main.ts -p [ tsify --noImplicitAny ] > bundle.js
var browserify = require("browserify");var tsify = require("tsify");browserify() .add('main.ts') .plugin('tsify', { noImplicitAny: true }) .bundle() .pipe(process.stdout);
更多详细信息:smrq/tsify
npm install duo-typescript
duo --use duo-typescript entry.ts
var Duo = require('duo');var fs = require('fs')var path = require('path')var typescript = require('duo-typescript');var out = path.join(__dirname, "output.js")Duo(__dirname) .entry('entry.ts') .use(typescript()) .run(function (err, results) { if (err) throw err; // Write compiled result to output file fs.writeFileSync(out, results.code); });
更多详细信息:frankwallis/duo-typescript
npm install grunt-ts
module.exports = function(grunt) { grunt.initConfig({ ts: { default : { src: ["**/*.ts", "!node_modules/**/*.ts"] } } }); grunt.loadNpmTasks("grunt-ts"); grunt.registerTask("default", ["ts"]);};
更多详细信息:TypeStrong/grunt-ts
npm install gulp-typescript
var gulp = require("gulp");var ts = require("gulp-typescript");gulp.task("default", function () { var tsResult = gulp.src("src/*.ts") .pipe(ts({ noImplicitAny: true, out: "output.js" })); return tsResult.js.pipe(gulp.dest('built/local'));});
更多详细信息:ivogabe/gulp-typescript
npm install -g jspm@beta
注意:目前jspm的0.16beta版本支持TypeScript
更多详细信息:TypeScriptSamples/jspm
npm install ts-loader --save-dev
module.exports = { entry: "./src/index.tsx", output: { filename: "bundle.js" }, resolve: { // Add '.ts' and '.tsx' as a resolvable extension. extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"] }, module: { loaders: [ // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader' { test: /.tsx?$/, loader: "ts-loader" } ] }};
或者
更新工程文件,包含本地安装的Microsoft.TypeScript.Default.props
(在顶端)和Microsoft.TypeScript.targets
(在底部)文件:
<?xml version="1.0" encoding="utf-8"?><Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <!-- Include default props at the bottom --> <Import Project="$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.Default.props" Condition="Exists('$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.Default.props')" /> <!-- TypeScript configurations go here --> <PropertyGroup Condition="'$(Configuration)' == 'Debug'"> <TypeScriptRemoveComments>false</TypeScriptRemoveComments> <TypeScriptSourceMap>true</TypeScriptSourceMap> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)' == 'Release'"> <TypeScriptRemoveComments>true</TypeScriptRemoveComments> <TypeScriptSourceMap>false</TypeScriptSourceMap> </PropertyGroup> <!-- Include default targets at the bottom --> <Import Project="$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets" Condition="Exists('$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets')" /></Project>
关于配置MSBuild编译器选项的更多详细信息,请参考:[在MSBuild里使用编译选项](./Compiler Options in MSBuild.md)
Microsoft.TypeScript.MSBuild
Install
更多详细信息请参考Package Manager Dialog和using nightly builds with NuGet
在太平洋标准时间每天午夜会自动构建TypeScript的master
分支代码并发布到NPM和NuGet上。下面将介绍如何获得并在工具里使用它们。
npm install -g typescript@next
注意:你需要配置工程来使用NuGet包。详细信息参考 配置MSBuild工程来使用NuGet。
有两个包:
Microsoft.TypeScript.Compiler
:仅包含工具(tsc.exe
,lib.d.ts
,等。)。Microsoft.TypeScript.MSBuild
:和上面一样的工具,还有的MSBuild的任务和目标(Microsoft.TypeScript.targets
, Microsoft.TypeScript.Default.props
。,等)你还可以配置IDE来使用每日构建。首先你要通过npm安装包。你可以进行全局安装或者安装到本地的node_modules
目录下。
下面的步骤里我们假设你已经安装好了typescript@next
。
更新.vscode/settings.json
如下:
"typescript.tsdk": "<path to your folder>/node_modules/typescript/lib"
详细信息参见VSCode文档。
更新Settings - User
如下:
"typescript_tsdk": "<path to your folder>/node_modules/typescript/lib"
详细信息参见如何在Sublime Text里安装TypeScript插件。
注意:大多数的改变不需要你安装新版本的VS TypeScript插件。
当前的每日构建不包含完整的插件安装包,但是我们正在试着提供每日构建的安装包。
下载VSDevMode.ps1脚本。
参考wiki文档:使用自定义语言服务文件。
在PowerShell命令行窗口里执行:
VS 2015:
VSDevMode.ps1 14 -tsScript <path to your folder>/node_modules/typescript/lib
VS 2013:
VSDevMode.ps1 12 -tsScript <path to your folder>/node_modules/typescript/lib
前往Preferences
> Languages & Frameworks
> TypeScript
:
TypeScript Version: 如果通过NPM安装:
/usr/local/lib/node_modules/typescript/lib
为了让程序有价值,我们需要能够处理最简单的数据单元:数字,字符串,结构体,布尔值等。 TypeScript支持与JavaScript几乎相同的数据类型,此外还提供了实用的枚举类型方便我们使用。
最基本的数据类型就是简单的true/false值,在JavaScript和TypeScript里叫做boolean
(其它语言中也一样)。
let isDone: boolean = false;
和JavaScript一样,TypeScript里的所有数字都是浮点数。 这些浮点数的类型是 number
。 除了支持十进制和十六进制字面量,Typescript还支持ECMAScript 2015中引入的二进制和八进制字面量。
let decLiteral: number = 6;let hexLiteral: number = 0xf00d;let binaryLiteral: number = 0b1010;let octalLiteral: number = 0o744;
JavaScript程序的另一项基本操作是处理网页或服务器端的文本数据。 像其它语言里一样,我们使用 string
表示文本数据类型。 和JavaScript一样,可以使用双引号( "
)或单引号('
)表示字符串。
let name: string = "bob";name = "smith";
你还可以使用模版字符串,它可以定义多行文本和内嵌表达式。 这种字符串是被反引号包围( `
),并且以${ expr }
这种形式嵌入表达式
let name: string = `Gene`;let age: number = 37;let sentence: string = `Hello, my name is ${ name }.I'll be ${ age + 1 } years old next month.`;
这与下面定义sentence
的方式效果相同:
let sentence: string = "Hello, my name is " + name + ".
" + "I'll be " + (age + 1) + " years old next month.";
TypeScript像JavaScript一样可以操作数组元素。 有两种方式可以定义数组。 第一种,可以在元素类型后面接上[]
,表示由此类型元素组成的一个数组:
let list: number[] = [1, 2, 3];
第二种方式是使用数组泛型,Array<元素类型>
:
let list: Array<number> = [1, 2, 3];
元组类型允许表示一个已知元素数量和类型的数组,各元素的类型不必相同。 比如,你可以定义一对值分别为string
和number
类型的元组。
// Declare a tuple typelet x: [string, number];// Initialize itx = ['hello', 10]; // OK// Initialize it incorrectlyx = [10, 'hello']; // Error
当访问一个已知索引的元素,会得到正确的类型:
console.log(x[0].substr(1)); // OKconsole.log(x[1].substr(1)); // Error, 'number' does not have 'substr'
当访问一个越界的元素,会使用联合类型替代:
x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型console.log(x[5].toString()); // OK, 'string' 和 'number' 都有 toStringx[6] = true; // Error, 布尔不是(string | number)类型
联合类型是高级主题,我们会在以后的章节里讨论它。
enum
类型是对JavaScript标准数据类型的一个补充。 像C#等其它语言一样,使用枚举类型可以为一组数值赋予友好的名字。
enum Color {Red, Green, Blue};let c: Color = Color.Green;
默认情况下,从0
开始为元素编号。 你也可以手动的指定成员的数值。 例如,我们将上面的例子改成从 1
开始编号:
enum Color {Red = 1, Green, Blue};let c: Color = Color.Green;
或者,全部都采用手动赋值:
enum Color {Red = 1, Green = 2, Blue = 4};let c: Color = Color.Green;
枚举类型提供的一个便利是你可以由枚举的值得到它的名字。 例如,我们知道数值为2,但是不确定它映射到Color里的哪个名字,我们可以查找相应的名字:
enum Color {Red = 1, Green, Blue};let colorName: string = Color[2];alert(colorName);
有时候,我们会想要为那些在编程阶段还不清楚类型的变量指定一个类型。 这些值可能来自于动态的内容,比如来自用户输入或第三方代码库。 这种情况下,我们不希望类型检查器对这些值进行检查而是直接让它们通过编译阶段的检查。 那么我们可以使用 any
类型来标记这些变量:
let notSure: any = 4;notSure = "maybe a string instead";notSure = false; // okay, definitely a boolean
在对现有代码进行改写的时候,any
类型是十分有用的,它允许你在编译时可选择地包含或移除类型检查。 你可能认为 Object
有相似的作用,就像它在其它语言中那样。 但是 Object
类型的变量只是允许你给它赋任意值 - 但是却不能够在它上面调用任意的方法,即便它真的有这些方法:
let notSure: any = 4;notSure.ifItExists(); // okay, ifItExists might exist at runtimenotSure.toFixed(); // okay, toFixed exists (but the compiler doesn't check)let prettySure: Object = 4;prettySure.toFixed(); // Error: Property 'toFixed' doesn't exist on type 'Object'.
当你只知道一部分数据的类型时,any
类型也是有用的。 比如,你有一个数组,它包含了不同的类型的数据:
let list: any[] = [1, true, "free"];list[1] = 100;
某种程度上来说,void
类型像是与any
类型相反,它表示没有任何类型。 当一个函数没有返回值时,你通常会见到其返回值类型是 void
:
function warnUser(): void { alert("This is my warning message");}
声明一个void
类型的变量没有什么大用,因为你只能为它赋予undefined
和null
:
let unusable: void = undefined;
TypeScript里,undefined
和null
两者各自有自己的类型分别叫做undefined
和null
。 和 void
相似,它们的本身的类型用处不是很大:
// Not much else we can assign to these variables!let u: undefined = undefined;let n: null = null;
默认情况下null
和undefined
是所有类型的子类型。 就是说你可以把 null
和undefined
赋值给number
类型的变量。
然而,当你指定了--strictNullChecks
标记,null
和undefined
只能赋值给void
和它们各自。 这能避免很多常见的问题。 也许在某处你想传入一个 string
或null
或undefined
,你可以使用联合类型string | null | undefined
。 再次说明,稍后我们会介绍联合类型。
注意:我们鼓励尽可能地使用
--strictNullChecks
,但在本手册里我们假设这个标记是关闭的。
never
类型表示的是那些永不存在的值的类型。 例如, never
类型是那些总是会抛出异常或根本就不会有返回值的函数表达式或箭头函数表达式的返回值类型; 变量也可能是 never
类型,当它们被永不为真的类型保护所约束时。
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是never
的子类型或可以赋值给never
类型(除了never
本身之外)。 即使 any
也不可以赋值给never
。
下面是一些返回never
类型的函数:
// 返回never的函数必须存在无法达到的终点function error(message: string): never { throw new Error(message);}// 推断的返回值类型为neverfunction fail() { return error("Something failed");}// 返回never的函数必须存在无法达到的终点function infiniteLoop(): never { while (true) { }}
有时候你会遇到这样的情况,你会比TypeScript更了解某个值的详细信息。 通常这会发生在你清楚地知道一个实体具有比它现有类型更确切的类型。
通过类型断言这种方式可以告诉编译器,“相信我,我知道自己在干什么”。 类型断言好比其它语言里的类型转换,但是不进行特殊的数据检查和解构。 它没有运行时的影响,只是在编译阶段起作用。 TypeScript会假设你,程序员,已经进行了必须的检查。
类型断言有两种形式。 其一是“尖括号”语法:
let someValue: any = "this is a string";let strLength: number = (<string>someValue).length;
另一个为as
语法:
let someValue: any = "this is a string";let strLength: number = (someValue as string).length;
两种形式是等价的。 至于使用哪个大多数情况下是凭个人喜好;然而,当你在TypeScript里使用JSX时,只有 as
语法断言是被允许的。
let
你可能已经注意到了,我们使用let
关键字来代替大家所熟悉的JavaScript关键字var
。 let
关键字是JavaScript的一个新概念,TypeScript实现了它。 我们会在以后详细介绍它,很多常见的问题都可以通过使用 let
来解决,所以尽可能地使用let
来代替var
吧。
let
和const
是JavaScript里相对较新的变量声明方式。 像我们之前提到过的, let
在很多方面与var
是相似的,但是可以帮助大家避免在JavaScript里常见一些问题。 const
是对let
的一个增强,它能阻止对一个变量再次赋值。
因为TypeScript是JavaScript的超集,所以它本身就支持let
和const
。 下面我们会详细说明这些新的声明方式以及为什么推荐使用它们来代替 var
。
如果你之前使用JavaScript时没有特别在意,那么这节内容会唤起你的回忆。 如果你已经对 var
声明的怪异之处了如指掌,那么你可以轻松地略过这节。
var
声明一直以来我们都是通过var
关键字定义JavaScript变量。
var a = 10;
大家都能理解,这里定义了一个名为a
值为10
的变量。
我们也可以在函数内部定义变量:
function f() { var message = "Hello, world!"; return message;}
并且我们也可以在其它函数内部访问相同的变量。
function f() { var a = 10; return function g() { var b = a + 1; return b; }}var g = f();g(); // returns 11;
上面的例子里,g
可以获取到f
函数里定义的a
变量。 每当 g
被调用时,它都可以访问到f
里的a
变量。 即使当g
在f
已经执行完后才被调用,它仍然可以访问及修改a
。
function f() { var a = 1; a = 2; var b = g(); a = 3; return b; function g() { return a; }}f(); // returns 2
对于熟悉其它语言的人来说,var
声明有些奇怪的作用域规则。 看下面的例子:
function f(shouldInitialize: boolean) { if (shouldInitialize) { var x = 10; } return x;}f(true); // returns '10'f(false); // returns 'undefined'
有些读者可能要多看几遍这个例子。 变量 x
是定义在*if
语句里面*,但是我们却可以在语句的外面访问它。 这是因为 var
声明可以在包含它的函数,模块,命名空间或全局作用域内部任何位置被访问(我们后面会详细介绍),包含它的代码块对此没有什么影响。 有些人称此为* var
作用域或函数作用域*。 函数参数也使用函数作用域。
这些作用域规则可能会引发一些错误。 其中之一就是,多次声明同一个变量并不会报错:
function sumMatrix(matrix: number[][]) { var sum = 0; for (var i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (var i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum;}
这里很容易看出一些问题,里层的for
循环会覆盖变量i
,因为所有i
都引用相同的函数作用域内的变量。 有经验的开发者们很清楚,这些问题可能在代码审查时漏掉,引发无穷的麻烦。
快速的猜一下下面的代码会返回什么:
for (var i = 0; i < 10; i++) { setTimeout(function() { console.log(i); }, 100 * i);}
介绍一下,setTimeout
会在若干毫秒的延时后执行一个函数(等待其它代码执行完毕)。
好吧,看一下结果:
10101010101010101010
很多JavaScript程序员对这种行为已经很熟悉了,但如果你很不解,你并不是一个人。 大多数人期望输出结果是这样:
0123456789
还记得我们上面讲的变量获取吗?
每当
g
被调用时,它都可以访问到f
里的a
变量。
让我们花点时间考虑在这个上下文里的情况。 setTimeout
在若干毫秒后执行一个函数,并且是在for
循环结束后。 for
循环结束后,i
的值为10
。 所以当函数被调用的时候,它会打印出 10
!
一个通常的解决方法是使用立即执行的函数表达式(IIFE)来捕获每次迭代时i
的值:
for (var i = 0; i < 10; i++) { // capture the current state of 'i' // by invoking a function with its current value (function(i) { setTimeout(function() { console.log(i); }, 100 * i); })(i);}
这种奇怪的形式我们已经司空见惯了。 参数 i
会覆盖for
循环里的i
,但是因为我们起了同样的名字,所以我们不用怎么改for
循环体里的代码。
let
声明现在你已经知道了var
存在一些问题,这恰好说明了为什么用let
语句来声明变量。 除了名字不同外, let
与var
的写法一致。
let hello = "Hello!";
主要的区别不在语法上,而是语义,我们接下来会深入研究。
当用let
声明一个变量,它使用的是词法作用域或块作用域。 不同于使用 var
声明的变量那样可以在包含它们的函数外访问,块作用域变量在包含它们的块或for
循环之外是不能访问的。
function f(input: boolean) { let a = 100; if (input) { // Still okay to reference 'a' let b = a + 1; return b; } // Error: 'b' doesn't exist here return b;}
这里我们定义了2个变量a
和b
。 a
的作用域是f
函数体内,而b
的作用域是if
语句块里。
在catch
语句里声明的变量也具有同样的作用域规则。
try { throw "oh no!";}catch (e) { console.log("Oh well.");}// Error: 'e' doesn't exist hereconsole.log(e);
拥有块级作用域的变量的另一个特点是,它们不能在被声明之前读或写。 虽然这些变量始终“存在”于它们的作用域里,但在直到声明它的代码之前的区域都属于 时间死区。 它只是用来说明我们不能在 let
语句之前访问它们,幸运的是TypeScript可以告诉我们这些信息。
a++; // illegal to use 'a' before it's declared;let a;
注意一点,我们仍然可以在一个拥有块作用域变量被声明前获取它。 只是我们不能在变量声明前去调用那个函数。 如果生成代码目标为ES2015,现代的运行时会抛出一个错误;然而,现今TypeScript是不会报错的。
function foo() { // okay to capture 'a' return a;}// 不能在'a'被声明前调用'foo'// 运行时应该抛出错误foo();let a;
关于时间死区的更多信息,查看这里Mozilla Developer Network.
我们提过使用var
声明时,它不在乎你声明多少次;你只会得到1个。
function f(x) { var x; var x; if (true) { var x; }}
在上面的例子里,所有x
的声明实际上都引用一个相同的x
,并且这是完全有效的代码。 这经常会成为bug的来源。 好的是, let
声明就不会这么宽松了。
let x = 10;let x = 20; // 错误,不能在1个作用域里多次声明`x`
并不是要求两个均是块级作用域的声明TypeScript才会给出一个错误的警告。
function f(x) { let x = 100; // error: interferes with parameter declaration}function g() { let x = 100; var x = 100; // error: can't have both declarations of 'x'}
并不是说块级作用域变量不能在函数作用域内声明。 而是块级作用域变量需要在不用的块里声明。
function f(condition, x) { if (condition) { let x = 100; return x; } return x;}f(false, 0); // returns 0f(true, 0); // returns 100
在一个嵌套作用域里引入一个新名字的行为称做屏蔽。 它是一把双刃剑,它可能会不小心地引入新问题,同时也可能会解决一些错误。 例如,假设我们现在用 let
重写之前的sumMatrix
函数。
function sumMatrix(matrix: number[][]) { let sum = 0; for (let i = 0; i < matrix.length; i++) { var currentRow = matrix[i]; for (let i = 0; i < currentRow.length; i++) { sum += currentRow[i]; } } return sum;}
这个版本的循环能得到正确的结果,因为内层循环的i
可以屏蔽掉外层循环的i
。
通常来讲应该避免使用屏蔽,因为我们需要写出清晰的代码。 同时也有些场景适合利用它,你需要好好打算一下。
在我们最初谈及获取用var
声明的变量时,我们简略地探究了一下在获取到了变量之后它的行为是怎样的。 直观地讲,每次进入一个作用域时,它创建了一个变量的 环境。 就算作用域内代码已经执行完毕,这个环境与其捕获的变量依然存在。
function theCityThatAlwaysSleeps() { let getCity; if (true) { let city = "Seattle"; getCity = function() { return city; } } return getCity();}
因为我们已经在city
的环境里获取到了city
,所以就算if
语句执行结束后我们仍然可以访问它。
回想一下前面setTimeout
的例子,我们最后需要使用立即执行的函数表达式来获取每次for
循环迭代里的状态。 实际上,我们做的是为获取到的变量创建了一个新的变量环境。 这样做挺痛苦的,但是幸运的是,你不必在TypeScript里这样做了。
当let
声明出现在循环体里时拥有完全不同的行为。 不仅是在循环里引入了一个新的变量环境,而是针对 每次迭代都会创建这样一个新作用域。 这就是我们在使用立即执行的函数表达式时做的事,所以在 setTimeout
例子里我们仅使用let
声明就可以了。
for (let i = 0; i < 10 ; i++) { setTimeout(function() {console.log(i); }, 100 * i);}
会输出与预料一致的结果:
0123456789
const
声明const
声明是声明变量的另一种方式。
const numLivesForCat = 9;
它们与let
声明相似,但是就像它的名字所表达的,它们被赋值后不能再改变。 换句话说,它们拥有与 let
相同的作用域规则,但是不能对它们重新赋值。
这很好理解,它们引用的值是不可变的。
const numLivesForCat = 9;const kitty = { name: "Aurora", numLives: numLivesForCat,}// Errorkitty = { name: "Danielle", numLives: numLivesForCat};// all "okay"kitty.name = "Rory";kitty.name = "Kitty";kitty.name = "Cat";kitty.numLives--;
除非你使用特殊的方法去避免,实际上const
变量的内部状态是可修改的。 幸运的是,TypeScript允许你将对象的成员设置成只读的。 接口一章有详细说明。
let
vs. const
现在我们有两种作用域相似的声明方式,我们自然会问到底应该使用哪个。 与大多数泛泛的问题一样,答案是:依情况而定。
使用最小特权原则,所有变量除了你计划去修改的都应该使用const
。 基本原则就是如果一个变量不需要对它写入,那么其它使用这些代码的人也不能够写入它们,并且要思考为什么会需要对这些变量重新赋值。 使用 const
也可以让我们更容易的推测数据的流动。
另一方面,用户很喜欢let
的简洁性。 这个手册大部分地方都使用了 let
。
跟据你的自己判断,如果合适的话,与团队成员商议一下。有趣的是,TypeScript允许您指定对象的成员为只读。关于接口的章节有详细说明。
解构
Another TypeScript已经可以解析其它 ECMAScript 2015 特性了。 完整列表请参见 the article on the Mozilla Developer Network。 本章,我们将给出一个简短的概述。
最简单的解构莫过于数组的解构赋值了:
let input = [1, 2];let [first, second] = input;console.log(first); // outputs 1console.log(second); // outputs 2
这创建了2个命名变量 first
和 second
。 相当于使用了索引,但更为方便:
first = input[0];second = input[1];
解构作用于已声明的变量会更好:
// swap variables[first, second] = [second, first];
作用于函数参数:
function f([first, second]: [number, number]) { console.log(first); console.log(second);}f(input);
你可以使用...name
语法创建一个剩余变量列表:
let [first, ...rest] = [1, 2, 3, 4];console.log(first); // outputs 1console.log(rest); // outputs [ 2, 3, 4 ]
当然,由于是JavaScript, 你可以忽略你不关心的尾随元素:
let [first] = [1, 2, 3, 4];console.log(first); // outputs 1
或其它元素:
let [, second, , fourth] = [1, 2, 3, 4];
你也可以解构对象:
let o = { a: "foo", b: 12, c: "bar"}let {a, b} = o;
这通过 o.a
and o.b
创建了 a
和 b
。 注意,如果你不需要 c
你可以忽略它。
就像数组解构,你可以用没有声明的赋值:
({a, b} = {a: "baz", b: 101});
注意,我们需要用括号将它括起来,因为Javascript通常会将以 {
起始的语句解析为一个块。
你也可以给属性以不同的名字:
let {a: newName1, b: newName2} = o;
这里的语法开始变得混乱。 你可以将 a: newName1
读做 "a
作为 newName1
"。 方向是从左到右,好像你写成了以下样子:
let newName1 = o.a;let newName2 = o.b;
令人困惑的是,这里的冒号不是指示类型的。 如果你想指定它的类型, 仍然需要在其后写上完整的模式。
let {a, b}: {a: string, b: number} = o;
默认值可以让你在属性为 undefined 时使用缺省值:
function keepWholeObject(wholeObject: {a: string, b?: number}) { let {a, b = 1001} = wholeObject;}
现在,即使 b
为 undefined , keepWholeObject
函数的变量 wholeObject
的属性 a
和 b
都会有值。
解构也能用于函数声明。 看以下简单的情况:
type C = {a: string, b?: number}function f({a, b}: C): void { // ...}
但是,通常情况下更多的是指定默认值,解构默认值有些棘手。 首先,你需要知道在设置默认值之前设置其类型。
function f({a, b} = {a: "", b: 0}): void { // ...}f(); // ok, default to {a: "", b: 0}
其次,你需要知道在解构属性上给予一个默认或可选的属性用来替换主初始化列表。 要知道 C
的定义有一个 b
可选属性:
function f({a, b = 0} = {a: ""}): void { // ...}f({a: "yes"}) // ok, default b = 0f() // ok, default to {a: ""}, which then defaults b = 0f({}) // error, 'a' is required if you supply an argument
要小心使用解构。 从前面的例子可以看出,就算是最简单的解构也会有很多问题。 尤其当存在深层嵌套解构的时候,就算这时没有堆叠在一起的重命名,默认值和类型注解,也是令人难以理解的。 解构表达式要尽量保持小而简单。 你自己也可以直接使用解构将会生成的赋值表达式。
TypeScript的核心原则之一是对值所具有的shape进行类型检查。 它有时被称做“鸭式辨型法”或“结构性子类型化”。 在TypeScript里,接口的作用就是为这些类型命名和为你的代码或第三方代码定义契约。
下面通过一个简单示例来观察接口是如何工作的:
function printLabel(labelledObj: { label: string }) { console.log(labelledObj.label);}let myObj = { size: 10, label: "Size 10 Object" };printLabel(myObj);
类型检查器会查看printLabel
的调用。 printLabel
有一个参数,并要求这个对象参数有一个名为label
类型为string
的属性。 需要注意的是,我们传入的对象参数实际上会包含很多属性,但是编译器只会检查那些必需的属性是否存在,并且其类型是否匹配。 然而,有些时候TypeScript却并不会这么宽松,我们下面会稍做讲解。
下面我们重写上面的例子,这次使用接口来描述:必须包含一个label
属性且类型为string
:
interface LabelledValue { label: string;}function printLabel(labelledObj: LabelledValue) { console.log(labelledObj.label);}let myObj = {size: 10, label: "Size 10 Object"};printLabel(myObj);
LabelledValue
接口就好比一个名字,用来描述上面例子里的要求。 它代表了有一个 label
属性且类型为string
的对象。 需要注意的是,我们在这里并不能像在其它语言里一样,说传给 printLabel
的对象实现了这个接口。我们只会去关注值的外形。 只要传入的对象满足上面提到的必要条件,那么它就是被允许的。
还有一点值得提的是,类型检查器不会去检查属性的顺序,只要相应的属性存在并且类型也是对的就可以。
接口里的属性不全都是必需的。 有些是只在某些条件下存在,或者根本不存在。 可选属性在应用“option bags”模式时很常用,即给函数传入的参数对象中只有部分属性赋值了。
下面是应用了“option bags”的例子:
interface SquareConfig { color?: string; width?: number;}function createSquare(config: SquareConfig): {color: string; area: number} { let newSquare = {color: "white", area: 100}; if (config.color) { newSquare.color = config.color; } if (config.width) { newSquare.area = config.width * config.width; } return newSquare;}let mySquare = createSquare({color: "black"});
带有可选属性的接口与普通的接口定义差不多,只是在可选属性名字定义的后面加一个?
符号。
可选属性的好处之一是可以对可能存在的属性进行预定义,好处之二是可以捕获引用了不存在的属性时的错误。 比如,我们故意将 createSquare
里的color
属性名拼错,就会得到一个错误提示:
interface SquareConfig { color?: string; width?: number;}function createSquare(config: SquareConfig): { color: string; area: number } { let newSquare = {color: "white", area: 100}; if (config.color) { // Error: Property 'collor' does not exist on type 'SquareConfig' newSquare.color = config.collor; // Type-checker can catch the mistyped name here } if (config.width) { newSquare.area = config.width * config.width; } return newSquare;}let mySquare = createSquare({color: "black"});
一些对象属性只能在对象刚刚创建的时候修改其值。 你可以在属性名前用 readonly
来指定只读属性:
interface Point { readonly x: number; readonly y: number;}
你可以通过赋值一个对象字面量来构造一个Point
。 赋值后, x
和y
再也不能被改变了。
let p1: Point = { x: 10, y: 20 };p1.x = 5; // error!
TypeScript具有ReadonlyArray<T>
类型,它与Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改:
let a: number[] = [1, 2, 3, 4];let ro: ReadonlyArray<number> = a;ro[0] = 12; // error!ro.push(5); // error!ro.length = 100; // error!a = ro; // error!
上面代码的最后一行,可以看到就算把整个ReadonlyArray
赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:
a = ro as number[];
readonly
vs const
最简单判断该用readonly
还是const
的方法是看要把它做为变量使用还是做为一个属性。 做为变量使用的话用const
,若做为属性则使用readonly
。
我们在第一个例子里使用了接口,TypeScript让我们传入{ size: number; label: string; }
到仅期望得到{ label: string; }
的函数里。 我们已经学过了可选属性,并且知道他们在“option bags”模式里很有用。
然而,天真地将这两者结合的话就会像在JavaScript里那样搬起石头砸自己的脚。 比如,拿 createSquare
例子来说:
interface SquareConfig { color?: string; width?: number;}function createSquare(config: SquareConfig): { color: string; area: number } { // ...}let mySquare = createSquare({ colour: "red", width: 100 });
注意传入createSquare
的参数拼写为*colour
*而不是color
。 在JavaScript里,这会默默地失败。
你可能会争辩这个程序已经正确地类型化了,因为width
属性是兼容的,不存在color
属性,而且额外的colour
属性是无意义的。
然而,TypeScript会认为这段代码可能存在bug。 对象字面量会被特殊对待而且会经过 额外属性检查,当将它们赋值给变量或作为参数传递的时候。 如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
// error: 'colour' not expected in type 'SquareConfig'let mySquare = createSquare({ colour: "red", width: 100 });
绕开这些检查非常简单。 最简便的方法是使用类型断言:
let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig);
然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。 如果 SquareConfig
带有上面定义的类型的color
和width
属性,并且还会带有任意数量的其它属性,那么我们可以这样定义它:
interface SquareConfig { color?: string; width?: number; [propName: string]: any;}
我们稍后会讲到索引签名,但在这我们要表示的是SquareConfig
可以有任意数量的属性,并且只要它们不是color
和width
,那么就无所谓它们的类型是什么。
还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量: 因为squareOptions
不会经过额外属性检查,所以编译器不会报错。
let squareOptions = { colour: "red", width: 100 };let mySquare = createSquare(squareOptions);
要留意,在像上面一样的简单代码里,你可能不应该去绕开这些检查。 对于包含方法和内部状态的复杂对象字面量来讲,你可能需要使用这些技巧,但是大部额外属性检查错误是真正的bug。 就是说你遇到了额外类型检查出的错误,比如选择包,你应该去审查一下你的类型声明。 在这里,如果支持传入 color
或colour
属性到createSquare
,你应该修改SquareConfig
定义来体现出这一点。
接口能够描述JavaScript中对象拥有的各种各样的外形。 除了描述带有属性的普通对象外,接口也可以描述函数类型。
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
interface SearchFunc { (source: string, subString: string): boolean;}
这样定义后,我们可以像使用其它接口一样使用这个函数类型的接口。 下例展示了如何创建一个函数类型的变量,并将一个同类型的函数赋值给这个变量。
let mySearch: SearchFunc;mySearch = function(source: string, subString: string) { let result = source.search(subString); if (result == -1) { return false; } else { return true; }}
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配。 比如,我们使用下面的代码重写上面的例子:
let mySearch: SearchFunc;mySearch = function(src: string, sub: string): boolean { let result = src.search(sub); if (result == -1) { return false; } else { return true; }}
函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。 如果你不想指定类型,Typescript的类型系统会推断出参数类型,因为函数直接赋值给了 SearchFunc
类型变量。 函数的返回值类型是通过其返回值推断出来的(此例是 false
和true
)。 如果让这个函数返回数字或字符串,类型检查器会警告我们函数的返回值类型与SearchFunc
接口中的定义不匹配。
let mySearch: SearchFunc;mySearch = function(src, sub) { let result = src.search(sub); if (result == -1) { return false; } else { return true; }}
与使用接口描述函数类型差不多,我们也可以描述那些能够“通过索引得到”的类型,比如a[10]
或ageMap["daniel"]
。 可索引类型具有一个 索引签名,它描述了对象索引的类型,还有相应的索引返回值类型。 让我们看一个例子:
interface StringArray { [index: number]: string;}let myArray: StringArray;myArray = ["Bob", "Fred"];let myStr: string = myArray[0];
上面例子里,我们定义了StringArray
接口,它具有索引签名。 这个索引签名表示了当用 number
去索引StringArray
时会得到string
类型的返回值。
共有支持两种索引签名:字符串和数字。 可以同时使用两种类型的索引,但是数字索引的返回值必须是字符串索引返回值类型的子类型。 这是因为当使用 number
来索引时,JavaScript会将它转换成string
然后再去索引对象。 也就是说用 100
(一个number
)去索引等同于使用"100"
(一个string
)去索引,因此两者需要保持一致。
class Animal { name: string;}class Dog extends Animal { breed: string;}// Error: indexing with a 'string' will sometimes get you a Dog!interface NotOkay { [x: number]: Animal; [x: string]: Dog;}
字符串索引签名能够很好的描述dictionary
模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property
和obj["property"]
两种形式都可以。 下面的例子里, name
的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
interface NumberDictionary { [index: string]: number; length: number; // 可以,length是number类型 name: string // 错误,`name`的类型不是索引类型的子类型}
最后,你可以将索引签名设置为只读,这样就防止了给索引赋值:
interface ReadonlyStringArray { readonly [index: number]: string;}let myArray: ReadonlyStringArray = ["Alice", "Bob"];myArray[2] = "Mallory"; // error!
你不能设置myArray[2]
,因为索引签名是只读的。
与C#或Java里接口的基本作用一样,TypeScript也能够用它来明确的强制一个类去符合某种契约。
interface ClockInterface { currentTime: Date;}class Clock implements ClockInterface { currentTime: Date; constructor(h: number, m: number) { }}
你也可以在接口中描述一个方法,在类里实现它,如同下面的setTime
方法一样:
interface ClockInterface { currentTime: Date; setTime(d: Date);}class Clock implements ClockInterface { currentTime: Date; setTime(d: Date) { this.currentTime = d; } constructor(h: number, m: number) { }}
接口描述了类的公共部分,而不是公共和私有两部分。 它不会帮你检查类是否具有某些私有成员。
当你操作类和接口的时候,你要知道类是具有两个类型的:静态部分的类型和实例的类型。 你会注意到,当你用构造器签名去定义一个接口并试图定义一个类去实现这个接口时会得到一个错误:
interface ClockConstructor { new (hour: number, minute: number);}class Clock implements ClockConstructor { currentTime: Date; constructor(h: number, m: number) { }}
这里因为当一个类实现了一个接口时,只对其实例部分进行类型检查。 constructor存在于类的静态部分,所以不在检查的范围内。
因此,我们应该直接操作类的静态部分。 看下面的例子,我们定义了两个接口, ClockConstructor
为构造函数所用和ClockInterface
为实例方法所用。 为了方便我们定义一个构造函数 createClock
,它用传入的类型创建实例。
interface ClockConstructor { new (hour: number, minute: number): ClockInterface;}interface ClockInterface { tick();}function createClock(ctor: ClockConstructor, hour: number, minute: number): ClockInterface { return new ctor(hour, minute);}class DigitalClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("beep beep"); }}class AnalogClock implements ClockInterface { constructor(h: number, m: number) { } tick() { console.log("tick tock"); }}let digital = createClock(DigitalClock, 12, 17);let analog = createClock(AnalogClock, 7, 32);
因为createClock
的第一个参数是ClockConstructor
类型,在createClock(AnalogClock, 7, 32)
里,会检查AnalogClock
是否符合构造函数签名。
和类一样,接口也可以相互扩展。 这让我们能够从一个接口里复制成员到另一个接口里,可以更灵活地将接口分割到可重用的模块里。
interface Shape { color: string;}interface Square extends Shape { sideLength: number;}let square = <Square>{};square.color = "blue";square.sideLength = 10;
一个接口可以继承多个接口,创建出多个接口的合成接口。
interface Shape { color: string;}interface PenStroke { penWidth: number;}interface Square extends Shape, PenStroke { sideLength: number;}let square = <Square>{};square.color = "blue";square.sideLength = 10;square.penWidth = 5.0;
先前我们提过,接口能够描述JavaScript里丰富的类型。 因为JavaScript其动态灵活的特点,有时你会希望一个对象可以同时具有上面提到的多种类型。
一个例子就是,一个接口可以同时做为函数和对象使用,并带有额外的属性。
interface Counter { (start: number): string; interval: number; reset(): void;}function getCounter(): Counter { let counter = <Counter>function (start: number) { }; counter.interval = 123; counter.reset = function () { }; return counter;}let c = getCounter();c(10);c.reset();c.interval = 5.0;
在使用JavaScript第三方库的时候,你可能需要像上面那样去完整地定义类型。
当接口继承了一个类类型时,它会继承类的成员但不包括其实现。 就好像接口声明了所有类中存在的成员,但并没有提供具体实现一样。 接口同样会继承到类的private和protected成员。 这意味着当你创建了一个接口继承了一个拥有私有或受保护的成员的类时,这个接口类型只能被这个类或其子类所实现(implement)。
这是很有用的,当你有一个很深层次的继承,但是只想你的代码只是针对拥有特定属性的子类起作用的时候。子类除了继承自基类外与基类没有任何联系。 例:
class Control { private state: any;}interface SelectableControl extends Control { select(): void;}class Button extends Control implements SelectableControl {
select() { }
}
class TextBox extends Control {
select() { }
}
// 错误:“Image”类型缺少“state”属性。
class Image implements SelectableControl {
select() { }
}
class Location {
}
在上面的例子里,SelectableControl
包含了Control
的所有成员,包括私有成员state
。 因为 state
是私有成员,所以只能够是Control
的子类们才能实现SelectableControl
接口。 因为只有 Control
的子类才能够拥有一个声明于Control
的私有成员state
,这对私有成员的兼容性是必需的。
在Control
类内部,是允许通过SelectableControl
的实例来访问私有成员state
的。 实际上,SelectableControl
就像Control
一样,并拥有一个select
方法。 Button
和TextBox
类是SelectableControl
的子类(因为它们都继承自Control
并有select
方法),但Image
和Location
类并不是这样的。
传统的JavaScript的程序使用函数和基于原型的继承来创建可重用的组件,但对于熟悉使用面向对象方式的程序员来讲就有些棘手,因为他们用的是基于类的继承并且对象是由类构建出来从ECMAScript 2015,也就是ECMAScript 6开始,JavaScript程序员将能够使用基于类的面向对象的方式。使用TypeScript,我们允许开发者现在就使用这些特性,并且编译后的JavaScript可以在所有主流浏览器和平台上运行,而不需要等到下个JavaScript的版本。
下面看一个使用类的例子:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; }}let greeter = new Greeter("world");
如果你使用过C#或Java,你会对这种语法非常熟悉。我们声明一个 Greeter
类。这个类有3个成员:一个叫做greeting
的属性,一个构造函数和一个greet
方法。
你会注意到,我们在引用任何一个类成员的时候都用了this
。它表示我们访问的是类的成员。
最后一行,我们使用new
构造了Greeter
类的一个实例。它会调用之前定义的构造函数,创建一个 Greeter
类型的新对象,并执行构造函数初始化它。
在TypeScript里,我们可以使用常用的面向对象模式。当然,基于类的程序设计中最基本的模式是允许使用继承来扩展现有的类。
看下面的例子:
class Animal { name:string; constructor(theName: string) { this.name = theName; } move(distanceInMeters: number = 0) { console.log(`${this.name} moved ${distanceInMeters}m.`); }}class Snake extends Animal { constructor(name: string) { super(name); } move(distanceInMeters = 5) { console.log("Slithering..."); super.move(distanceInMeters); }}class Horse extends Animal { constructor(name: string) { super(name); } move(distanceInMeters = 45) { console.log("Galloping..."); super.move(distanceInMeters); }}let sam = new Snake("Sammy the Python");let tom: Animal = new Horse("Tommy the Palomino");sam.move();tom.move(34);
这个例子展示了TypeScript中继承的一些特征,它们与其它语言类似。 我们使用 extends
关键字来创建子类。你可以看到Horse
和Snake
类是基类Animal
的子类,并且可以访问其属性和方法。
包含构造函数的派生类必须调用super()
,它会执行基类的构造方法。
这个例子演示了如何在子类里可以重写父类的方法。 Snake
类和Horse
类都创建了move
方法,它们重写了从Animal
继承来的move
方法,使得move
方法根据不同的类而具有不同的功能。 注意,即使 tom
被声明为Animal
类型,但因为它的值是Horse
,tom.move(34)
会调用Horse
里的重写方法:
Slithering...Sammy the Python moved 5m.Galloping...Tommy the Palomino moved 34m.
public
在上面的例子里,我们可以自由的访问程序里定义的成员。 如果你对其它语言中的类比较了解,就会注意到我们在之前的代码里并没有使用 public
来做修饰;例如,C#要求必须明确地使用public
指定成员是可见的。 在TypeScript里,成员都默认为 public
。
你也可以明确的将一个成员标记成public
。 我们可以用下面的方式来重写上面的 Animal
类:
class Animal { public name: string; public constructor(theName: string) { this.name = theName; } public move(distanceInMeters: number) { console.log(`${this.name} moved ${distanceInMeters}m.`); }}
private
当成员被标记成private
时,它就不能在声明它的类的外部访问。比如:
class Animal { private name: string; constructor(theName: string) { this.name = theName; }}new Animal("Cat").name; // Error: 'name' is private;
TypeScript使用的是结构性类型系统。 当我们比较两种不同的类型时,并不在乎它们从何处而来,如果所有成员的类型都是兼容的,我们就认为它们的类型是兼容的。
然而,当我们比较带有private
或protected
成员的类型的时候,情况就不同了。 如果其中一个类型里包含一个 private
成员,那么只有当另外一个类型中也存在这样一个private
成员, 并且它们都是来自同一处声明时,我们才认为这两个类型是兼容的。 对于 protected
成员也使用这个规则。
下面来看一个例子,更好地说明了这一点:
class Animal { private name: string; constructor(theName: string) { this.name = theName; }}class Rhino extends Animal { constructor() { super("Rhino"); }}class Employee { private name: string; constructor(theName: string) { this.name = theName; }}let animal = new Animal("Goat");let rhino = new Rhino();let employee = new Employee("Bob");animal = rhino;animal = employee; // Error: Animal and Employee are not compatible
这个例子中有Animal
和Rhino
两个类,Rhino
是Animal
类的子类。 还有一个 Employee
类,其类型看上去与Animal
是相同的。 我们创建了几个这些类的实例,并相互赋值来看看会发生什么。 因为 Animal
和Rhino
共享了来自Animal
里的私有成员定义private name: string
,因此它们是兼容的。 然而 Employee
却不是这样。当把Employee
赋值给Animal
的时候,得到一个错误,说它们的类型不兼容。 尽管 Employee
里也有一个私有成员name
,但它明显不是Animal
里面定义的那个。
protected
protected
修饰符与private
修饰符的行为很相似,但有一点不同,protected
成员在派生类中仍然可以访问。例如:
class Person { protected name: string; constructor(name: string) { this.name = name; }}class Employee extends Person { private department: string; constructor(name: string, department: string) { super(name) this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; }}let howard = new Employee("Howard", "Sales");console.log(howard.getElevatorPitch());console.log(howard.name); // error
注意,我们不能在Person
类外使用name
,但是我们仍然可以通过Employee
类的实例方法访问,因为Employee
是由Person
派生而来的。
构造函数也可以被标记成protected
。 这意味着这个类不能在包含它的类外被实例化,但是能被继承。比如,
class Person { protected name: string; protected constructor(theName: string) { this.name = theName; }}// Employee can extend Personclass Employee extends Person { private department: string; constructor(name: string, department: string) { super(name); this.department = department; } public getElevatorPitch() { return `Hello, my name is ${this.name} and I work in ${this.department}.`; }}let howard = new Employee("Howard", "Sales");let john = new Person("John"); // Error: The 'Person' constructor is protected
你可以使用readonly
关键字将属性设置为只读的。 只读属性必须在声明时或构造函数里被初始化。
class Octopus { readonly name: string; readonly numberOfLegs: number = 8; constructor (theName: string) { this.name = theName; }}let dad = new Octopus("Man with the 8 strong legs");dad.name = "Man with the 3-piece suit"; // error! name is readonly.
在上面的例子中,我们不得不定义一个受保护的成员name
和一个构造函数参数theName
在Person
类里,并且立刻给name
和theName
赋值。 这种情况经常会遇到。 参数属性可以方便地让我们在一个地方定义并初始化一个成员。 下面的例子是对之前 Animal
类的修改版,使用了参数属性:
class Animal { constructor(private name: string) { } move(distanceInMeters: number) { console.log(`${this.name} moved ${distanceInMeters}m.`); }}
注意看我们是如何舍弃了theName
,仅在构造函数里使用private name: string
参数来创建和初始化name
成员。 我们把声明和赋值合并至一处。
参数属性通过给构造函数参数添加一个访问限定符来声明。 使用 private
限定一个参数属性会声明并初始化一个私有成员;对于public
和protected
来说也是一样。
TypeScript支持通过getters/setters来截取对对象成员的访问。 它能帮助你有效的控制对对象成员的访问。
下面来看如何把一个简单的类改写成使用get
和set
。 首先,我们从一个没有使用存取器的例子开始。
class Employee { fullName: string;}let employee = new Employee();employee.fullName = "Bob Smith";if (employee.fullName) { console.log(employee.fullName);}
我们可以随意的设置fullName
,这是非常方便的,但是这也可能会带来麻烦。
下面这个版本里,我们先检查用户密码是否正确,然后再允许其修改员工信息。 我们把对 fullName
的直接访问改成了可以检查密码的set
方法。 我们也加了一个 get
方法,让上面的例子仍然可以工作。
let passcode = "secret passcode";class Employee { private _fullName: string; get fullName(): string { return this._fullName; } set fullName(newName: string) { if (passcode && passcode == "secret passcode") { this._fullName = newName; } else { console.log("Error: Unauthorized update of employee!"); } }}let employee = new Employee();employee.fullName = "Bob Smith";if (employee.fullName) { alert(employee.fullName);}
我们可以修改一下密码,来验证一下存取器是否是工作的。当密码不对时,会提示我们没有权限去修改员工。
对于存取器有下面几点需要注意的:
首先,存取器要求你将编译器设置为输出ECMAScript 5或更高。 不支持降级到ECMAScript 3。 其次,只带有 get
不带有set
的存取器自动被推断为readonly
。 这在从代码生成 .d.ts
文件时是有帮助的,因为利用这个属性的用户会看到不允许够改变它的值。
到目前为止,我们只讨论了类的实例成员,那些仅当类被实例化的时候才会被初始化的属性。 我们也可以创建类的静态成员,这些属性存在于类本身上面而不是类的实例上。 在这个例子里,我们使用 static
定义origin
,因为它是所有网格都会用到的属性。 每个实例想要访问这个属性的时候,都要在 origin
前面加上类名。 如同在实例属性上使用 this.
前缀来访问属性一样,这里我们使用Grid.
来访问静态属性。
class Grid { static origin = {x: 0, y: 0}; calculateDistanceFromOrigin(point: {x: number; y: number;}) { let xDist = (point.x - Grid.origin.x); let yDist = (point.y - Grid.origin.y); return Math.sqrt(xDist * xDist + yDist * yDist) / this.scale; } constructor (public scale: number) { }}let grid1 = new Grid(1.0); // 1x scalelet grid2 = new Grid(5.0); // 5x scaleconsole.log(grid1.calculateDistanceFromOrigin({x: 10, y: 10}));console.log(grid2.calculateDistanceFromOrigin({x: 10, y: 10}));
抽象类做为其它派生类的基类使用。 它们一般不会直接被实例化。 不同于接口,抽象类可以包含成员的实现细节。abstract
关键字是用于定义抽象类和在抽象类内部定义抽象方法。
abstract class Animal { abstract makeSound(): void; move(): void { console.log('roaming the earch...'); }}
抽象类中的抽象方法不包含具体实现并且必须在派生类中实现。 抽象方法的语法与接口方法相似。 两者都是定义方法签名但不包含方法体。 然而,抽象方法必须包含 abstract
关键字并且可以包含访问修饰符。
abstract class Department { constructor(public name: string) { } printName(): void { console.log('Department name: ' + this.name); } abstract printMeeting(): void; // 必须在派生类中实现}class AccountingDepartment extends Department { constructor() { super('Accounting and Auditing'); // constructors in derived classes must call super() } printMeeting(): void { console.log('The Accounting Department meets each Monday at 10am.'); } generateReports(): void { console.log('Generating accounting reports...'); }}let department: Department; // ok to create a reference to an abstract typedepartment = new Department(); // error: cannot create an instance of an abstract classdepartment = new AccountingDepartment(); // ok to create and assign a non-abstract subclassdepartment.printName();department.printMeeting();department.generateReports(); // error: method doesn't exist on declared abstract type
当你在TypeScript里声明了一个类的时候,实际上同时声明了很多东西。 首先就是类的 实例的类型。
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; }}let greeter: Greeter;greeter = new Greeter("world");console.log(greeter.greet());
这里,我们写了let greeter: Greeter
,意思是Greeter
类的实例的类型是Greeter
。 这对于用过其它面向对象语言的程序员来讲已经是老习惯了。
我们也创建了一个叫做构造函数的值。这个函数会在我们使用 new
创建类实例的时候被调用。下面我们来看看,上面的代码被编译成JavaScript后是什么样子的:
let Greeter = (function () { function Greeter(message) { this.greeting = message; } Greeter.prototype.greet = function () { return "Hello, " + this.greeting; }; return Greeter;})();let greeter;greeter = new Greeter("world");console.log(greeter.greet());
上面的代码里,let Greeter
将被赋值为构造函数。我们当调用 new
并执行了这个函数后,便会得到一个类的实例。这个构造函数也包含了类的所有静态属性。换个角度说,我们可以认为类具有 实例部分与静态部分这两个部分。
让我们稍微改写一下这个例子,看看它们之前的区别:
class Greeter { static standardGreeting = "Hello, there"; greeting: string; greet() { if (this.greeting) { return "Hello, " + this.greeting; } else { return Greeter.standardGreeting; } }}let greeter1: Greeter;greeter1 = new Greeter();console.log(greeter1.greet());let greeterMaker: typeof Greeter = Greeter;greeterMaker.standardGreeting = "Hey there!";let greeter2: Greeter = new greeterMaker();console.log(greeter2.greet());
这个例子里,greeter1
与之前看到的一样。我们实例化 Greeter
类,并使用这个对象。与我们之前看到的一样。
再之后,我们直接使用类。我们创建³³了一个叫做 greeterMaker
的变量。这个变量保存了这个类或者说保存了类构造函数。然后我们使用 typeof Greeter
,意思是取Greeter类的类型,而不是实例的类型。或者更确切的说, “我告诉 Greeter
标识符的类型”,也就是构造函数的类型。这个类型包含了类的所有静态成员和构造函数。之后,就和前面一样,在我们 greeterMaker
上使用new
,创建³³ Greeter
的实例。
如上一节里所讲的,类定义会创建两个东西:类的实例类型和一个构造函数。因为类可以创建出类型,所以你能够在允许使用接口的地方使用类。
class Point { x: number; y: number;}interface Point3d extends Point { z: number;}let point3d: Point3d = {x: 1, y: 2, z: 3};
函数是JavaScript应用程序的基础。 它帮助你实现抽象层,模拟类,信息隐藏和模块。 在TypeScript里,虽然已经支持类,命名空间和模块,但函数仍然是主要的定义 行为的地方。 TypeScript为JavaScript函数添加了额外的功能,让我们可以更容易地使用。
和JavaScript一样,TypeScript函数可以创建有名字的函数和匿名函数。 你可以随意选择适合应用程序的方式,不论是定义一系列API函数还是只使用一次的函数。
通过下面的例子可以迅速回想起这两种JavaScript中的函数:
// Named functionfunction add(x, y) { return x + y;}// Anonymous functionlet myAdd = function(x, y) { return x + y; };
在JavaScript里,函数可以使用函数体外部的变量。 当函数这么做时,我们说它‘捕获’了这些变量。 至于为什么可以这样做以及其中的利弊超出了本文的范围,但是深刻理解这个机制对学习JavaScript和TypeScript会很有帮助。
let z = 100;function addToZ(x, y) { return x + y + z;}
让我们为上面那个函数添加类型:
function add(x: number, y: number): number { return x + y;}let myAdd = function(x: number, y: number): number { return x+y; };
我们可以给每个参数添加类型之后再为函数本身添加返回值类型。 TypeScript能够根据返回语句自动推断出返回值类型,因此我们通常省略它。
现在我们已经为函数指定了类型,下面让我们写出函数的完整类型。
let myAdd: (x:number, y:number)=>number = function(x: number, y: number): number { return x+y; };
函数类型包含两部分:参数类型和返回值类型。 当写出完整函数类型的时候,这两部分都是需要的。 我们以参数列表的形式写出参数类型,为每个参数指定一个名字和类型。 这个名字只是为了增加可读性。 我们也可以这么写:
let myAdd: (baseValue:number, increment:number) => number = function(x: number, y: number): number { return x + y; };
只要参数类型是匹配的,那么就认为它是有效的函数类型,而不在乎参数名是否正确。
第二部分是返回值类型。 对于返回值,我们在函数和返回值类型之前使用( =>
)符号,使之清晰明了。 如之前提到的,返回值类型是函数类型的必要部分,如果函数没有返回任何值,你也必须指定返回值类型为 void
而不能留空。
函数的类型只是由参数类型和返回值组成的。 函数中使用的捕获变量不会体现在类型里。 实际上,这些变量是函数的隐藏状态并不是组成API的一部分。
尝试这个例子的时候,你会发现如果你在赋值语句的一边指定了类型但是另一边没有类型的话,TypeScript编译器会自动识别出类型:
// myAdd has the full function typelet myAdd = function(x: number, y: number): number { return x + y; };// The parameters `x` and `y` have the type numberlet myAdd: (baseValue:number, increment:number) => number = function(x, y) { return x + y; };
这叫做“按上下文归类”,是类型推论的一种。 它帮助我们更好地为程序指定类型。
TypeScript里的每个函数参数都是必须的。 这不是指不能传递 null
或undefined
作为参数,而是说编译器检查用户是否为每个参数都传入了值。 编译器还会假设只有这些参数会被传递进函数。 简短地说,传递给一个函数的参数个数必须与函数期望的参数个数一致。
function buildName(firstName: string, lastName: string) { return firstName + " " + lastName;}let result1 = buildName("Bob"); // error, too few parameterslet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result3 = buildName("Bob", "Adams"); // ah, just right
JavaScript里,每个参数都是可选的,可传可不传。 没传参的时候,它的值就是undefined。 在TypeScript里我们可以在参数名旁使用 ?
实现可选参数的功能。 比如,我们想让last name是可选的:
function buildName(firstName: string, lastName?: string) { if (lastName) return firstName + " " + lastName; else return firstName;}let result1 = buildName("Bob"); // works correctly nowlet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result3 = buildName("Bob", "Adams"); // ah, just right
可选参数必须跟在必须参数后面。 如果上例我们想让first name是可选的,那么就必须调整它们的位置,把first name放在后面。
在TypeScript里,我们也可以为参数提供一个默认值当用户没有传递这个参数或传递的值是undefined
时。 它们叫做有默认初始化值的参数。 让我们修改上例,把last name的默认值设置为"Smith"
。
function buildName(firstName: string, lastName = "Smith") { return firstName + " " + lastName;}let result1 = buildName("Bob"); // works correctly now, returns "Bob Smith"let result2 = buildName("Bob", undefined); // still works, also returns "Bob Smith"let result3 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result4 = buildName("Bob", "Adams"); // ah, just right
在所有必须参数后面的带默认初始化的参数都是可选的,与可选参数一样,在调用函数的时候可以省略。 也就是说可选参数与末尾的默认参数共享参数类型。
function buildName(firstName: string, lastName?: string) { // ...}
和
function buildName(firstName: string, lastName = "Smith") { // ...}
共享同样的类型(firstName: string, lastName?: string) => string
。 默认参数的默认值消失了,只保留了它是一个可选参数的信息。
与普通可选参数不同的是,带默认值的参数不需要放在必须参数的后面。 如果带默认值的参数出现在必须参数前面,用户必须明确的传入 undefined
值来获得默认值。 例如,我们重写最后一个例子,让 firstName
是带默认值的参数:
function buildName(firstName = "Will", lastName: string) { return firstName + " " + lastName;}let result1 = buildName("Bob"); // error, too few parameterslet result2 = buildName("Bob", "Adams", "Sr."); // error, too many parameterslet result3 = buildName("Bob", "Adams"); // okay and returns "Bob Adams"let result4 = buildName(undefined, "Adams"); // okay and returns "Will Adams"
必要参数,默认参数和可选参数有个共同点:它们表示某一个参数。 有时,你想同时操作多个参数,或者你并不知道会有多少参数传递进来。 在JavaScript里,你可以使用 arguments
来访问所有传入的参数。
在TypeScript里,你可以把所有参数收集到一个变量里:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" ");}let employeeName = buildName("Joseph", "Samuel", "Lucas", "MacKinzie");
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。 编译器创建参数数组,名字是你在省略号( ...
)后面给定的名字,你可以在函数体内使用这个数组。
这个省略号也会在带有剩余参数的函数类型定义上使用到:
function buildName(firstName: string, ...restOfName: string[]) { return firstName + " " + restOfName.join(" ");}let buildNameFun: (fname: string, ...rest: string[]) => string = buildName;
this
学习使用JavaScript里this
就好比一场成年礼。 由于TypeScript是JavaScript的超集,TypeScript程序员也需要弄清this
工作机制并且当有bug的时候能够找出错误所在。 幸运的是,TypeScript能通知你错误地使用了 this
的地方。 如果你想了解JavaScript里的 this
是如何工作的,那么首先阅读Yehuda Katz写的Understanding JavaScript Function Invocation and "this"。 Yehuda的文章详细的阐述了 this
的内部工作原理,因此我们这里只做简单介绍。
this
和箭头函数JavaScript里,this
的值在函数被调用的时候才会指定。 这是个既强大又灵活的特点,但是你需要花点时间弄清楚函数调用的上下文是什么。 但众所周知,这不是一件很简单的事,尤其是在返回一个函数或将函数当做参数传递的时候。
下面看一个例子:
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { return function() { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }}let cardPicker = deck.createCardPicker();let pickedCard = cardPicker();alert("card: " + pickedCard.card + " of " + pickedCard.suit);
可以看到createCardPicker
是个函数,并且它又返回了一个函数。 如果我们尝试运行这个程序,会发现它并没有弹出对话框而是报错了。 因为 createCardPicker
返回的函数里的this
被设置成了window
而不是deck
对象。 因为我们只是独立的调用了 cardPicker()
。 顶级的非方法式调用会将 this
视为window
。 (注意:在严格模式下, this
为undefined
而不是window
)。
为了解决这个问题,我们可以在函数被返回时就绑好正确的this
。 这样的话,无论之后怎么使用它,都会引用绑定的‘deck’对象。 我们需要改变函数表达式来使用ECMAScript 6箭头语法。 箭头函数能保存函数创建时的 this
值,而不是调用时的值:
我们把函数表达式变为使用lambda表达式( () => {} )。 这样就会在函数创建的时候就指定了‘this’值,而不是在函数调用的时候。
let deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), createCardPicker: function() { // NOTE: the line below is now an arrow function, allowing us to capture 'this' right here return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }}let cardPicker = deck.createCardPicker();let pickedCard = cardPicker();alert("card: " + pickedCard.card + " of " + pickedCard.suit);
更好事情是,TypeScript会警告你犯了一个错误,如果你给编译器设置了--noImplicitThis
标记。 它会指出this.suits[pickedSuit]
里的this
的类型为any
。
this
参数不幸的是,this.suits[pickedSuit]
的类型依旧为any
。 这是因为 this
来自对象字面量里的函数表达式。 修改的方法是,提供一个显式的 this
参数。 this
参数是个假的参数,它出现在参数列表的最前面:
function f(this: void) { // make sure `this` is unusable in this standalone function}
让我们往例子里添加一些接口,Card
和 Deck
,让类型重用能够变得清晰简单些:
interface Card { suit: string; card: number;}interface Deck { suits: string[]; cards: number[]; createCardPicker(this: Deck): () => Card;}let deck: Deck = { suits: ["hearts", "spades", "clubs", "diamonds"], cards: Array(52), // NOTE: The function now explicitly specifies that its callee must be of type Deck createCardPicker: function(this: Deck) { return () => { let pickedCard = Math.floor(Math.random() * 52); let pickedSuit = Math.floor(pickedCard / 13); return {suit: this.suits[pickedSuit], card: pickedCard % 13}; } }}let cardPicker = deck.createCardPicker();let pickedCard = cardPicker();alert("card: " + pickedCard.card + " of " + pickedCard.suit);
现在TypeScript知道createCardPicker
期望在某个Deck
对象上调用。 也就是说 this
是Deck
类型的,而非any
,因此--noImplicitThis
不会报错了。
this
参数在回调函数里你可以也看到过在回调函数里的this
报错,当你将一个函数传递到某个库函数里稍后会被调用时。 因为当回调被调用的时候,它们会被当成一个普通函数调用, this
将为undefined
。 稍做改动,你就可以通过 this
参数来避免错误。 首先,库函数的作者要指定 this
的类型:
interface UIElement { addClickListener(onclick: (this: void, e: Event) => void): void;}
this: void
表示 addClickListener
期望 onclick
是一个不需要 this
类型的函数。 其次,用 this
注释您的调用代码:
class Handler { info: string; onClickBad(this: Handler, e: Event) { // oops, used this here. using this callback would crash at runtime this.info = e.message; };}let h = new Handler();uiElement.addClickListener(h.onClickBad); // error!
指定了this
类型后,你显式声明onClickBad
必须在Handler
的实例上调用。 然后TypeScript会检测到addClickListener
要求函数带有this: void
。 改变 this
类型来修复这个错误:
class Handler { info: string; onClickGood(this: void, e: Event) { // can't use this here because it's of type void! console.log('clicked!'); }}let h = new Handler();uiElement.addClickListener(h.onClickGood);
因为onClickGood
指定了this
类型为void
,因此传递addClickListener
是合法的。 当然了,这也意味着不能使用 this.info
. 如果你两者都想要,你不得不使用箭头函数了:
class Handler { info: string; onClickGood = (e: Event) => { this.info = e.message }}
这是可行的因为箭头函数不会捕获this
,所以你总是可以把它们传给期望this: void
的函数。 缺点是每个Handler
对象都会创建一个箭头函数。 另一方面,方法只会被创建一次,添加到 Handler
的原型链上。 它们在不同 Handler
对象间是共享的。
JavaScript本身是个动态语言。 JavaScript里函数根据传入不同的参数而返回不同类型的数据是很常见的。
let suits = ["hearts", "spades", "clubs", "diamonds"];function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card else if (typeof x == "number") { let pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; }}let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];let pickedCard1 = myDeck[pickCard(myDeck)];alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);let pickedCard2 = pickCard(15);alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
pickCard
方法根据传入参数的不同会返回两种不同的类型。 如果传入的是代表纸牌的对象,函数作用是从中抓一张牌。 如果用户想抓牌,我们告诉他抓到了什么牌。 但是这怎么在类型系统里表示呢。
方法是为同一个函数提供多个函数类型定义来进行函数重载。 编译器会根据这个列表去处理函数的调用。 下面我们来重载 pickCard
函数。
let suits = ["hearts", "spades", "clubs", "diamonds"];function pickCard(x: {suit: string; card: number; }[]): number;function pickCard(x: number): {suit: string; card: number; };function pickCard(x): any { // Check to see if we're working with an object/array // if so, they gave us the deck and we'll pick the card if (typeof x == "object") { let pickedCard = Math.floor(Math.random() * x.length); return pickedCard; } // Otherwise just let them pick the card else if (typeof x == "number") { let pickedSuit = Math.floor(x / 13); return { suit: suits[pickedSuit], card: x % 13 }; }}let myDeck = [{ suit: "diamonds", card: 2 }, { suit: "spades", card: 10 }, { suit: "hearts", card: 4 }];let pickedCard1 = myDeck[pickCard(myDeck)];alert("card: " + pickedCard1.card + " of " + pickedCard1.suit);let pickedCard2 = pickCard(15);alert("card: " + pickedCard2.card + " of " + pickedCard2.suit);
这样改变后,重载的pickCard
函数在调用的时候会进行正确的类型检查。
为了让编译器能够选择正确的检查类型,它与JavaScript里的处理流程相似。 它查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。
注意,function pickCard(x): any
并不是重载列表的一部分,因此这里只有两个重载:一个是接收对象另一个接收数字。 以其它参数调用 pickCard
会产生错误。
软件工程中,我们不仅要创建一致的定义良好的API,同时也要考虑可重用性。 组件不仅能够支持当前的数据类型,同时也能支持未来的数据类型,这在创建大型系统时为你提供了十分灵活的功能。
在像C#和Java这样的语言中,可以使用泛型
来创建可重用的组件,一个组件可以支持多种类型的数据。 这样用户就可以以自己的数据类型来使用组件。
下面来创建第一个使用泛型的例子:identity函数。 这个函数会返回任何传入它的值。 你可以把这个函数当成是echo
命令。
不用泛型的话,这个函数可能是下面这样:
function identity(arg: number): number { return arg;}
或者,我们使用any
类型来定义函数:
function identity(arg: any): any { return arg;}
虽然使用any
类型后这个函数已经能接收任何类型的arg参数,但是却丢失了一些信息:传入的类型与返回的类型应该是相同的。 如果我们传入一个数字,我们只知道任何类型的值都有可能被返回。
因此,我们需要一种方法使返回值的类型与传入参数的类型是相同的。 这里,我们使用了 类型变量,它是一种特殊的变量,只用于表示类型而不是值。
function identity<T>(arg: T): T { return arg;}
我们给identity添加了类型变量T
。 T
帮助我们捕获用户传入的类型(比如:number
),之后我们就可以使用这个类型。 之后我们再次使用了 T
当做返回值类型。现在我们可以知道参数类型与返回值类型是相同的了。 这允许我们跟踪函数里使用的类型的信息。
我们把这个版本的identity
函数叫做泛型,因为它可以适用于多个类型。 不同于使用 any
,它不会丢失信息,像第一个例子那像保持准确性,传入数值类型并返回数值类型。
我们定义了泛型函数后,可以用两种方法使用。 第一种是,传入所有的参数,包含类型参数:
let output = identity<string>("myString"); // type of output will be 'string'
这里我们明确的指定了T
是string
类型,并做为一个参数传给函数,使用了<>
括起来而不是()
。
第二种方法更普遍。利用了类型推论 -- 即编译器会根据传入的参数自动地帮助我们确定T的类型:
let output = identity("myString"); // type of output will be 'string'
注意我们没必要使用尖括号(<>
)来明确地传入类型;编译器可以查看myString
的值,然后把T
设置为它的类型。 类型推论帮助我们保持代码精简和高可读性。如果编译器不能够自动地推断出类型的话,只能像上面那样明确的传入T的类型,在一些复杂的情况下,这是可能出现的。
使用泛型创建像identity
这样的泛型函数时,编译器要求你在函数体必须正确的使用这个通用的类型。 换句话说,你必须把这些参数当做是任意或所有类型。
看下之前identity
例子:
function identity<T>(arg: T): T { return arg;}
如果我们想同时打印出arg
的长度。 我们很可能会这样做:
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn't have .length return arg;}
如果这么做,编译器会报错说我们使用了arg
的.length
属性,但是没有地方指明arg
具有这个属性。 记住,这些类型变量代表的是任意类型,所以使用这个函数的人可能传入的是个数字,而数字是没有 .length
属性的。
现在假设我们想操作T
类型的数组而不直接是T
。由于我们操作的是数组,所以.length
属性是应该存在的。 我们可以像创建其它数组一样创建这个数组:
function loggingIdentity<T>(arg: T[]): T[] { console.log(arg.length); // Array has a .length, so no more error return arg;}
你可以这样理解loggingIdentity
的类型:泛型函数loggingIdentity
,接收类型参数T
,和函数arg
,它是个元素类型是T
的数组,并返回元素类型是T
的数组。 如果我们传入数字数组,将返回一个数字数组,因为此时T
的的类型为number
。 这可以让我们把泛型变量T当做类型的一部分使用,而不是整个类型,增加了灵活性。
我们也可以这样实现上面的例子:
function loggingIdentity<T>(arg: Array<T>): Array<T> { console.log(arg.length); // Array has a .length, so no more error return arg;}
使用过其它语言的话,你可能对这种语法已经很熟悉了。 在下一节,会介绍如何创建自定义泛型像 Array<T>
一样。
我们来到了泛型接口;让我们创建一个泛型接口来与 identities()
一起使用:
interface Identities<V, W> { id1: V, id2: W}
我在这里使用 V 和 W 作为类型变量来表示任何字母(或有效的字母和数字名称的组合)都是可以的——除了用于常规目的之外,它们的名称没有任何意义。
现在,我们可以将这个接口应用为 identities()
的返回类型,并稍稍修改返回类型去迎合它。我们还可以 console.log
这些参数和它们的类型,以便进一步说明:
function identities<T, U> (arg1: T, arg2: U): Identities<T, U> { console.log(arg1 + ": " + typeof (arg1)); console.log(arg2 + ": " + typeof (arg2)); let identities: Identities<T, U> = { id1: arg1, id2: arg2 }; return identities;}
我们现在 identities()
所做的是将类型 T 和 U 传递到函数和 Identities
接口中,使我们能够定义与参数类型相关的返回类型。
注意:如果编译 TS 项目并查找泛型,则不会找到任何泛型。由于在 Javascript 中不支持泛型,所以在编译器生成的构建中不会看到泛型。泛型纯粹是用于编译时的开发安全网,它将确保代码的类型安全抽象。
泛型类看上去与泛型接口差不多。 泛型类使用( <>
)括起泛型类型,跟在类名后面。
class GenericNumber<T> { zeroValue: T; add: (x: T, y: T) => T;}let myGenericNumber = new GenericNumber<number>();myGenericNumber.zeroValue = 0;myGenericNumber.add = function(x, y) { return x + y; };
GenericNumber
类的使用是十分直观的,并且你可能已经注意到了,没有什么去限制它只能使用number
类型。 也可以使用字符串或其它更复杂的类型。
let stringNumeric = new GenericNumber<string>();stringNumeric.zeroValue = "";stringNumeric.add = function(x, y) { return x + y; };alert(stringNumeric.add(stringNumeric.zeroValue, "test"));
与接口一样,直接把泛型类型放在类后面,可以帮助我们确认类的所有属性都在使用相同的类型。
我们在类那节说过,类有两部分:静态部分和实例部分。 泛型类指的是实例部分的类型,所以类的静态属性不能使用这个泛型类型。
你应该会记得之前的一个例子,我们有时候想操作某类型的一组值,并且我们知道这组值具有什么样的属性。 在loggingIdentity
例子中,我们想访问arg
的length
属性,但是编译器并不能证明每种类型都有length
属性,所以就报错了。
function loggingIdentity<T>(arg: T): T { console.log(arg.length); // Error: T doesn't have .length return arg;}
相比于操作any
所有类型,我们想要限制函数去处理任意带有.length
属性的所有类型。 只要传入的类型有这个属性,我们就允许,就是说至少包含这一属性。 为此,我们需要列出对于T的约束要求。
为此,我们定义一个接口来描述约束条件。 创建一个包含 .length
属性的接口,使用这个接口和extends
关键字还实现约束:
interface Lengthwise { length: number;}function loggingIdentity<T extends Lengthwise>(arg: T): T { console.log(arg.length); // Now we know it has a .length property, so no more error return arg;}
现在这个泛型函数被定义了约束,因此它不再是适用于任意类型:
loggingIdentity(3); // Error, number doesn't have a .length property
我们需要传入符合约束类型的值,必须包含必须的属性:
loggingIdentity({length: 10, value: 3});
你可以声明一个类型参数,且它被另一个类型参数所约束。比如,
function find<T, U extends Findable<T>>(n: T, s: U) { // ...}find (giraffe, myAnimals);
在 TypeScript 使用泛型创建工厂函数时,需要引用构造函数的类类型。比如,
function create<T>(c: {new(): T; }): T { return new c();}
一个更高级的例子,使用原型属性推断并约束构造函数与类实例的关系。
class BeeKeeper { hasMask: boolean;}class ZooKeeper { nametag: string;}class Animal { numLegs: number;}class Bee extends Animal { keeper: BeeKeeper;}class Lion extends Animal { keeper: ZooKeeper;}function findKeeper<A extends Animal, K> (a: {new(): A; prototype: {keeper: K}}): K { return a.prototype.keeper;}findKeeper(Lion).nametag; // typechecks!
使用枚举我们可以定义一些有名字的数字常量。 枚举通过 enum
关键字来定义。
enum Direction { Up = 1, Down, Left, Right}
一个枚举类型可以包含零个或多个枚举成员。 枚举成员具有一个数字值,它可以是 常数或是计算得出的值 当满足如下条件时,枚举成员被当作是常数:
0
。+
, -
, ~
一元运算符应用于常数枚举表达式+
, -
, *
, /
, %
, <<
, >>
, >>>
, &
, |
, ^
二元运算符,常数枚举表达式做为其一个操作对象 若常数枚举表达式求值后为 NaN
或Infinity
,则会在编译阶段报错。所有其它情况的枚举成员被当作是需要计算得出的值。
enum FileAccess { // constant members None, Read = 1 << 1, Write = 1 << 2, ReadWrite = Read | Write // computed member G = "123".length}
枚举是在运行时真正存在的一个对象。 其中一个原因是因为这样可以从枚举值到枚举名进行反向映射。
enum Enum { A}let a = Enum.A;let nameOfA = Enum[Enum.A]; // "A"
编译成:
var Enum;(function (Enum) { Enum[Enum["A"] = 0] = "A";})(Enum || (Enum = {}));var a = Enum.A;var nameOfA = Enum[Enum.A]; // "A"
生成的代码中,枚举类型被编译成一个对象,它包含双向映射(name
-> value
)和(value
-> name
)。 引用枚举成员总会生成一次属性访问并且永远不会内联。 在大多数情况下这是很好的并且正确的解决方案。 然而有时候需求却比较严格。 当访问枚举值时,为了避免生成多余的代码和间接引用,可以使用常数枚举。 常数枚举是在enum
关键字前使用const
修饰符。
const enum Enum { A = 1, B = A * 2}
常数枚举只能使用常数枚举表达式并且不同于常规的枚举的是它们在编译阶段会被删除。 常数枚举成员在使用的地方被内联进来。 这是因为常数枚举不可能有计算成员。
const enum Directions { Up, Down, Left, Right}let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right]
生成后的代码为:
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
外部枚举用来描述已经存在的枚举类型的形状。
declare enum Enum { A = 1, B, C = 2}
外部枚举和非外部枚举之间有一个重要的区别,在正常的枚举里,没有初始化方法的成员被当成常数成员。 对于非常数的外部枚举而言,没有初始化方法时被当做需要经过计算的。
这节介绍TypeScript里的类型推论。即,类型是在哪里如何被推断的。
TypeScript里,在有些没有明确指出类型的地方,类型推论会帮助提供类型。如下面的例子
let x = 3;
变量x
的类型被推断为数字。 这种推断发生在初始化变量和成员,设置默认参数值和决定函数返回值时。
大多数情况下,类型推论是直截了当地。 后面的小节,我们会浏览类型推论时的细微差别。
当需要从几个表达式中推断类型时候,会使用这些表达式的类型来推断出一个最合适的通用类型。例如,
let x = [0, 1, null];
为了推断x
的类型,我们必须考虑所有元素的类型。 这里有两种选择: number
和null
。 计算通用类型算法会考虑所有的候选类型,并给出一个兼容所有候选类型的类型。
由于最终的通用类型取自候选类型,有些时候候选类型共享相同的通用类型,但是却没有一个类型能做为所有候选类型的类型。例如:
let zoo = [new Rhino(), new Elephant(), new Snake()];
这里,我们想让zoo被推断为Animal[]
类型,但是这个数组里没有对象是Animal
类型的,因此不能推断出这个结果。 为了更正,当候选类型不能使用的时候我们需要明确的指出类型:
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
如果没有找到最佳通用类型的话,类型推论的结果是空对象类型,{}
。 因为这个类型没有任何成员,所以访问其成员的时候会报错。
TypeScript类型推论也可能按照相反的方向进行。 这被叫做“按上下文归类”。按上下文归类会发生在表达式的类型与所处的位置相关时。比如:
window.onmousedown = function(mouseEvent) { console.log(mouseEvent.buton); //<- Error};
这个例子会得到一个类型错误,TypeScript类型检查器使用Window.onmousedown
函数的类型来推断右边函数表达式的类型。 因此,就能推断出 mouseEvent
参数的类型了。 如果函数表达式不是在上下文类型的位置,mouseEvent
参数的类型需要指定为any
,这样也不会报错了。
如果上下文类型表达式包含了明确的类型信息,上下文的类型被忽略。 重写上面的例子:
window.onmousedown = function(mouseEvent: any) { console.log(mouseEvent.buton); //<- Now, no error is given};
这个函数表达式有明确的参数类型注解,上下文类型被忽略。 这样的话就不报错了,因为这里不会使用到上下文类型。
上下文归类会在很多情况下使用到。 通常包含函数的参数,赋值表达式的右边,类型断言,对象成员和数组字面量和返回值语句。 上下文类型也会做为最佳通用类型的候选类型。比如:
function createZoo(): Animal[] { return [new Rhino(), new Elephant(), new Snake()];}
这个例子里,最佳通用类型有4个候选者:Animal
,Rhino
,Elephant
和Snake
。 当然, Animal
会被做为最佳通用类型。
TypeScript里的类型兼容性是基于结构子类型的。 结构类型是一种只使用其成员来描述类型的方式。 它正好与名义(nominal)类型形成对比。(译者注:在基于名义类型的类型系统中,数据类型的兼容性或等价性是通过明确的声明和/或类型的名称来决定的。这与结构性类型系统不同,它是基于类型的组成结构,且不要求明确地声明。) 看下面的例子:
interface Named { name: string;}class Person { name: string;}let p: Named;// OK, because of structural typingp = new Person();
在使用基于名义类型的语言,比如C#或Java中,这段代码会报错,因为Person类没有明确说明其实现了Named接口。
TypeScript的结构性子类型是根据JavaScript代码的典型写法来设计的。 因为JavaScript里广泛地使用匿名对象,例如函数表达式和对象字面量,所以使用结构类型系统来描述这些类型比使用名义类型系统更好。
TypeScript的类型系统允许某些在编译阶段无法确认其安全性的操作。当一个类型系统具此属性时,被当做是“不可靠”的。TypeScript允许这种不可靠行为的发生是经过仔细考虑的。通过这篇文章,我们会解释什么时候会发生这种情况和其有利的一面。
TypeScript结构化类型系统的基本规则是,如果x
要兼容y
,那么y
至少具有与x
相同的属性。比如:
interface Named { name: string;}let x: Named;// y's inferred type is { name: string; location: string; }let y = { name: 'Alice', location: 'Seattle' };x = y;
这里要检查y
是否能赋值给x
,编译器检查x
中的每个属性,看是否能在y
中也找到对应属性。 在这个例子中,y
必须包含名字是name
的string
类型成员。y
满足条件,因此赋值正确。
检查函数参数时使用相同的规则:
function greet(n: Named) { alert('Hello, ' + n.name);}greet(y); // OK
注意,y
有个额外的location
属性,但这不会引发错误。 只有目标类型(这里是 Named
)的成员会被一一检查是否兼容。
这个比较过程是递归进行的,检查每个成员及子成员。
相对来讲,在比较原始类型和对象类型的时候是比较容易理解的,问题是如何判断两个函数是兼容的。 下面我们从两个简单的函数入手,它们仅是参数列表略有不同:
let x = (a: number) => 0;let y = (b: number, s: string) => 0;y = x; // OKx = y; // Error
要查看x
是否能赋值给y
,首先看它们的参数列表。 x
的每个参数必须能在y
里找到对应类型的参数。 注意的是参数的名字相同与否无所谓,只看它们的类型。 这里, x
的每个参数在y
中都能找到对应的参数,所以允许赋值。
第二个赋值错误,因为y
有个必需的第二个参数,但是x
并没有,所以不允许赋值。
你可能会疑惑为什么允许忽略
参数,像例子y = x
中那样。 原因是忽略额外的参数在JavaScript里是很常见的。 例如, Array#forEach
给回调函数传3个参数:数组元素,索引和整个数组。 尽管如此,传入一个只使用第一个参数的回调函数也是很有用的:
let items = [1, 2, 3];// Don't force these extra argumentsitems.forEach((item, index, array) => console.log(item));// Should be OK!items.forEach((item) => console.log(item));
下面来看看如何处理返回值类型,创建两个仅是返回值类型不同的函数:
let x = () => ({name: 'Alice'});let y = () => ({name: 'Alice', location: 'Seattle'});x = y; // OKy = x; // Error because x() lacks a location property
类型系统强制源函数的返回值类型必须是目标函数返回值类型的子类型。
当比较函数参数类型时,只有当源函数参数能够赋值给目标函数或者反过来时才能赋值成功。 这是不稳定的,因为调用者可能传入了一个具有更精确类型信息的函数,但是调用这个传入的函数的时候却使用了不是那么精确的类型信息。 实际上,这极少会发生错误,并且能够实现很多JavaScript里的常见模式。例如:
enum EventType { Mouse, Keyboard }interface Event { timestamp: number; }interface MouseEvent extends Event { x: number; y: number }interface KeyEvent extends Event { keyCode: number }function listenEvent(eventType: EventType, handler: (n: Event) => void) { /* ... */}// Unsound, but useful and commonlistenEvent(EventType.Mouse, (e: MouseEvent) => console.log(e.x + ',' + e.y));// Undesirable alternatives in presence of soundnesslistenEvent(EventType.Mouse, (e: Event) => console.log((<MouseEvent>e).x + ',' + (<MouseEvent>e).y));listenEvent(EventType.Mouse, <(e: Event) => void>((e: MouseEvent) => console.log(e.x + ',' + e.y)));// Still disallowed (clear error). Type safety enforced for wholly incompatible typeslistenEvent(EventType.Mouse, (e: number) => console.log(e));
比较函数兼容性的时候,可选参数与必须参数是可交换的。 原类型上额外的可选参数并不会造成错误,目标类型的可选参数没有对应的参数也不是错误。
当一个函数有剩余参数时,它被当做无限个可选参数。
这对于类型系统来说是不稳定的,但从运行时的角度来看,可选参数一般来说是不强制的,因为对于大多数函数来说相当于传递了一些undefinded
。
有一个好的例子,常见的函数接收一个回调函数并用对于程序员来说是可预知的参数但对类型系统来说是不确定的参数来调用:
function invokeLater(args: any[], callback: (...args: any[]) => void) { /* ... Invoke callback with 'args' ... */}// Unsound - invokeLater "might" provide any number of argumentsinvokeLater([1, 2], (x, y) => console.log(x + ', ' + y));// Confusing (x and y are actually required) and undiscoverableinvokeLater([1, 2], (x?, y?) => console.log(x + ', ' + y));
对于有重载的函数,源函数的每个重载都要在目标函数上找到对应的函数签名。 这确保了目标函数可以在所有源函数可调用的地方调用。
枚举类型与数字类型兼容,并且数字类型与枚举类型兼容。不同枚举类型之间是不兼容的。比如,
enum Status { Ready, Waiting };enum Color { Red, Blue, Green };let status = Status.Ready;status = Color.Green; //error
类与对象字面量和接口差不多,但有一点不同:类有静态部分和实例部分的类型。 比较两个类类型的对象时,只有实例的成员会被比较。 静态成员和构造函数不在比较的范围内。
class Animal { feet: number; constructor(name: string, numFeet: number) { }}class Size { feet: number; constructor(numFeet: number) { }}let a: Animal;let s: Size;a = s; //OKs = a; //OK
私有成员会影响兼容性判断。 当类的实例用来检查兼容时,如果它包含一个私有成员,那么目标类型必须包含来自同一个类的这个私有成员。 这允许子类赋值给父类,但是不能赋值给其它有同样类型的类。
因为TypeScript是结构性的类型系统,类型参数只影响使用其做为类型一部分的结果类型。比如,
interface Empty<T> {}let x: Empty<number>;let y: Empty<string>;x = y; // okay, y matches structure of x
上面代码里,x
和y
是兼容的,因为它们的结构使用类型参数时并没有什么不同。 把这个例子改变一下,增加一个成员,就能看出是如何工作的了:
interface NotEmpty<T> { data: T;}let x: NotEmpty<number>;let y: NotEmpty<string>;x = y; // error, x and y are not compatible
在这里,泛型类型在使用时就好比不是一个泛型类型。
对于没指定泛型类型的泛型参数时,会把所有泛型参数当成any
比较。 然后用结果类型进行比较,就像上面第一个例子。
比如,
let identity = function<T>(x: T): T { // ...}let reverse = function<U>(y: U): U { // ...}identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any
目前为止,我们使用了兼容性
,它在语言规范里没有定义。 在TypeScript里,有两种类型的兼容性:子类型与赋值。 它们的不同点在于,赋值扩展了子类型兼容,允许给 any
赋值或从any
取值和允许数字赋值给枚举类型或枚举类型赋值给数字。
语言里的不同地方分别使用了它们之中的机制。 实际上,类型兼容性是由赋值兼容性来控制的甚至在implements
和extends
语句里。 更多信息,请参阅 TypeScript语言规范.
交叉类型是将多个类型合并为一个类型。 这让我们可以把现有的多种类型叠加到一起成为一种类型,它包含了所需的所有类型的特性。 例如, Person & Serializable & Loggable
同时是Person
和Serializable
和Loggable
。 就是说这个类型的对象同时拥有了这三种类型的成员。
我们大多是在混入(mixins)或其它不适合典型面向对象模型的地方看到交叉类型的使用。 (在JavaScript里发生这种情况的场合很多!) 下面是如何创建混入的一个简单例子:
function extend<T, U>(first: T, second: U): T & U { let result = <T & U>{}; for (let id in first) { (<any>result)[id] = (<any>first)[id]; } for (let id in second) { if (!result.hasOwnProperty(id)) { (<any>result)[id] = (<any>second)[id]; } } return result;}class Person { constructor(public name: string) { }}interface Loggable { log(): void;}class ConsoleLogger implements Loggable { log() { // ... }}var jim = extend(new Person("Jim"), new ConsoleLogger());var n = jim.name;jim.log();
联合类型与交叉类型很有关联,但是使用上却完全不同。 偶尔你会遇到这种情况,一个代码库希望传入 number
或string
类型的参数。 例如下面的函数:
/** * Takes a string and adds "padding" to the left. * If 'padding' is a string, then 'padding' is appended to the left side. * If 'padding' is a number, then that number of spaces is added to the left side. */function padLeft(value: string, padding: any) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value; } if (typeof padding === "string") { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`);}padLeft("Hello world", 4); // returns " Hello world"
padLeft
存在一个问题,padding
参数的类型指定成了any
。 这就是说我们可以传入一个既不是 number
也不是string
类型的参数,但是TypeScript却不报错。
let indentedString = padLeft("Hello world", true); // 编译阶段通过,运行时报错
在传统的面向对象语言里,我们可能会将这两种类型抽象成有层级的类型。 这么做显然是非常清晰的,但同时也存在了过度设计。 padLeft
原始版本的好处之一是允许我们传入原始类型。 这样做的话使用起来既简单又方便。 如果我们就是想使用已经存在的函数的话,这种新的方式就不适用了。
代替any
, 我们可以使用联合类型做为padding
的参数:
/** * Takes a string and adds "padding" to the left. * If 'padding' is a string, then 'padding' is appended to the left side. * If 'padding' is a number, then that number of spaces is added to the left side. */function padLeft(value: string, padding: string | number) { // ...}let indentedString = padLeft("Hello world", true); // errors during compilation
联合类型表示一个值可以是几种类型之一。 我们用竖线( |
)分隔每个类型,所以number | string | boolean
表示一个值可以是number
,string
,或boolean
。
如果一个值是联合类型,我们只能访问此联合类型的所有类型里共有的成员。
interface Bird { fly(); layEggs();}interface Fish { swim(); layEggs();}function getSmallPet(): Fish | Bird { // ...}let pet = getSmallPet();pet.layEggs(); // okaypet.swim(); // errors
这里的联合类型可能有点复杂,但是你很容易就习惯了。 如果一个值的类型是 A | B
,我们能够确定的是它包含了A
和B
中共有的成员。 这个例子里, Bird
具有一个fly
成员。 我们不能确定一个 Bird | Fish
类型的变量是否有fly
方法。 如果变量在运行时是 Fish
类型,那么调用pet.fly()
就出错了。
联合类型非常适合这样的情形,可接收的值有不同的类型。 当我们想明确地知道是否拿到 Fish
时会怎么做? JavaScript里常用来区分2个可能值的方法是检查它们是否存在。 像之前提到的,我们只能访问联合类型的所有类型中共有的成员。
let pet = getSmallPet();// 每一个成员访问都会报错if (pet.swim) { pet.swim();}else if (pet.fly) { pet.fly();}
为了让这段代码工作,我们要使用类型断言:
let pet = getSmallPet();if ((<Fish>pet).swim) { (<Fish>pet).swim();}else { (<Bird>pet).fly();}
可以注意到我们使用了多次类型断言。 如果我们只要检查过一次类型,就能够在后面的每个分支里清楚 pet
的类型的话就好了。
TypeScript里的类型保护机制让它成为了现实。 类型保护就是一些表达式,它们会在运行时检查以确保在某个作用域里的类型。 要定义一个类型保护,我们只要简单地定义一个函数,它的返回值是一个 类型断言:
function isFish(pet: Fish | Bird): pet is Fish { return (<Fish>pet).swim !== undefined;}
在这个例子里,pet is Fish
就是类型谓词。 谓词是 parameterName is Type
这种形式,parameterName
必须是来自于当前函数签名里的一个参数名。
每当使用一些变量调用isFish
时,TypeScript会将变量缩减为那个具体的类型,只要这个类型与变量的原始类型是兼容的。
// 'swim' 和 'fly' 调用都没有问题了if (isFish(pet)) { pet.swim();}else { pet.fly();}
注意TypeScript不仅知道在if
分支里pet
是Fish
类型; 它还清楚在 else
分支里,一定不是Fish
类型,一定是Bird
类型。
typeof
类型保护现在我们回过头来看看怎么使用联合类型书写padLeft
代码。 我们可以像下面这样利用类型断言来写:
function isNumber(x: any): x is number { return typeof x === "number";}function isString(x: any): x is string { return typeof x === "string";}function padLeft(value: string, padding: string | number) { if (isNumber(padding)) { return Array(padding + 1).join(" ") + value; } if (isString(padding)) { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`);}
然而,必须要定义一个函数来判断类型是否是原始类型,这太痛苦了。 幸运的是,现在我们不必将 typeof x === "number"
抽象成一个函数,因为TypeScript可以将它识别为一个类型保护。 也就是说我们可以直接在代码里检查类型了。
function padLeft(value: string, padding: string | number) { if (typeof padding === "number") { return Array(padding + 1).join(" ") + value; } if (typeof padding === "string") { return padding + value; } throw new Error(`Expected string or number, got '${padding}'.`);}
这些*typeof
类型保护*只有两种形式能被识别:typeof v === "typename"
和typeof v !== "typename"
,"typename"
必须是"number"
,"string"
,"boolean"
或"symbol"
。 但是TypeScript并不会阻止你与其它字符串比较,语言不会把那些表达式识别为类型保护。
instanceof
类型保护如果你已经阅读了typeof
类型保护并且对JavaScript里的instanceof
操作符熟悉的话,你可能已经猜到了这节要讲的内容。
instanceof
类型保护是通过构造函数来细化类型的一种方式。 比如,我们借鉴一下之前字符串填充的例子:
interface Padder { getPaddingString(): string}class SpaceRepeatingPadder implements Padder { constructor(private numSpaces: number) { } getPaddingString() { return Array(this.numSpaces + 1).join(" "); }}class StringPadder implements Padder { constructor(private value: string) { } getPaddingString() { return this.value; }}function getRandomPadder() { return Math.random() < 0.5 ? new SpaceRepeatingPadder(4) : new StringPadder(" ");}// 类型为SpaceRepeatingPadder | StringPadderlet padder: Padder = getRandomPadder();if (padder instanceof SpaceRepeatingPadder) { padder; // 类型细化为'SpaceRepeatingPadder'}if (padder instanceof StringPadder) { padder; // 类型细化为'StringPadder'}
instanceof
的右侧要求是一个构造函数,TypeScript将细化为:
prototype
属性的类型,如果它的类型不为any
的话以此顺序。
类型别名会给一个类型起个新名字。 类型别名有时和接口很像,但是可以作用于原始值,联合类型,元组以及其它任何你需要手写的类型。
type Name = string;type NameResolver = () => string;type NameOrResolver = Name | NameResolver;function getName(n: NameOrResolver): Name { if (typeof n === 'string') { return n; } else { return n(); }}
起别名不会新建一个类型 - 它创建了一个新名字来引用那个类型。 给原始类型起别名通常没什么用,尽管可以做为文档的一种形式使用。
同接口一样,类型别名也可以是泛型 - 我们可以添加类型参数并且在别名声明的右侧传入:
type Container<T> = { value: T };
我们也可以使用类型别名来在属性里引用自己:
type Tree<T> = { value: T; left: Tree<T>; right: Tree<T>;}
然而,类型别名不能够出现在声名语句的右侧:
type LinkedList<T> = T & { next: LinkedList<T> };interface Person { name: string;}var people: LinkedList<Person>;var s = people.name;var s = people.next.name;var s = people.next.next.name;var s = people.next.next.next.name;
然而,类型别名不能出现在声明右侧的任何地方。
type Yikes = Array<Yikes>; // error
像我们提到的,类型别名可以像接口一样;然而,仍有一些细微差别。
其一,接口创建了一个新的名字,可以在其它任何地方使用。 类型别名并不创建新名字—比如,错误信息就不会使用别名。 在下面的示例代码里,在编译器中将鼠标悬停在 interfaced
上,显示它返回的是Interface
,但悬停在aliased
上时,显示的却是对象字面量类型。
type Alias = { num: number }interface Interface { num: number;}declare function aliased(arg: Alias): Alias;declare function interfaced(arg: Interface): Interface;
另一个重要区别是类型别名不能被extends
和implements
(自己也不能extends
和implements
其它类型)。 因为 软件中的对象应该对于扩展是开放的,但是对于修改是封闭的,你应该尽量去使用接口代替类型别名。
另一方面,如果你无法通过接口来描述一个类型并且需要使用联合类型或元组类型,这时通常会使用类型别名。
字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。
type Easing = "ease-in" | "ease-out" | "ease-in-out";class UIElement { animate(dx: number, dy: number, easing: Easing) { if (easing === "ease-in") { // ... } else if (easing === "ease-out") { } else if (easing === "ease-in-out") { } else { // error! should not pass null or undefined. } }}let button = new UIElement();button.animate(0, 0, "ease-in");button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here
你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'
字符串字面量类型还可以用于区分函数重载:
function createElement(tagName: "img"): HTMLImageElement;function createElement(tagName: "input"): HTMLInputElement;// ... more overloads ...function createElement(tagName: string): Element { // ... code goes here ...}
你可以合并字符串字面量类型,联合类型,类型保护和类型别名来创建一个叫做可辨识联合的高级模式,它也称做标签联合或代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有4个要素:
interface Square { kind: "square"; size: number;}interface Rectangle { kind: "rectangle"; width: number; height: number;}interface Circle { kind: "circle"; radius: number;}
首先我们声明了将要联合的接口。 每个接口都有 kind
属性但有不同的字符器字面量类型。 kind
属性称做可辨识的特征或标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:
type Shape = Square | Rectangle | Circle;
现在我们使用可辨识联合:
function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; }}
当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了 Triangle
到Shape
,我们同时还需要更新area
:
type Shape = Square | Rectangle | Circle | Triangle;function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; } // should error here - we didn't handle case "triangle"}
有两种方式可以实现。 首先是启用 --strictNullChecks
并且指定一个返回值类型:
function area(s: Shape): number { // error: returns number | undefined switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; }}
因为switch
没有包涵所有情况,所以TypeScript认为这个函数有时候会返回undefined
。 如果你明确地指定了返回值类型为 number
,那么你会看到一个错误,因为实际上返回值的类型为number | undefined
。 然而,这种方法存在些微妙之处且 --strictNullChecks
对旧代码支持不好。
第二种方法使用never
类型,编译器用它来进行完整性检查:
function assertNever(x: never): never { throw new Error("Unexpected object: " + x);}function area(s: Shape) { switch (s.kind) { case "square": return s.size * s.size; case "rectangle": return s.height * s.width; case "circle": return Math.PI * s.radius ** 2; default: return assertNever(s); // error here if there are missing cases }}
这里,assertNever
检查s
是否为never
类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s
将具有一个赶写的类型,因此你会得到一个错误。 这种方式需要你定义一个额外的函数。
this
类型多态的this
类型表示的是某个包含类或接口的子类型。 这被称做 F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在计算器的例子里,在每个操作之后都返回 this
类型:
class BasicCalculator { public constructor(protected value: number = 0) { } public currentValue(): number { return this.value; } public add(operand: number): this { this.value += operand; return this; } public multiply(operand: number): this { this.value *= operand; return this; } // ... other operations go here ...}let v = new BasicCalculator(2) .multiply(5) .add(1) .currentValue();
由于这个类使用了this
类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。
class ScientificCalculator extends BasicCalculator { public constructor(value = 0) { super(value); } public sin() { this.value = Math.sin(this.value); return this; } // ... other operations go here ...}let v = new ScientificCalculator(2) .multiply(5) .sin() .add(1) .currentValue();
如果没有this
类型,ScientificCalculator
就不能够在继承BasicCalculator
的同时还保持接口的连贯性。 multiply
将会返回BasicCalculator
,它并没有sin
方法。 然而,使用 this
类型,multiply
会返回this
,在这里就是ScientificCalculator
。
自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的列表:
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,迭代器只允许在Array
类型上使用。 在非数组值上使用 for..of
语句会得到一个错误,就算这些非数组值已经实现了Symbol.iterator
属性。
编译器会生成一个简单的for
循环做为for..of
循环,比如:
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 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
的文件都被当成一个模块。
任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export
关键字来导出。
export interface StringValidator { isAcceptable(s: string): boolean;}
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 };
我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
export class ParseIntBasedZipCodeValidator { isAcceptable(s: string) { return s.length === 5 && parseInt(s).toString() === s; }}// 导出原先的验证器但做了重命名export {ZipCodeValidator as RegExpBasedZipCodeValidator} from "./ZipCodeValidator";
或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from "module"
。
export * from "./StringValidator"; // exports interface StringValidatorexport * from "./LettersOnlyValidator"; // exports class LettersOnlyValidatorexport * 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。
declare let $: JQuery;export default $;
import $ from "JQuery";$("button.continue").html( "Next Step..." );
类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。
export default class ZipCodeValidator { static numberRegexp = /^[0-9]+$/; isAcceptable(s: string) { return s.length === 5 && ZipCodeValidator.numberRegexp.test(s); }}
import validator from "./ZipCodeValidator";let myValidator = new validator();
或者
const numberRegexp = /^[0-9]+$/;export default function (s: string) { return s.length === 5 && numberRegexp.test(s);}
import validate from "./StaticZipCodeValidator";let strings = ["Hello", "98052", "101"];// Use function validatestrings.forEach(s => { console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);});
default
导出也可以是一个值
export default "123";
import num from "./OneTwoThree";console.log(num); // "123"
export =
和 import = require()
CommonJS和AMD都有一个exports
对象的概念,它包含了一个模块的所有导出内容。
它们也支持把exports
替换为一个自定义对象。 默认导出就好比这样一个功能;然而,它们却并不相互兼容。 TypeScript模块支持 export =
语法以支持传统的CommonJS和AMD的工作流模型。
export =
语法定义一个模块的导出对象。 它可以是类,接口,命名空间,函数或枚举。
若要导入一个使用了export =
的模块时,必须使用TypeScript提供的特定语法import let = require("module")
。
let numberRegexp = /^[0-9]+$/;class ZipCodeValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); }}export = ZipCodeValidator;
import zip = require("./ZipCodeValidator");// Some samples to trylet strings = ["Hello", "98052", "101"];// Validators to uselet validator = new zip();// Show whether each string passed each validatorstrings.forEach(s => { console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);});
根据编译时指定的模块目标参数,编译器会生成相应的供Node.js (CommonJS),Require.js (AMD),isomorphic (UMD), SystemJS或ECMAScript 2015 native modules (ES6)模块加载系统使用的代码。 想要了解生成代码中define
,require
和 register
的意义,请参考相应模块加载器的文档。
下面的例子说明了导入导出语句里使用的名字是怎么转换为相应的模块加载器代码的。
import m = require("mod");export let t = m.something + 1;
define(["require", "exports", "./mod"], function (require, exports, mod_1) { exports.t = mod_1.something + 1;});
let mod_1 = require("./mod");exports.t = mod_1.something + 1;
(function (factory) { if (typeof module === "object" && typeof module.exports === "object") { let 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) { let mod_1 = require("./mod"); exports.t = mod_1.something + 1;});
System.register(["./mod"], function(exports_1) { let mod_1; let t; return { setters:[ function (mod_1_1) { mod_1 = mod_1_1; }], execute: function() { exports_1("t", t = mod_1.something + 1); } }});
import { something } from "./mod";export let t = something + 1;
下面我们来整理一下前面的验证器实现,每个模块只有一个命名的导出。
为了编译,我们必需要在命令行上指定一个模块目标。对于Node.js来说,使用--module commonjs
; 对于Require.js来说,使用``--module amd`。比如:
tsc --module commonjs Test.ts
编译完成后,每个模块会生成一个单独的.js
文件。 好比使用了reference标签,编译器会根据 import
语句编译相应的文件。
export interface StringValidator { isAcceptable(s: string): boolean;}
import { StringValidator } from "./Validation";const lettersRegexp = /^[A-Za-z]+$/;export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); }}
import { StringValidator } from "./Validation";const numberRegexp = /^[0-9]+$/;export class ZipCodeValidator implements StringValidator { isAcceptable(s: string) { return s.length === 5 && numberRegexp.test(s); }}
import { StringValidator } from "./Validation";import { ZipCodeValidator } from "./ZipCodeValidator";import { LettersOnlyValidator } from "./LettersOnlyValidator";// Some samples to trylet strings = ["Hello", "98052", "101"];// Validators to uselet validators: { [s: string]: StringValidator; } = {};validators["ZIP code"] = new ZipCodeValidator();validators["Letters only"] = new LettersOnlyValidator();// Show whether each string passed each validatorstrings.forEach(s => { for (let name in validators) { console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`); }});
有时候,你只想在某种条件下才加载某个模块。 在TypeScript里,使用下面的方式来实现它和其它的高级加载场景,我们可以直接调用模块加载器并且可以保证类型完全。
编译器会检测是否每个模块都会在生成的JavaScript中用到。 如果一个模块标识符只在类型注解部分使用,并且完全没有在表达式中使用时,就不会生成 require
这个模块的代码。 省略掉没有用到的引用对性能提升是很有益的,并同时提供了选择性加载模块的能力。
这种模式的核心是import id = require("...")
语句可以让我们访问模块导出的类型。 模块加载器会被动态调用(通过 require
),就像下面if
代码块里那样。 它利用了省略引用的优化,所以模块只在被需要时加载。 为了让这个模块工作,一定要注意 import
定义的标识符只能在表示类型处使用(不能在会转换成JavaScript的地方)。
为了确保类型安全性,我们可以使用typeof
关键字。 typeof
关键字,当在表示类型的地方使用时,会得出一个类型值,这里就表示模块的类型。
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("...")) { /* ... */ }}
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("...")) { /* ... */ } });}
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编写的类库的类型,我们需要声明类库所暴露出的API。
我们叫它声明因为它不是“外部程序”的具体实现。 它们通常是在 .d.ts
文件里定义的。 如果你熟悉C/C++,你可以把它们当做 .h
文件。 让我们看一些例子。
在Node.js里大部分工作是通过加载一个或多个模块实现的。 我们可以使用顶级的 export
声明来为每个模块都定义一个.d.ts
文件,但最好还是写在一个大的.d.ts
文件里。 我们使用与构造一个外部命名空间相似的方法,但是这里使用 module
关键字并且把名字用引号括起来,方便之后import
。 例如:
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;}
现在我们可以/// <reference>
node.d.ts
并且使用import url = require("url");
加载模块。
/// <reference path="node.d.ts"/>import * as URL from "url";let myUrl = URL.parse("http://www.typescriptlang.org");
假如你不想在使用一个新模块之前花时间去编写声明,你可以采用声明的简写形式以便能够快速使用它。
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或Isomorphic模块为代表。 这些库可以通过导入的形式或全局变量的形式访问。 例如:
export const 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);
用户应该更容易地使用你模块导出的内容。 嵌套层次过多会变得难以处理,因此仔细考虑一下如何组织你的代码。
从你的模块中导出一个命名空间就是一个增加嵌套的例子。 虽然命名空间有时候有它们的用处,在使用模块的时候它们额外地增加了一层。 这对用户来说是很不便的并且通常是多余的。
导出类的静态方法也有同样的问题 - 这个类本身就增加了一层嵌套。 除非它能方便表述或便于清晰使用,否则请考虑直接导出一个辅助方法。
class
或 function
,使用 export default
就像“在顶层上导出”帮助减少用户使用的难度,一个默认的导出也能起到这个效果。 如果一个模块就是为了导出特定的内容,那么你应该考虑使用一个默认导出。 这会令模块的导入和使用变得些许简单。 比如:
export default class SomeType { constructor() { ... }}
export default function getThing() { return 'thing'; }
import t from "./MyClass";import f from "./MyFunc";let x = new t();console.log(f());
对用户来说这是最理想的。他们可以随意命名导入模块的类型(本例为t
)并且不需要多余的(.)来找到相关对象。
export class SomeType { /* ... */ }export function someFunc() { /* ... */ }
相反地,当导入的时候:
import { SomeType, SomeFunc } from "./MyThings";let x = new SomeType();let y = someFunc();
export class Dog { ... }export class Cat { ... }export class Tree { ... }export class Flower { ... }
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 handelChar(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.handelChar(input[i]); } console.log(`result of '${input}' is '${c.getResult()}'`);}
这是使用导出的test
函数来测试计算器。
import { Calculator, test } from "./Calculator";let c = new Calculator();test(c, "1+2*33/11="); // prints 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(); if (base <= 0 || base > ProgrammerCalculator.digits.length) { throw new Error("base has to be within 0 to 16 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 Calculatorexport { ProgrammerCalculator as Calculator };// Also, export the helper functionexport { test } from "./Calculator";
新的ProgrammerCalculator
模块导出的API与原先的Calculator
模块很相似,但却没有改变原模块里的对象。 下面是测试ProgrammerCalculator类的代码:
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
-- 两个类型的名字相同,但命名空间不同。 然而,这对于模块来说却不是一个问题。 在一个模块里,没有理由两个对象拥有同一个名字。 从模块的使用角度来说,使用者会挑出他们用来引用模块的名字,所以也没有理由发生重名的情况。
更多关于模块和命名空间的资料查看[命名空间和模块](./Namespaces and Modules.md)
以下均为模块结构上的危险信号。重新检查以确保你没有在对模块使用命名空间:
export namespace Foo { ... }
(删除Foo
并把所有内容向上层移动一层)export class
或export function
(考虑使用export default
)export namespace Foo {
(不要以为这些会合并到一个Foo
中!)关于术语的一点说明: 请务必注意一点,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 trylet strings = ["Hello", "98052", "101"];// Validators to uselet validators: { [s: string]: StringValidator; } = {};validators["ZIP code"] = new ZipCodeValidator();validators["Letters only"] = new LettersOnlyValidator();// Show whether each string passed each validatorstrings.forEach(s => { for (let name in validators) { console.log(""" + s + "" " + (validators[name].isAcceptable(s) ? " 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 trylet strings = ["Hello", "98052", "101"];// Validators to uselet validators: { [s: string]: Validation.StringValidator; } = {};validators["ZIP code"] = new Validation.ZipCodeValidator();validators["Letters only"] = new Validation.LettersOnlyValidator();// Show whether each string passed each validatorstrings.forEach(s => { for (let name in validators) { console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches" : "does not match" } ${ name }`); }});
当应用变得越来越大时,我们需要将代码分离到不同的文件中以便于维护。
现在,我们把Validation
命名空间分割成多个文件。 尽管是不同的文件,它们仍是同一个命名空间,并且在使用的时候就如同它们在一个文件中定义的一样。 因为不同文件之间存在依赖关系,所以我们加入了引用标签来告诉编译器文件之间的关联。 我们的测试代码保持不变。
namespace Validation { export interface StringValidator { isAcceptable(s: string): boolean; }}
/// <reference path="Validation.ts" />namespace Validation { const lettersRegexp = /^[A-Za-z]+$/; export class LettersOnlyValidator implements StringValidator { isAcceptable(s: string) { return lettersRegexp.test(s); } }}
/// <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); } }}
/// <reference path="Validation.ts" />/// <reference path="LettersOnlyValidator.ts" />/// <reference path="ZipCodeValidator.ts" />// Some samples to trylet strings = ["Hello", "98052", "101"];// Validators to uselet validators: { [s: string]: Validation.StringValidator; } = {};validators["ZIP code"] = new Validation.ZipCodeValidator();validators["Letters only"] = new Validation.LettersOnlyValidator();// Show whether each string passed each validatorstrings.forEach(s => { 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文件按正确的顺序引进来,比如:
<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
会生成与原始符号不同的引用,所以改变别名的var
值并不会影响原始变量的值。
为了描述不是用TypeScript编写的类库的类型,我们需要声明类库导出的API。 由于大部分程序库只提供少数的顶级对象,命名空间是用来表示它们的一个好办法。
我们称其为声明是因为它不是外部程序的具体实现。 我们通常在 .d.ts
里写这些声明。 如果你熟悉C/C++,你可以把它们当做 .h
文件。 让我们看一些例子。
流行的程序库D3在全局对象d3
里定义它的功能。 因为这个库通过一个 <script>
标签加载(不是通过模块加载器),它的声明文件使用内部模块来定义它的类型。 为了让TypeScript编译器识别它的类型,我们使用外部命名空间声明。 比如,我们可以像下面这样写:
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 let d3: D3.Base;
关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做“命名空间”。 “外部模块”现在则简称为“模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说
module X {
相当于现在推荐的写法namespace X {
)。
这篇文章将概括介绍在TypeScript里使用模块与命名空间来组织代码的方法。 我们也会谈及命名空间和模块的高级使用场景,和在使用它们的过程中常见的陷阱。
查看模块章节了解关于模块的更多信息。 查看 命名空间章节了解关于命名空间的更多信息。
命名空间是位于全局命名空间下的一个普通的带有名字的JavaScript对象。 这令命名空间十分容易使用。 它们可以在多文件中同时使用,并通过 --outFile
结合在一起。 命名空间是帮你组织Web应用不错的方式,你可以把所有依赖都放在HTML页面的 <script>
标签里。
但就像其它的全局命名空间污染一样,它很难去识别组件之间的依赖关系,尤其是在大型的应用中。
像命名空间一样,模块可以包含代码和声明。 不同的是模块可以 声明它的依赖。
模块会把依赖添加到模块加载器上(例如CommonJs / Require.js)。 对于小型的JS应用来说可能没必要,但是对于大型应用,这一点点的花费会带来长久的模块化和可维护性上的便利。 模块也提供了更好的代码重用,更强的封闭性以及更好的使用工具进行优化。
对于Node.js应用来说,模块是默认并推荐的组织代码的方式。
从ECMAScript 2015开始,模块成为了语言内置的部分,应该会被所有正常的解释引擎所支持。 因此,对于新项目来说推荐使用模块做为组织代码的方式。
这部分我们会描述常见的命名空间和模块的使用陷阱和如何去避免它们。
/// <reference>
一个常见的错误是使用/// <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例子中引用 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以上的版本能够使用outFile
当目标为amd
或system
。
这节假设你已经了解了模块的一些基本知识 请阅读 模块文档了解更多信息。
模块解析就是指编译器所要依据的一个流程,用它来找出某个导入操作所引用的具体值。 假设有一个导入语句import { a } from "moduleA"
; 为了去检查任何对 a
的使用,编译器需要准确的知道它表示什么,并且会需要检查它的定义moduleA
。
这时候,编译器会想知道“moduleA
的shape是怎样的?” 这听上去很简单, moduleA
可能在你写的某个.ts
/.tsx
文件里或者在你的代码所依赖的.d.ts
里。
首先,编译器会尝试定位表示导入模块的文件。 编译会遵循下列二种策略之一: Classic或Node。 这些策略会告诉编译器到 哪里去查找moduleA
。
如果它们失败了并且如果模块名是非相对的(且是在"moduleA"
的情况下),编译器会尝试定位一个外部模块声明。 我们接下来会讲到非相对导入。
最后,如果编译器还是不能解析这个模块,它会记录一个错误。 在这种情况下,错误可能为 error TS2307: Cannot find module 'moduleA'.
根据模块引用是相对的还是非相对的,模块导入会以不同的方式解析。
相对导入是以/
,./
或../
开头的。 下面是一些例子:
import Entry from "./components/Entry";
import { DefaultHeaders } from "../constants/http";
import "/mod";
所有其它形式的导入被当作非相对的。 下面是一些例子:
import * as $ from "jQuery";
import { Component } from "angular2/core";
相对导入解析时是相对于导入它的文件来的,并且不能解析为一个外部模块声明。 你应该为你自己写的模块使用相对导入,这样能确保它们在运行时的相对位置。
共有两种可用的模块解析策略:Node和Classic。 你可以使用 --moduleResolution
标记为指定使用哪个。 默认值为 Node。
这种策略以前是TypeScript默认的解析策略。 现在,它存在的理由主要是为了向后兼容。
相对导入的模块是相对于导入它的文件进行解析的。 因此 /root/src/folder/A.ts
文件里的import { b } from "./moduleB"
会使用下面的查找流程:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
对于非相对模块的导入,编译器则会从包含导入文件的目录开始依次向上级目录遍历,尝试定位匹配的声明文件。
比如:
有一个对moduleB
的非相对导入import { b } from "moduleB"
,它是在/root/src/folder/A.ts
文件里,会以如下的方式来定位"moduleB"
:
/root/src/folder/moduleB.ts
/root/src/folder/moduleB.d.ts
/root/src/moduleB.ts
/root/src/moduleB.d.ts
/root/moduleB.ts
/root/moduleB.d.ts
/moduleB.ts
/moduleB.d.ts
这个解析策略试图在运行时模仿Node.js模块解析机制。 完整的Node.js解析算法可以在 Node.js module documentation找到。
为了理解TypeScript编译依照的解析步骤,先弄明白Node.js模块是非常重要的。 通常,在Node.js里导入是通过require
函数调用进行的。 Node.js会根据 require
的是相对路径还是非相对路径做出不同的行为。
相对路径很简单。 例如,假设有一个文件路径为 /root/src/moduleA.js
,包含了一个导入var x = require("./moduleB");
Node.js以下面的顺序解析这个导入:
将/root/src/moduleB.js
视为文件,检查是否存在。
将/root/src/moduleB
视为目录,检查是否它包含package.json
文件并且其指定了一个"main"
模块。 在我们的例子里,如果Node.js发现文件 /root/src/moduleB/package.json
包含了{ "main": "lib/mainModule.js" }
,那么Node.js会引用/root/src/moduleB/lib/mainModule.js
。
将/root/src/moduleB
视为目录,检查它是否包含index.js
文件。 这个文件会被隐式地当作那个文件夹下的"main"模块。
你可以阅读Node.js文档了解更多详细信息:file modules 和 folder modules。
但是,非相对模块名的解析是个完全不同的过程。 Node会在一个特殊的文件夹 node_modules
里查找你的模块。node_modules
可能与当前文件在同一级目录下,或者在上层目录里。 Node会向上级目录遍历,查找每个node_modules
直到它找到要加载的模块。
还是用上面例子,但假设/root/src/moduleA.js
里使用的是非相对路径导入var x = require("moduleB");
。 Node则会以下面的顺序去解析 moduleB
,直到有一个匹配上。
/root/src/node_modules/moduleB.js
/root/src/node_modules/moduleB/package.json
(如果指定了"main"
属性)/root/src/node_modules/moduleB/index.js
/root/node_modules/moduleB.js
/root/node_modules/moduleB/package.json
(如果指定了"main"
属性)/root/node_modules/moduleB/index.js
/node_modules/moduleB.js
/node_modules/moduleB/package.json
(如果指定了"main"
属性)/node_modules/moduleB/index.js
注意Node.js在步骤(4)和(7)会向上跳一级目录。
你可以阅读Node.js文档了解更多详细信息:loading modules from node_modules
。
TypeScript是模仿Node.js运行时的解析策略来在编译阶段定位模块定义文件。 因此,TypeScript在Node解析逻辑基础上增加了TypeScript源文件的扩展名( .ts
,.tsx
和.d.ts
)。 同时,TypeScript在 package.json
里使用字段"typings"
来表示类似"main"
的意义 - 编译器会使用它来找到要使用的"main"定义文件。
比如,有一个导入语句import { b } from "./moduleB"
在/root/src/moduleA.ts
里,会以下面的流程来定位"./moduleB"
:
/root/src/moduleB.ts
/root/src/moduleB.tsx
/root/src/moduleB.d.ts
/root/src/moduleB/package.json
(如果指定了"typings"
属性)/root/src/moduleB/index.ts
/root/src/moduleB/index.tsx
/root/src/moduleB/index.d.ts
回想一下Node.js先查找moduleB.js
文件,然后是合适的package.json
,再之后是index.js
。
类似地,非相对的导入会遵循Node.js的解析逻辑,首先查找文件,然后是合适的文件夹。 因此/src/moduleA.ts
文件里的import { b } from "moduleB"
会以下面的查找顺序解析:
/root/src/node_modules/moduleB.ts
/root/src/node_modules/moduleB.tsx
/root/src/node_modules/moduleB.d.ts
/root/src/node_modules/moduleB/package.json
(如果指定了"typings"
属性)/root/src/node_modules/moduleB/index.ts
/root/src/node_modules/moduleB/index.tsx
/root/src/node_modules/moduleB/index.d.ts
/root/node_modules/moduleB.ts
/root/node_modules/moduleB.tsx
/root/node_modules/moduleB.d.ts
/root/node_modules/moduleB/package.json
(如果指定了"typings"
属性)/root/node_modules/moduleB/index.ts
/root/node_modules/moduleB/index.tsx
/root/node_modules/moduleB/index.d.ts
/node_modules/moduleB.ts
/node_modules/moduleB.tsx
/node_modules/moduleB.d.ts
/node_modules/moduleB/package.json
(如果指定了"typings"
属性)/node_modules/moduleB/index.ts
/node_modules/moduleB/index.tsx
/node_modules/moduleB/index.d.ts
不要被这里步骤的数量吓到 - TypeScript只是在步骤(8)和(15)向上跳了两次目录。 这并不比Node.js里的流程复杂。
--noResolve
正常来讲编译器会在开始编译之前解析模块导入。 每当它成功地解析了对一个文件 import
,这个文件被会加到一个文件列表里,以供编译器稍后处理。
--noResolve
编译选项告诉编译器不要添加任何不是在命令行上传入的文件到编译列表。 编译器仍然会尝试解析模块,但是只要没有指定这个文件,那么它就不会被包含在内。
比如
import * as A from "moduleA" // OK, moduleA passed on the command-lineimport * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
tsc app.ts moduleA.ts --noResolve
使用--noResolve
编译app.ts
:
moduleA
,因为它在命令行上指定了。moduleB
,因为没有在命令行上传递。exclude
列表里的模块还会被编译器使用tsconfig.json
将文件夹转变一个“工程” 如果不指定任何 “exclude”
或“files”
,文件夹里的所有文件包括tsconfig.json
和所有的子目录都会在编译列表里。 如果你想利用 “exclude”
排除某些文件,甚至你想指定所有要编译的文件列表,请使用“files”
。
有些是被tsconfig.json
自动加入的。 它不会涉及到上面讨论的模块解析。 如果编译器识别出一个文件是模块导入目标,它就会加到编译列表里,不管它是否被排除了。
因此,要从编译列表中排除一个文件,你需要在排除它的同时,还要排除所有对它进行import
或使用了/// <reference path="..." />
指令的文件。
介绍
TypeScript中有些独特的概念可以在类型层面上描述JavaScript对象的模型。 这其中尤其独特的一个例子是“声明合并”的概念。 理解了这个概念,将有助于操作现有的JavaScript代码。 同时,也会有助于理解更多高级抽象的概念。
对本文件来讲,“声明合并”是指编译器将针对同一个名字的两个独立声明合并为单一声明。 合并后的声明同时拥有原先两个声明的特性。 任何数量的声明都可被合并;不局限于两个声明。
Typescript中的声明会创建以下三种实体之一:命名空间,类型或值。 创建命名空间的声明会新建一个命名空间,它包含了用(.)符号来访问时使用的名字。 创建类型的声明是:用声明的模型创建一个类型并绑定到给定的名字上。 最后,创建值的声明会创建在JavaScript输出中看到的值。
Declaration Type | Namespace | Type | Value |
---|---|---|---|
Namespace | X | X | |
Class | X | X | |
Enum | X | X | |
Interface | X | ||
Type Alias | X | ||
Function | X | ||
Variable | X |
理解每个声明创建了什么,有助于理解当声明合并时有哪些东西被合并了。
最简单也最常见的声明合并类型是接口合并。 从根本上说,合并的机制是把双方的成员放到一个同名的接口里。
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;}
与接口相似,同名的命名空间也会合并其成员。 命名空间会创建出命名空间和值,我们需要知道这两者都是怎么合并的。
对于命名空间的合并,模块导出的同名接口进行合并,构成单一命名空间内含合并后的接口。
对于命名空间里值的合并,如果当前已经存在给定名字的命名空间,那么后来的命名空间的导出成员会被加到已经存在的那个模块里。
Animals
声明合并示例:
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, haveMuscles is not visible here }}
因为haveMuscles
并没有导出,只有animalsHaveMuscles
函数共享了原始未合并的命名空间可以访问这个变量。 doAnimalsHaveMuscles
函数虽是合并命名空间的一部分,但是访问不了未导出的成员。
命名空间可以与其它类型的声明进行合并。 只要命名空间的定义符合将要合并类型的定义。合并结果包含两者的声明类型。 Typescript使用这个功能去实现一些JavaScript里的设计模式。
这让我们可以表示内部类。
class Album { label: Album.AlbumLabel;}namespace Album { export class AlbumLabel { }}
合并规则与上面合并命名空间
小节里讲的规则一致,我们必须导出AlbumLabel
类,好让合并的类能访问。 合并结果是一个类并带有一个内部类。 你也可以使用命名空间为类增加一些静态属性。
除了内部类的模式,你在JavaScript里,创建一个函数稍后扩展它增加一些属性也是很常见的。 Typescript使用声明合并来达到这个目的并保证类型安全。
function buildLabel(name: string): string { return buildLabel.prefix + name + buildLabel.suffix;}namespace buildLabel { export let suffix = ""; export let prefix = "Hello, ";}alert(buildLabel("Sam Smith"));
相似的,命名空间可以用来扩展枚举型:
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; } }}
TypeScript并非允许所有的合并。 目前,类不能与其它类或变量合并。 想要了解如何模仿类的合并,请参考TypeScript的混入。
虽然JavaScript不支持合并,但你可以为导入的对象打补丁以更新它们。让我们考察一下这个玩具性的示例:
// observable.jsexport class Observable<T> { // ... implementation left as an exercise for the reader ...}// map.jsimport { Observable } from "./observable";Observable.prototype.map = function (f) { // ... another exercise for the reader}
它也可以很好地工作在TypeScript中, 但编译器对 Observable.prototype.map
一无所知。 你可以使用扩展模块来将它告诉编译器:
// observable.ts stays the same// map.tsimport { Observable } from "./observable";declare module "./observable" { interface Observable<T> { map<U>(f: (x: T) => U): Observable<U>; }}Observable.prototype.map = function (f) { // ... another exercise for the reader}// consumer.tsimport { Observable } from "./observable";import "./map";let o: Observable<number>;o.map(x => x.toFixed());
模块名的解析和用import
/export
解析模块标识符的方式是一致的。 更多信息请参考 Modules。 当这些声明在扩展中合并时,就好像在原始位置被声明了一样。但是,你不能在扩展中声明新的顶级声明--仅可以扩展模块中已经存在的声明。
你也以在模块内部添加声明到全局作用域中。
// observable.tsexport class Observable<T> { // ... still no implementation ...}declare global { interface Array<T> { toObservable(): Observable<T>; }}Array.prototype.toObservable = function () { // ...}
全局扩展与模块扩展的行为和限制是相同的。
JSX是一种嵌入式的类似XML的语法。 它可以被转换成合法的JavaScript,尽管转换的语义是依据不同的实现而定的。 JSX因 React框架而流行,但是也被其它应用所使用。 TypeScript支持内嵌,类型检查和将JSX直接编译为JavaScript。
想要使用JSX必须做两件事:
.tsx
扩展名jsx
选项TypeScript具有两种JSX模式:preserve
和react
。 这些模式只在代码生成阶段起作用 - 类型检查并不受影响。 在 preserve
模式下生成代码中会保留JSX以供后续的转换操作使用(比如:Babel)。 另外,输出文件会带有.jsx
扩展名。 react
模式会生成React.createElement
,在使用前不需要再进行转换操作了,输出文件的扩展名为.js
。
模式 | 输入 | 输出 | 输出文件扩展名 |
---|---|---|---|
preserve | <div /> | <div /> | .jsx |
react | <div /> | React.createElement("div") | .js |
你可以通过在命令行里使用--jsx
标记或tsconfig.json里的选项来指定模式。
注意:
React
标识符是写死的硬代码,所以你必须保证React(大写的R)是可用的。 Note: The identifierReact
is hard-coded, so you must make React available with an uppercase R.
as
操作符回想一下怎么写类型断言:
var foo = <foo>bar;
这里我们断言bar
变量是foo
类型的。 因为TypeScript也使用尖括号来表示类型断言,JSX的语法带来了解析的困难。因此,TypeScript在 .tsx
文件里禁用了使用尖括号的类型断言。
为了弥补.tsx
里的这个功能,新加入了一个类型断言符号:as
。 上面的例子可以很容易地使用 as
操作符改写:
var foo = bar as foo;
as
操作符在.ts
和.tsx
里都可用,并且与其它类型断言行为是等价的。
为了理解JSX的类型检查,你必须首先理解固有元素与基于值的元素之间的区别。 假设有这样一个JSX表达式 <expr />
,expr
可能引用环境自带的某些东西(比如,在DOM环境里的div
或span
)或者是你自定义的组件。 这是非常重要的,原因有如下两点:
React.createElement("div")
),然而由你自定义的组件却不会生成(React.createElement(MyComponent)
)。TypeScript使用与React相同的规范 来区别它们。 固有元素总是以一个小写字母开头,基于值的元素总是以一个大写字母开头。
固有元素使用特殊的接口JSX.IntrinsicElements
来查找。 默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。 然而,如果接口存在,那么固有元素的名字需要在 JSX.IntrinsicElements
接口的属性里查找。 例如:
declare namespace JSX { interface IntrinsicElements { foo: any }}<foo />; // 正确<bar />; // 错误
在上例中,<foo />
没有问题,但是<bar />
会报错,因为它没在JSX.IntrinsicElements
里指定。
注意:你也可以在
JSX.IntrinsicElements
上指定一个用来捕获所有字符串索引:declare namespace JSX { interface IntrinsicElements { [elemName: string]: any; }}
基于值的元素会简单的在它所在的作用域里按标识符查找。
import MyComponent from "./myComponent";<MyComponent />; // 正确<SomeOtherComponent />; // 错误
可以限制基于值的元素的类型。 然而,为了这么做我们需要引入两个新的术语: 元素类的类型和元素实例的类型。
现在有<Expr />
,元素类的类型为Expr
的类型。 所以在上面的例子里,如果 MyComponent
是ES6的类,那么它的类类型就是这个类。 如果 MyComponent
是个工厂函数,类类型为这个函数。
一旦建立起了类类型,实例类型就确定了,为类类型调用签名的返回值与构造签名的联合类型。 再次说明,在ES6类的情况下,实例类型为这个类的实例的类型,并且如果是工厂函数,实例类型为这个函数返回值类型。
class MyComponent { render() {}}// 使用构造签名var myComponent = new MyComponent();// 元素类的类型 => MyComponent// 元素实例的类型 => { render: () => void }function MyFactoryFunction() { return { render: () => { } }}// 使用调用签名var myComponent = MyFactoryFunction();// 元素类的类型 => FactoryFunction// 元素实例的类型 => { render: () => void }
元素的实例类型很有趣,因为它必须赋值给JSX.ElementClass
或抛出一个错误。 默认的 JSX.ElementClass
为{}
,但是它可以被扩展用来限制JSX的类型以符合相应的接口。
declare namespace JSX { interface ElementClass { render: any; }}class MyComponent { render() {}}function MyFactoryFunction() { return { render: () => {} }}<MyComponent />; // 正确<MyFactoryFunction />; // 正确class NotAValidComponent {}function NotAValidFactoryFunction() { return {};}<NotAValidComponent />; // 错误<NotAValidFactoryFunction />; // 错误
属性类型检查的第一步是确定元素属性类型。 这在固有元素和基于值的元素之间稍有不同。
对于固有元素,这是JSX.IntrinsicElements
属性的类型。
declare namespace JSX { interface IntrinsicElements { foo: { bar?: boolean } }}// `foo`的元素属性类型为`{bar?: boolean}`<foo bar />;
对于基于值的元素,就稍微复杂些。 它取决于先前确定的在元素实例类型上的某个属性的类型。 至于该使用哪个属性来确定类型取决于 JSX.ElementAttributesProperty
。 它应该使用单一的属性来定义。 这个属性名之后会被使用。
declare namespace JSX { interface ElementAttributesProperty { props; // 指定用来使用的属性名 }}class MyComponent { // 在元素实例类型上指定属性 props: { foo?: string; }}// `MyComponent`的元素属性类型为`{foo?: string}`<MyComponent foo="bar" />
元素属性类型用于的JSX里进行属性的类型检查。 支持可选属性和必须属性。
declare namespace JSX { interface IntrinsicElements { foo: { requiredProp: string; optionalProp?: number } }}<foo requiredProp="bar" />; // 正确<foo requiredProp="bar" optionalProp={0} />; // 正确<foo />; // 错误, 缺少 requiredProp<foo requiredProp={0} />; // 错误, requiredProp 应该是字符串<foo requiredProp="bar" unknownProp />; // 错误, unknownProp 不存在<foo requiredProp="bar" some-unknown-prop />; // 正确, `some-unknown-prop`不是个合法的标识符
注意:如果一个属性名不是个合法的JS标识符(像
data-*
属性),并且它没出现在元素属性类型里时不会当做一个错误。
延展操作符也可以使用:
var props = { requiredProp: 'bar' };<foo {...props} />; // 正确var badProps = {};<foo {...badProps} />; // 错误
默认地JSX表达式结果的类型为any。 你可以自定义这个类型,通过指定
JSX.Element`接口。 然而,不能够从接口里检索元素,属性或JSX的子元素的类型信息。 它是一个黑盒。
JSX允许你使用{ }
标签来内嵌表达式。
var a = <div> {['foo', 'bar'].map(i => <span>{i / 2}</span>)}</div>
上面的代码产生一个错误,因为你不能用数字来除以一个字符串。 输出如下,若你使用了 preserve
选项:
var a = <div> {['foo', 'bar'].map(function (i) { return <span>{i / 2}</span>; })}</div>
要想一起使用JSX和React,你应该使用React类型定义。 这些类型声明定义了 JSX
合适命名空间来使用React。
/// <reference path="react.d.ts" />interface Props { foo: string;}class MyComponent extends React.Component<Props, {}> { render() { return <span>{this.props.foo}</span> }}<MyComponent foo="bar" />; // 正确<MyComponent foo={0} />; // 错误
随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。 装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。 Javascript里的装饰器目前处在 建议征集的第一阶段,但在TypeScript里已做为一项实验性特性予以支持。
注意 装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json
里启用experimentalDecorators
编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators
tsconfig.json:
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true }}
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。 装饰器使用@expression
这种形式,expression
求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
例如,有一个@sealed
装饰器,我们会这样定义sealed
函数:
function sealed(target) { // do something with "target" ...}
注意 后面类装饰器小节里有一个更加详细的例子。
如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。 装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
我们可以通过下面的方式来写一个装饰器工厂函数:
function color(value: string) { // 这是一个装饰器工厂 return function (target) { // 这是装饰器 // do something with "target" and "value"... }}
注意 下面方法装饰器小节里有一个更加详细的例子。
多个装饰器可以同时应用到一个声明上,就像下面的示例:
@f @g x
@f@gx
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合f和g时,复合的结果(f ∘ g)(x)等同于f(g(x))。
同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
如果我们使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:
function f() { console.log("f(): evaluated"); return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("f(): called"); }}function g() { console.log("g(): evaluated"); return function (target, propertyKey: string, descriptor: PropertyDescriptor) { console.log("g(): called"); }}class C { @f() @g() method() {}}
在控制台里会打印出如下结果:
f(): evaluatedg(): evaluatedg(): calledf(): called
类中不同声明上的装饰器将按以下规定的顺序应用:
类装饰器在类声明之前被声明(紧靠着类声明)。 类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。 类装饰器不能用在声明文件中( .d.ts
),也不能用在任何外部上下文中(比如declare
的类)。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
注意 如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。 在运行时的装饰器调用逻辑中 不会为你做这些。
下面是使用类装饰器(@sealed
)的例子,应用在Greeter
类:
@sealedclass Greeter { greeting: string; constructor(message: string) { this.greeting = message; } greet() { return "Hello, " + this.greeting; }}
我们可以这样定义@sealed
装饰器:
function sealed(constructor: Function) { Object.seal(constructor); Object.seal(constructor.prototype);}
当@sealed
被执行的时候,它将密封此类的构造函数和原型。(注:参见Object.seal)
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。 它会被应用到方法的 属性描述符上,可以用来监视,修改或者替换方法定义。 方法装饰器不能用在声明文件( .d.ts
),重载或者任何外部上下文(比如declare
的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
注意 如果代码输出目标版本小于
ES5
,Property Descriptor将会是undefined
。
如果方法装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
返回值会被忽略。
下面是一个方法装饰器(@enumerable
)的例子,应用于Greeter
类的方法上:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } @enumerable(false) greet() { return "Hello, " + this.greeting; }}
我们可以用下面的函数声明来定义@enumerable
装饰器:
function enumerable(value: boolean) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.enumerable = value; };}
这里的@enumerable(false)
是一个装饰器工厂。 当装饰器 @enumerable(false)
被调用时,它会修改属性描述符的enumerable
属性。
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。 访问器装饰器应用于访问器的 属性描述符并且可以用来监视,修改或替换一个访问器的定义。 访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare
的类)里。
注意 TypeScript不允许同时装饰一个成员的
get
和set
访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get
和set
访问器,而不是分开声明的。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
注意 如果代码输出目标版本小于
ES5
,Property Descriptor将会是undefined
。
如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
返回值会被忽略。
下面是使用了访问器装饰器(@configurable
)的例子,应用于Point
类的成员上:
class Point { private _x: number; private _y: number; constructor(x: number, y: number) { this._x = x; this._y = y; } @configurable(false) get x() { return this._x; } @configurable(false) get y() { return this._y; }}
我们可以通过如下函数声明来定义@configurable
装饰器:
function configurable(value: boolean) { return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) { descriptor.configurable = value; };}
属性装饰器声明在一个属性声明之前(紧靠着属性声明)。 属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare
的类)里。
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
注意 属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。 因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。 因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
如果属性装饰器返回一个值,它会被用作方法的属性描述符。
注意 如果代码输出目标版本小于
ES5
,返回值会被忽略。
如果访问符装饰器返回一个值,它会被用作方法的属性描述符。
我们可以用它来记录这个属性的元数据,如下例所示:
class Greeter { @format("Hello, %s") greeting: string; constructor(message: string) { this.greeting = message; } greet() { let formatString = getFormat(this, "greeting"); return formatString.replace("%s", this.greeting); }}
然后定义@format
装饰器和getFormat
函数:
import "reflect-metadata";const formatMetadataKey = Symbol("format");function format(formatString: string) { return Reflect.metadata(formatMetadataKey, formatString);}function getFormat(target: any, propertyKey: string) { return Reflect.getMetadata(formatMetadataKey, target, propertyKey);}
这个@format("Hello, %s")
装饰器是个 装饰器工厂。 当 @format("Hello, %s")
被调用时,它添加一条这个属性的元数据,通过reflect-metadata
库里的Reflect.metadata
函数。 当 getFormat
被调用时,它读取格式的元数据。
注意 这个例子需要使用
reflect-metadata
库。 查看 元数据了解reflect-metadata
库更详细的信息。
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。 参数装饰器应用于类构造函数或方法声明。 参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare
的类)里。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
注意 参数装饰器只能用来监视一个方法的参数是否被传入。
参数装饰器的返回值会被忽略。
下例定义了参数装饰器(@required
)并应用于Greeter
类方法的一个参数:
class Greeter { greeting: string; constructor(message: string) { this.greeting = message; } @validate greet(@required name: string) { return "Hello " + name + ", " + this.greeting; }}
然后我们使用下面的函数定义 @required
和 @validate
装饰器:
import "reflect-metadata";const requiredMetadataKey = Symbol("required");function required(target: Object, propertyKey: string | symbol, parameterIndex: number) { let existingRequiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyKey) || []; existingRequiredParameters.push(parameterIndex); Reflect.defineMetadata(requiredMetadataKey, existingRequiredParameters, target, propertyKey);}function validate(target: any, propertyName: string, descriptor: TypedPropertyDescriptor<Function>) { let method = descriptor.value; descriptor.value = function () { let requiredParameters: number[] = Reflect.getOwnMetadata(requiredMetadataKey, target, propertyName); if (requiredParameters) { for (let parameterIndex of requiredParameters) { if (parameterIndex >= arguments.length || arguments[parameterIndex] === undefined) { throw new Error("Missing required argument."); } } } return method.apply(this, arguments); }}
@required
装饰器添加了元数据实体把参数标记为必需的。 @validate
装饰器把greet
方法包裹在一个函数里在调用原先的函数前验证函数参数。
注意 这个例子使用了
reflect-metadata
库。 查看 元数据了解reflect-metadata
库的更多信息。
一些例子使用了reflect-metadata
库来支持实验性的metadata API。 这个库还不是ECMAScript (JavaScript)标准的一部分。 然而,当装饰器被ECMAScript官方标准采纳后,这些扩展也将被推荐给ECMAScript以采纳。
你可以通过npm安装这个库:
npm i reflect-metadata --save
TypeScript支持为带有装饰器的声明生成元数据。 你需要在命令行或 tsconfig.json
里启用emitDecoratorMetadata
编译器选项。
Command Line:
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata
tsconfig.json:
{ "compilerOptions": { "target": "ES5", "experimentalDecorators": true, "emitDecoratorMetadata": true }}
当启用后,只要reflect-metadata
库被引入了,设计阶段添加的类型信息可以在运行时使用。
如下例所示:
import "reflect-metadata";class Point { x: number; y: number;}class Line { private _p0: Point; private _p1: Point; @validate set p0(value: Point) { this._p0 = value; } get p0() { return this._p0; } @validate set p1(value: Point) { this._p1 = value; } get p1() { return this._p1; }}function validate<T>(target: any, propertyKey: string, descriptor: TypedPropertyDescriptor<T>) { let set = descriptor.set; descriptor.set = function (value: T) { let type = Reflect.getMetadata("design:type", target, propertyKey); if (!(value instanceof type)) { throw new TypeError("Invalid type."); } }}
TypeScript编译器可以通过@Reflect.metadata
装饰器注入设计阶段的类型信息。 你可以认为它相当于下面的TypeScript:
class Line { private _p0: Point; private _p1: Point; @validate @Reflect.metadata("design:type", Point) set p0(value: Point) { this._p0 = value; } get p0() { return this._p0; } @validate @Reflect.metadata("design:type", Point) set p1(value: Point) { this._p1 = value; } get p1() { return this._p1; }}
注意 装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes)。
除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。 你可能在Scala等语言里对mixins及其特性已经很熟悉了,但它在JavaScript中也是很流行的。
下面的代码演示了如何在TypeScript里使用混入。 后面我们还会解释这段代码是怎么工作的。
// Disposable Mixinclass Disposable { isDisposed: boolean; dispose() { this.isDisposed = true; }}// Activatable Mixinclass Activatable { isActive: boolean; activate() { this.isActive = true; } deactivate() { this.isActive = false; }}class SmartObject implements Disposable, Activatable { constructor() { setInterval(() => console.log(this.isActive + " : " + this.isDisposed), 500); } interact() { this.activate(); } // Disposable isDisposed: boolean = false; dispose: () => void; // Activatable isActive: boolean = false; activate: () => void; deactivate: () => void;}applyMixins(SmartObject, [Disposable, Activatable]);let smartObj = new SmartObject();setTimeout(() => smartObj.interact(), 1000);////////////////////////////////////////// In your runtime library somewhere////////////////////////////////////////function applyMixins(derivedCtor: any, baseCtors: any[]) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { derivedCtor.prototype[name] = baseCtor.prototype[name]; }); });}
代码里首先定义了两个类,它们将做为mixins。 可以看到每个类都只定义了一个特定的行为或功能。 稍后我们使用它们来创建一个新类,同时具有这两种功能。
// Disposable Mixinclass Disposable { isDisposed: boolean; dispose() { this.isDisposed = true; }}// Activatable Mixinclass Activatable { isActive: boolean; activate() { this.isActive = true; } deactivate() { this.isActive = false; }}
下面创建一个类,结合了这两个mixins。 下面来看一下具体是怎么操作的:
class SmartObject implements Disposable, Activatable {
首先应该注意到的是,没使用extends
而是使用implements
。 把类当成了接口,仅使用Disposable和Activatable的类型而非其实现。 这意味着我们需要在类里面实现接口。 但是这是我们在用mixin时想避免的。
我们可以这么做来达到目的,为将要mixin进来的属性方法创建出占位属性。 这告诉编译器这些成员在运行时是可用的。 这样就能使用mixin带来的便利,虽说需要提前定义一些占位属性。
// DisposableisDisposed: boolean = false;dispose: () => void;// ActivatableisActive: boolean = false;activate: () => void;deactivate: () => void;
最后,把mixins混入定义的类,完成全部实现部分。
applyMixins(SmartObject, [Disposable, Activatable]);
最后,创建这个帮助函数,帮我们做混入操作。 它会遍历mixins上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。
function applyMixins(derivedCtor: any, baseCtors: any[]) { baseCtors.forEach(baseCtor => { Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => { derivedCtor.prototype[name] = baseCtor.prototype[name]; }) });}
三斜线指令是包含单个XML标签的单行注释。 注释的内容会做为编译器指令使用。
三斜线指令仅可放在包含它的文件的最顶端。 一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。 如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。
/// <reference path="..." />
/// <reference path="..." />
指令是三斜线指令中最常见的一种。 它用于声明文件间的 依赖。
三斜线引用告诉编译器在编译过程中要引入的额外的文件。
当使用--out
或--outFile
时,它也可以做为调整输出内容顺序的一种方法。 文件在输出文件内容中的位置与经过预处理后的输入顺序一致。
编译器会对输入文件进行预处理来解析所有三斜线引用指令。 在这个过程中,额外的文件会加到编译过程中。
这个过程会以一些根文件开始; 它们是在命令行中指定的文件或是在 tsconfig.json
中的"files"
列表里的文件。 这些根文件按指定的顺序进行预处理。 在一个文件被加入列表前,它包含的所有三斜线引用都要被处理,还有它们包含的目标。 三斜线引用以它们在文件里出现的顺序,使用深度优先的方式解析。
一个三斜线引用路径是相对于包含它的文件的,如果不是根文件。
引用不存在的文件会报错。 一个文件用三斜线指令引用自己会报错。
--noResolve
如果指定了--noResolve
编译选项,三斜线引用会被忽略;它们不会增加新文件,也不会改变给定文件的顺序。
/// <reference no-default-lib="true"/>
这个指令把一个文件标记成默认库。 你会在 lib.d.ts
文件和它不同的变体的顶端看到这个注释。
这个指令告诉编译器在编译过程中不要包含这个默认库(比如,lib.d.ts
)。 这与在命令行上使用 --noLib
相似。
还要注意,当传递了--skipDefaultLibCheck
时,编译器只会忽略检查带有/// <reference no-default-lib="true"/>
的文件。
/// <amd-module />
默认情况下生成的AMD模块都是匿名的。 但是,当一些工具需要处理生成的模块时会产生问题,比如 r.js
。
amd-module
指令允许给编译器传入一个可选的模块名:
///<amd-module name='NamedModule'/>export class C {}
这会将NamedModule
传入到AMD define
函数里:
define("NamedModule", ["require", "exports"], function (require, exports) { var C = (function () { function C() { } return C; })(); exports.C = C;});
/// <amd-dependency />
注意:这个指令被废弃了。使用
import "moduleName";
语句代替。
/// <amd-dependency path="x" />
告诉编译器有一个非TypeScript模块依赖需要被注入,做为目标模块require
调用的一部分。
amd-dependency
指令也可以带一个可选的name
属性;它允许我们为amd-dependency传入一个可选名字:
/// <amd-dependency path="legacy/moduleA" name="moduleA"/>declare var moduleA:MyTypemoduleA.callStuff()
生成的JavaScript代码:
define(["require", "exports", "legacy/moduleA"], function (require, exports, moduleA) { moduleA.callStuff()});
这篇指南的目的是教你如何书写高质量的TypeScript声明文件。
在这篇指南里,我们假设你对TypeScript已经有了基本的了解。 如果没有,请先阅读 TypeScript手册 来了解一些基本知识,尤其是类型和命名空间部分。
这篇指南被分成了以下章节。
结构一节将帮助你了解常见库的格式以及如何为每种格式书写正确的声明文件。 如果你在编辑一个已经存在的文件,那么你可能不需要阅读此章节。 如果你在书写新的声明文件,那么你必须阅读此章节以理解库的不同格式是如何影响声明文件的书写的。
声明文件里有很多常见的错误是很容易避免的。规范一节指出了常见的错误, 描述了如何发现它们, 与怎样去修复。 每个人都要阅读这个章节以了解如何避免常见错误。
很多时候,我们只能通过一些示例来了解第三方库是如何工作的,同时我们需要为这样的库书写声明文件。举例一节展示了很多常见的API模式以及如何为它们书写声明文件。 这篇指南是针对TypeScript初学者的,他们可能还不了解TypeScript里的所有语言结构。
对于那些对声明文件底层工作机制感兴趣的老手们,深入一节解释了很多高级书写声明文件的高级概念, 以及展示了如何利用这些概念来创建整洁和直观的声明文件。
在模版一节里,你能找到一些声明文件,它们可以帮助你快速开始 当你在书写一个新声明文件的时候。 参考结构这篇文档来找到应该使用哪个模版文件。
发布一节讲解了如何发布声明文件为npm包,及如何管理包的依赖。
对于JavaScript库的使用者来讲,使用一节提供了一些简单步骤来定位与安装相应的声明文件。
一般来讲,你组织声明文件的方式取决于库是如何被使用的。 在JavaScript里提供了很多库的使用方法,这就需要你书写声明文件去匹配它们。 这篇指南涵盖了如何识别常见库的模式,与怎么样书写符合相应模式的声明文件。
针对每种主要的库的组织模式,在模版一节都有对应的文件。 你可以利用它们帮助你快速上手。
首先,我们先看一下TypeScript声明文件能够表示的库的类型。 这里会简单展示每种类型的库的使用方式,如何去书写,还有一些真实案例。
识别库的类型是书写声明文件的第一步。 我们将会给出一些提示,关于怎样通过库的 使用方法及其源码来识别库的类型。 根据库的文档及组织结构不同,这两种方式可能一个会比另外的那个简单一些。 我们推荐你使用任意你喜欢的方式。
全局库是指能在全局命名空间下访问的(例如:不需要使用任何形式的import
)。 许多库都是简单的暴露出一个或多个全局变量。 比如,如果你使用过 jQuery,$
变量可以被够简单的引用:
$(() => { console.log('hello!'); } );
你经常会在全局库的指南文档上看到如何在HTML里用脚本标签引用库:
<script src="http://a.great.cdn.for/someLib.js"></script>
目前,大多数流行的全局访问型库实际上都以UMD库的形式进行书写(见后文)。 UMD库的文档很难与全局库文档两者之间难以区分。 在书写全局声明文件前,一定要确认一下库是否真的不是UMD。
全局库的代码通常都十分简单。 一个全局的“Hello, world”库可能是这样的:
function createGreeting(s) { return "Hello, " + s;}
或这样:
window.createGreeting = function(s) { return "Hello, " + s;}
当你查看全局库的源代码时,你通常会看到:
var
语句或function
声明window.someName
document
或window
是存在的你不会看到:
require
或define
var fs = require("fs");
define(...)
调用require
或导入这个库由于把一个全局库转变成UMD库是非常容易的,所以很少流行的库还再使用全局的风格。 然而,小型的且需要DOM(或 没有依赖)的库可能还是全局类型的。
模版文件global.d.ts
定义了myLib
库作为例子。 一定要阅读 "防止命名冲突"补充说明。
一些库只能工作在模块加载器的环境下。 比如,像 express
只能在Node.js里工作所以必须使用CommonJS的require
函数加载。
ECMAScript 2015(也就是ES2015,ECMAScript 6或ES6),CommonJS和RequireJS具有相似的导入一个模块的表示方法。 例如,对于JavaScript CommonJS (Node.js),有下面的代码
var fs = require("fs");
对于TypeScript或ES6,import
关键字也具有相同的作用:
import fs = require("fs");
你通常会在模块化库的文档里看到如下说明:
var someLib = require('someLib');
或
define(..., ['someLib'], function(someLib) {});
与全局模块一样,你也可能会在UMD模块的文档里看到这些例子,因此要仔细查看源码和文档。
模块库至少会包含下列具有代表性的条目之一:
require
或define
import * as a from 'b';
or export c;
这样的声明exports
或module.exports
它们极少包含:
window
或global
的赋值许多流行的Node.js库都是这种模块化的,例如express
,gulp
和 request
。
UMD模块是指那些既可以作为模块使用(通过导入)又可以作为全局(在没有模块加载器的环境里)使用的模块。 许多流行的库,比如 Moment.js,就是这样的形式。 比如,在Node.js或RequireJS里,你可以这样写:
import moment = require("moment");console.log(moment.format());
然而在纯净的浏览器环境里你也可以这样写:
console.log(moment.format());
UMD模块会检查是否存在模块加载器环境。 这是非常形容观察到的模块,它们会像下面这样:
(function (root, factory) { if (typeof define === "function" && define.amd) { define(["libName"], factory); } else if (typeof module === "object" && module.exports) { module.exports = factory(require("libName")); } else { root.returnExports = factory(root.libName); }}(this, function (b) {
如果你在库的源码里看到了typeof define
,typeof window
,或typeof module
这样的测试,尤其是在文件的顶端,那么它几乎就是一个UMD库。
UMD库的文档里经常会包含通过require
“在Node.js里使用”例子, 和“在浏览器里使用”的例子,展示如何使用<script>
标签去加载脚本。
大多数流行的库现在都能够被当成UMD包。 比如 jQuery,Moment.js,lodash和许多其它的。
针对模块有三种可用的模块, module.d.ts
, module-class.d.ts
and module-function.d.ts
.
使用module-function.d.ts
,如果模块能够作为函数调用。
var x = require("foo");// Note: calling 'x' as a functionvar y = x(42);
一定要阅读补充说明: “ES6模块调用签名的影响”
使用module-class.d.ts
如果模块能够使用new
来构造:
var x = require("bar");// Note: using 'new' operator on the imported variablevar y = new x("hello");
相同的补充说明作用于这些模块。
如果模块不能被调用或构造,使用module.d.ts
文件。
一个模块插件可以改变一个模块的结构(UMD或模块)。 例如,在Moment.js里, moment-range
添加了新的range
方法到monent
对象。
对于声明文件的目标,我们会写相同的代码不论被改变的模块是一个纯粹的模块还是UMD模块。
使用module-plugin.d.ts
模版。
一个全局插件是全局代码,它们会改变全局对象的结构。 对于 全局修改的模块,在运行时存在冲突的可能。
比如,一些库往Array.prototype
或String.prototype
里添加新的方法。
全局通常很容易地从它们的文档识别出来。
你会看到像下面这样的例子:
var x = "hello, world";// Creates new methods on built-in typesconsole.log(x.startsWithHello());var y = [1, 2, 3];// Creates new methods on built-in typesconsole.log(y.reverseAndSort());
使用global-plugin.d.ts
模版。
当一个全局修改的模块被导入的时候,它们会改变全局作用域里的值。 比如,存在一些库它们添加新的成员到String.prototype
当导入它们的时候。 这种模式很危险,因为可能造成运行时的冲突, 但是我们仍然可以为它们书写声明文件。
全局修改的模块通常可以很容易地从它们的文档识别出来。 通常来讲,它们与全局插件相似,但是需要 require
调用来激活它们的效果。
你可能会看到像下面这样的文档:
// 'require' call that doesn't use its return valuevar unused = require("magic-string-time");/* or */require("magic-string-time");var x = "hello, world";// Creates new methods on built-in typesconsole.log(x.startsWithHello());var y = [1, 2, 3];// Creates new methods on built-in typesconsole.log(y.reverseAndSort());
使用global-modifying-module.d.ts
模版。
可能会有以下几种依赖。
如果你的库依赖于某个全局库,使用/// <reference types="..." />
指令:
/// <reference types="someLib" />function getThing(): someLib.thing;
如果你的库依赖于模块,使用import
语句:
import * as moment from "moment";function getThing(): moment;
如果你的全局库依赖于某个UMD模块,使用/// <reference types
指令:
/// <reference types="moment" />function getThing(): moment;
如果你的模块或UMD库依赖于一个UMD库,使用import
语句:
import * as someLib from 'someLib';
不要使用/// <reference
指令去声明UMD库的依赖!
注意,在书写全局声明文件时,允许在全局作用域里定义很多类型。 我们十分不建义这样做,当一个工程里有许多声明文件时,它会导致无法处理的命名冲突。
一个简单的规则是使用库定义的全局变量名来声明命名空间类型。 比如,库定义了一个全局的值 cats
,你可以这样写
declare namespace cats { interface KittySettings { }}
不要
// at top-levelinterface CatsKittySettings { }
这样也保证了库在转换成UMD的时候没有任何的破坏式改变,对于声明文件用户来说。
一些插件添加或修改已存在的顶层模块的导出部分。 当然这在CommonJS和其它加载器里是允许的,ES模块被当作是不可改变的因此这种模式就不可行了。 因为TypeScript是能不预知加载器类型的,所以没没在编译时保证,但是开发者如果要转到ES6模块加载器上应该注意这一点。
很多流行库,比如Express,暴露出自己作为可以调用的函数。 比如,典型的Express使用方法如下:
import exp = require("express");var app = exp();
在ES6模块加载器里,顶层的对象(这里以exp
导入)只能具有属性; 顶层的模块对象 永远不能被调用。 十分常见的解决方法是定义一个 default
导出到一个可调用的/可构造的对象; 一会模块加载器助手工具能够自己探测到这种情况并且使用 default
导出来替换顶层对象。
这篇指南的目的是教你如何书写高质量的TypeScript声明文件。 我们在这里会展示一些API的文档,还有它们的使用示例, 并且阐述了如何为它们书写声明文件。
这些例子是按复杂度递增的顺序组织的。
文档
全局变量
foo
包含了存在组件总数。
代码
console.log("Half the number of widgets is " + (foo / 2));
声明
使用declare var
声明变量。 如果变量是只读的,那么可以使用 declare const
。 你还可以使用 declare let
如果变量拥有块级作用域。
/** 组件总数 */declare var foo: number;
文档
用一个字符串参数调用
greet
函数向用户显示一条欢迎信息。
代码
greet("hello, world");
声明
使用declare function
声明函数。
declare function greet(greeting: string): void;
文档
全局变量
myLib
包含一个makeGreeting
函数, 还有一个属性numberOfGreetings
指示目前为止欢迎数量。
代码
let result = myLib.makeGreeting("hello, world");console.log("The computed greeting is:" + result);let count = myLib.numberOfGreetings;
声明
使用declare namespace
描述用点表示法访问的类型或值。
declare namespace myLib { function makeGreeting(s: string): string; let numberOfGreetings: number;}
文档
getWidget
函数接收一个数字,返回一个组件,或接收一个字符串并返回一个组件数组。
代码
let x: Widget = getWidget(43);let arr: Widget[] = getWidget("all of them");
声明
declare function getWidget(n: number): Widget;declare function getWidget(s: string): Widget[];
文档
当指定一个欢迎词时,你必须传入一个
GreetingSettings
对象。 这个对象具有以下几个属性:
- greeting:必需的字符串
- duration: 可靠的时长(毫秒表示)
- color: 可选字符串,比如‘#ff00ff’
代码
greet({ greeting: "hello world", duration: 4000});
声明
使用interface
定义一个带有属性的类型。
interface GreetingSettings { greeting: string; duration?: number; color?: string;}declare function greet(setting: GreetingSettings): void;
文档
在任何需要欢迎词的地方,你可以提供一个
string
,一个返回string
的函数或一个Greeter
实例。
代码
function getGreeting() { return "howdy";}class MyGreeter extends Greeter { }greet("hello");greet(getGreeting);greet(new MyGreeter());
声明
你可以使用类型别名来定义类型的短名:
type GreetingLike = string | (() => string) | Greeting;declare function greet(g: GreetingLike): void;
文档
greeter
对象能够记录到文件或显示一个警告。 你可以为.log(...)
提供LogOptions和为.alert(...)
提供选项。
代码
const g = new Greeter("Hello");g.log({ verbose: true });g.alert({ modal: false, title: "Current Greeting" });
声明
使用命名空间组织类型。
declare namespace GreetingLib { interface LogOptions { verbose?: boolean; } interface AlertOptions { modal: boolean; title?: string; color?: string; }}
你也可以在一个声明中创建嵌套的命名空间:
declare namespace GreetingLib.Options { // Refer to via GreetingLib.Options.Log interface Log { verbose?: boolean; } interface Alert { modal: boolean; title?: string; color?: string; }}
文档
你可以通过实例化
Greeter
对象来创建欢迎词,或者继承Greeter
对象来自定义欢迎词。
代码
const myGreeter = new Greeter("hello, world");myGreeter.greeting = "howdy";myGreeter.showGreeting();class SpecialGreeter extends Greeter { constructor() { super("Very special greetings"); }}
声明
使用declare class
描述一个类或像类一样的对象。 类可以有属性和方法,就和构造函数一样。
declare class Greeter { constructor(greeting: string); greeting: string; showGreeting(): void;}
Number
,String
,Boolean
和Object
不要使用如下类型Number
,String
,Boolean
或Object
。 这些类型指的是非原始的装盒对象,它们几乎没在JavaScript代码里正确地使用过。
/* 错误 */function reverse(s: String): String;
应该使用类型number
,string
,and boolean
。
/* OK */function reverse(s: string): string;
如果你就要使用Object
类型,考虑使用any
代替。 目前在TypeScript里无法指定一个对象“不是一个原始值”。
不要定义一个从来没使用过其类型参数的泛型类型。 了解详情 TypeScript FAQ page。
不要为返回值被忽略的回调函数设置一个any
类型的返回值类型:
/* 错误 */function fn(x: () => any) { x();}
应该给返回值被忽略的回调函数设置void
类型的返回值类型:
/* OK */function fn(x: () => void) { x();}
为什么:使用void
相对安全,因为它防止了你不小心使用x
的返回值:
function fn(x: () => void) { var k = x(); // oops! meant to do something else k.doSomething(); // error, but would be OK if the return type had been 'any'}
不要在回调函数里使用可选参数除非你真的要这么做:
/* 错误 */interface Fetcher { getObject(done: (data: any, elapsedTime?: number) => void): void;}
这里有一种特殊的意义:done
回调函数可能以1个参数或2个参数调用。 代码大概的意思是说这个回调函数不在乎是否有 elapsedTime
参数, 但是不需要把这个参数当成可选参数来达到此目的 -- 因为总是允许提供一个接收较少参数的回调函数。
应该写出回调函数的非可选参数:
/* OK */interface Fetcher { getObject(done: (data: any, elapsedTime: number) => void): void;}
不要因为回调函数参数个数不同而写不同的重载:
/* 错误 */declare function beforeAll(action: () => void, timeout?: number): void;declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;
应该只使用最大参数个数写一个重载:
/* OK */declare function beforeAll(action: (done: DoneFn) => void, timeout?: number): void;
为什么:回调函数总是可以忽略某个参数的,因此没必要为参数少的情况写重载。 参数少的回调函数首先允许错误类型的函数被传入,因为它们匹配第一个重载。
不要把一般的重载放在精确的重载前面:
/* 错误 */declare function fn(x: any): any;declare function fn(x: HTMLElement): number;declare function fn(x: HTMLDivElement): string;var myElem: HTMLDivElement;var x = fn(myElem); // x: any, wat?
应该排序重载令精确的排在一般的之前:
/* OK */declare function fn(x: HTMLDivElement): string;declare function fn(x: HTMLElement): number;declare function fn(x: any): any;var myElem: HTMLDivElement;var x = fn(myElem); // x: string, :)
为什么:TypeScript会选择第一个匹配到的重载当解析函数调用的时候。 当前面的重载比后面的“普通”,那么后面的被隐藏了不会被调用。
不要为仅在末尾参数不同时写不同的重载:
/* 错误 */interface Example { diff(one: string): number; diff(one: string, two: string): number; diff(one: string, two: string, three: boolean): number;}
应该尽可能使用可选参数:
/* OK */interface Example { diff(one: string, two?: string, three?: boolean): number;}
注意这在所有重载都有相同类型的返回值时会不好用。
为什么:有两种生要的原因。
TypeScript解析签名兼容性时会查看是否某个目标签名能够使用源的参数调用, 且允许外来参数。 下面的代码暴露出一个bug,当签名被正确的使用可选参数书写时:
function fn(x: (a: string, b: number, c: number) => void) { }var x: Example;// When written with overloads, OK -- used first overload// When written with optionals, correctly an errorfn(x.diff);
第二个原因是当使用了TypeScript“严格检查null”特性时。 因为没有指定的参数在JavaScript里表示为undefined
,通常显示地为可选参数传入一个undefined
。 这段代码在严格null模式下可以工作:
var x: Example;// When written with overloads, incorrectly an error because of passing 'undefined' to 'string'// When written with optionals, correctly OKx.diff("something", true ? undefined : "hour");
不要为仅在某个位置上的参数类型不同的情况下定义重载:
/* WRONG */interface Moment { utcOffset(): number; utcOffset(b: number): Moment; utcOffset(b: string): Moment;}
应该尽可能使用联合类型:
/* OK */interface Moment { utcOffset(): number; utcOffset(b: number|string): Moment;}
注意我们没有让b
成为可选的,因为签名的返回值类型不同。
为什么:This is important for people who are "passing through" a value to your function:
function fn(x: string): void;function fn(x: number): void;function fn(x: number|string) { // When written with separate overloads, incorrectly an error // When written with union types, correctly OK return moment().utcOffset(x);}
组织模块以提供你想要的API形式保持一致是比较难的。 比如,你可能想要这样一个模块,可以用或不用 new
来创建不同的类型, 在不同层级上暴露出不同的命名类型, 且模块对象上还带有一些属性。
阅读这篇指定后,你就会了解如果书写复杂的暴露出友好API的声明文件。 这篇指定针对于模块(UMD)库,因为它们的选择具有更高的可变性。
如果你理解了一些关于TypeScript是如何工作的核心概念, 那么你就能够为任何结构书写声明文件。
如果你正在阅读这篇指南,你可能已经大概了解TypeScript里的类型指是什么。 明确一下, 类型通过以下方式引入:
type sn = number | string;
)interface I { x: number[]; }
)class C { }
)enum E { A, B, C }
)import
声明以上每种声明形式都会创建一个新的类型名称。
与类型相比,你可能已经理解了什么是值。 值是运行时名字,可以在表达式里引用。 比如 let x = 5;
创建一个名为x
的值。
同样,以下方式能够创建值:
let
,const
,和var
声明namespace
或module
声明enum
声明class
声明import
声明function
声明类型可以存在于命名空间里。 比如,有这样的声明 let x: A.B.C
, 我们就认为 C
类型来自A.B
命名空间。
这个区别虽细微但很重要 -- 这里,A.B
不是必需的类型或值。
一个给定的名字A
,我们可以找出三种不同的意义:一个类型,一个值或一个命名空间。 要如何去解析这个名字要看它所在的上下文是怎样的。 比如,在声明 let m: A.A = A;
, A
首先被当做命名空间,然后做为类型名,最后是值。 这些意义最终可能会指向完全不同的声明!
这看上去另人迷惑,但是只要我们不过度的重载这还是很方便的。 下面让我们来看看一些有用的组合行为。
眼尖的读者可能会注意到,比如,class
同时出现在类型和值列表里。 class C { }
声明创建了两个东西: 类型C
指向类的实例结构, 值C
指向类构造函数。 枚举声明拥有相似的行为。
假设我们写了模块文件foo.d.ts
:
export var SomeVar: { a: SomeType };export interface SomeType { count: number;}
这样使用它:
import * as foo from './foo';let x: foo.SomeType = foo.SomeVar.a;console.log(x.count);
这可以很好地工作,但是我们知道SomeType
和SomeVar
很相关 因此我们想让他们有相同的名字。 我们可以使用组合通过相同的名字 Bar
表示这两种不同的对象(值和对象):
export var Bar: { a: Bar };export interface Bar { count: number;}
这提供了解构使用的机会:
import { Bar } from './foo';let x: Bar = Bar.a;console.log(x.count);
再次地,这里我们使用Bar
做为类型和值。 注意我们没有声明 Bar
值为Bar
类型 -- 它们是独立的。
有一些声明能够通过多个声明组合。 比如, class C { }
和interface C { }
可以同时存在并且都可以做为C
类型的属性。
只要不产生冲突就是合法的。 一个普通的规则是值总是会和同名的其它值产生冲突除非它们在不同命名空间里, 类型冲突则发生在使用类型别名声明的情况下( type s = string
), 命名空间永远不会发生冲突。
让我们看看如何使用。
interface
添加我们可以使用一个interface
往别一个interface
声明里添加额外成员:
interface Foo { x: number;}// ... elsewhere ...interface Foo { y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
这同样作用于类:
class Foo { x: number;}// ... elsewhere ...interface Foo { y: number;}let a: Foo = ...;console.log(a.x + a.y); // OK
注意我们不能使用接口往类型别名里添加成员(type s = string;
)
namespace
添加namespace
声明可以用来添加新类型,值和命名空间,只要不出现冲突。
比如,我们可能添加静态成员到一个类:
class C {}// ... elsewhere ...namespace C { export let x: number;}let y = C.x; // OK
注意在这个例子里,我们添加一个值到C
的静态部分(它的构造函数)。 这里因为我们添加了一个 值,且其它值的容器是另一个值 (类型包含于命名空间,命名空间包含于另外的命名空间)。
我们还可以给类添加一个命名空间类型:
class C {}// ... elsewhere ...namespace C { export interface D { }}let y: C.D; // OK
在这个例子里,直到我们写了namespace
声明才有了命名空间C
。 做为命名空间的 C
不会与类创建的值C
或类型C
相互冲突。
最后,我们可以进行不同的合并通过namespace
声明。 Finally, we could perform many different merges usingnamespace
declarations. This isn't a particularly realistic example, but shows all sorts of interesting behavior:
namespace X { export interface Y { } export class Z { }}// ... elsewhere ...namespace X { export var Y: number; export namespace Z { export class C { } }}type X = string;
在这个例子里,第一个代码块创建了以下名字与含义:
X
(因为namespace
声明包含一个值,Z
)X
(因为namespace
声明包含一个值,Z
)X
里的类型Y
X
里的类型Z
(类的实例结构)X
的一个属性值Z
(类的构造函数)第二个代码块创建了以下名字与含义:
Y
(number
类型),它是值X
的一个属性Z
Z
,它是值X
的一个属性X.Z
命名空间下的类型C
X.Z
的一个属性值C
X
export =
或import
一个重要的原则是export
和import
声明会导出或导入目标的所有含义。
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the global-modifying module template file. You should rename it to index.d.ts *~ and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ Note: If your global-modifying module is callable or constructable, you'll *~ need to combine the patterns here with those in the module-class or module-function *~ template files */declare global { /*~ Here, declare things that go in the global namespace, or augment *~ existing declarations in the global namespace */ interface String { fancyFormat(opts: StringFormatOptions): string; }}/*~ If your module exports types or values, write them as usual */export interface StringFormatOptions { fancinessLevel: number;}/*~ For example, declaring a method on the module (in addition to its global side effects) */export function doSomething(): void;/*~ If your module exports nothing, you'll need this line. Otherwise, delete it */export { };
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This template shows how to write a global plugin. *//*~ Write a declaration for the original type and add new members. *~ For example, this adds a 'toBinaryString' method with to overloads to *~ the built-in number type. */interface Number { toBinaryString(opts?: MyLibrary.BinaryFormatOptions): string; toBinaryString(callback: MyLibrary.BinaryFormatCallback, opts?: MyLibrary.BinaryFormatOptions): string;}/*~ If you need to declare several types, place them inside a namespace *~ to avoid adding too many things to the global namespace. */declare namespace MyLibrary { type BinaryFormatCallback = (n: number) => string; interface BinaryFormatOptions { prefix?: string; padding: number; }}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ If this library is callable (e.g. can be invoked as myLib(3)), *~ include those call signatures here. *~ Otherwise, delete this section. */declare function myLib(a: string): string;declare function myLib(a: number): number;/*~ If you want the name of this library to be a valid type name, *~ you can do so here. *~ *~ For example, this allows us to write 'var x: myLib'; *~ Be sure this actually makes sense! If it doesn't, just *~ delete this declaration and add types inside the namespace below. */interface myLib { name: string; length: number; extras?: string[];}/*~ If your library has properties exposed on a global variable, *~ place them here. *~ You should also place types (interfaces and type alias) here. */declare namespace myLib { //~ We can write 'myLib.timeout = 50;' let timeout: number; //~ We can access 'myLib.version', but not change it const version: string; //~ There's some class we can create via 'let c = new myLib.Cat(42)' //~ Or reference e.g. 'function f(c: myLib.Cat) { ... } class Cat { constructor(n: number); //~ We can read 'c.age' from a 'Cat' instance readonly age: number; //~ We can invoke 'c.purr()' from a 'Cat' instance purr(): void; } //~ We can declare a variable as //~ 'var s: myLib.CatSettings = { weight: 5, name: "Maru" };' interface CatSettings { weight: number; name: string; tailLength?: number; } //~ We can write 'const v: myLib.VetID = 42;' //~ or 'const v: myLib.VetID = "bob";' type VetID = string | number; //~ We can invoke 'myLib.checkCat(c)' or 'myLib.checkCat(c, v);' function checkCat(c: Cat, s?: VetID);}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module template file for class modules. *~ You should rename it to index.d.ts and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ Note that ES6 modules cannot directly export class objects. *~ This file should be imported using the CommonJS-style: *~ import x = require('someLibrary'); *~ *~ Refer to the documentation to understand common *~ workarounds for this limitation of ES6 modules. *//*~ If this module is a UMD module that exposes a global variable 'myClassLib' when *~ loaded outside a module loader environment, declare that global here. *~ Otherwise, delete this declaration. */export as namespace myClassLib;/*~ This declaration specifies that the class constructor function *~ is the exported object from the file */export = MyClass;/*~ Write your module's methods and properties in this class */declare class MyClass { constructor(someParam?: string); someProperty: string[]; myMethod(opts: MyClass.MyClassMethodOptions): number;}/*~ If you want to expose types from your module as well, you can *~ place them in this block. */declare namespace MyClass { export interface MyClassMethodOptions { width?: number; height?: number; }}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module template file for function modules. *~ You should rename it to index.d.ts and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ Note that ES6 modules cannot directly export callable functions. *~ This file should be imported using the CommonJS-style: *~ import x = require('someLibrary'); *~ *~ Refer to the documentation to understand common *~ workarounds for this limitation of ES6 modules. *//*~ If this module is a UMD module that exposes a global variable 'myFuncLib' when *~ loaded outside a module loader environment, declare that global here. *~ Otherwise, delete this declaration. */export as namespace myFuncLib;/*~ This declaration specifies that the function *~ is the exported object from the file */export = MyFunction;/*~ This example shows how to have multiple overloads for your function */declare function MyFunction(name: string): MyFunction.NamedReturnType;declare function MyFunction(length: number): MyFunction.LengthReturnType;/*~ If you want to expose types from your module as well, you can *~ place them in this block. Often you will want to describe the *~ shape of the return type of the function; that type should *~ be declared in here, as this example shows. */declare namespace MyFunction { export interface LengthReturnType { width: number; height: number; } export interface NamedReturnType { firstName: string; lastName: string; } /*~ If the module also has properties, declare them here. For example, *~ this declaration says that this code is legal: *~ import f = require('myFuncLibrary'); *~ console.log(f.defaultName); */ export const defaultName: string; export let defaultLength: number;}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module plugin template file. You should rename it to index.d.ts *~ and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ On this line, import the module which this module adds to */import * as m from 'someModule';/*~ You can also import other modules if needed */import * as other from 'anotherModule';/*~ Here, declare the same module as the one you imported above */declare module 'someModule' { /*~ Inside, add new function, classes, or variables. You can use *~ unexported types from the original module if needed. */ export function theNewMethod(x: m.foo): other.bar; /*~ You can also add new properties to existing interfaces from *~ the original module by writing interface augmentations */ export interface SomeModuleOptions { someModuleSetting?: string; } /*~ New types can also be declared and will appear as if they *~ are in the original module */ export interface MyModulePluginOptions { size: number; }}
// Type definitions for [~THE LIBRARY NAME~] [~OPTIONAL VERSION NUMBER~]// Project: [~THE PROJECT NAME~]// Definitions by: [~YOUR NAME~] <[~A URL FOR YOU~]>/*~ This is the module template file. You should rename it to index.d.ts *~ and place it in a folder with the same name as the module. *~ For example, if you were writing a file for "super-greeter", this *~ file should be 'super-greeter/index.d.ts' *//*~ If this module is a UMD module that exposes a global variable 'myLib' when *~ loaded outside a module loader environment, declare that global here. *~ Otherwise, delete this declaration. */export as namespace myLib;/*~ If this module has methods, declare them as functions like so. */export function myMethod(a: string): string;export function myOtherMethod(a: number): number;/*~ You can declare types that are available via importing the module */export interface someType { name: string; length: number; extras?: string[];}/*~ You can declare properties of the module using const, let, or var */export const myField: number;/*~ If there are types, properties, or methods inside dotted names *~ of the module, declare them inside a 'namespace'. */export namespace subProp { /*~ For example, given this definition, someone could write: *~ import { subProp } from 'yourModule'; *~ subProp.foo(); *~ or *~ import * as yourMod from 'yourModule'; *~ yourMod.subProp.foo(); */ export function foo(): void;}
现在我们已经按照指南里的步骤写好一个声明文件,是时候把它发布到npm了。 有两种主要方式用来发布声明文件到npm:
如果你能控制要使用你发布的声明文件的那个npm包的话,推荐第一种方式。 这样的话,你的声明文件与JavaScript总是在一起传递。
如果你的包有一个主.js
文件,你还是需要在package.json
里指定主声明文件。 设置 types
属性指向捆绑在一起的声明文件。 比如:
{ "name": "awesome", "author": "Vandelay Industries", "version": "1.0.0", "main": "./lib/main.js", "types": "./lib/main.d.ts"}
注意"typings"
与"types"
具有相同的意义,也可以使用它。
同样要注意的是如果主声明文件名是index.d.ts
并且位置在包的根目录里(与index.js
并列),你就不需要使用"types"
属性指定了。
所有的依赖是由npm管理的。 确保所依赖的声明包都在 package.json
的"dependencies"
里指明了 比如,假设我们写了一个包它依赖于Browserify和TypeScript。
{ "name": "browserify-typescript-extension", "author": "Vandelay Industries", "version": "1.0.0", "main": "./lib/main.js", "types": "./lib/main.d.ts", "dependencies": [ "browserify@latest", "@types/browserify@latest", "typescript@next" ]}
这里,我们的包依赖于browserify
和typescript
包。 browserify
没有把它的声明文件捆绑在它的npm包里,所以我们需要依赖于@types/browserify
得到它的声明文件。 typescript
相反,它把声明文件放在了npm包里,因此我们不需要依赖额外的包。
我们的包要从这两个包里暴露出声明文件,因此browserify-typescript-extension
的用户也需要这些依赖。 正因此,我们使用 "dependencies"
而不是"devDependencies"
,否则用户将需要手动安装那些包。 如果我们只是在写一个命令行应用,并且我们的包不会被当做一个库使用的话,那么我就可以使用devDependencies
。
/// <reference path="..." />
不要在声明文件里使用/// <reference path="..." />
。
/// <reference path="../typescript/lib/typescriptServices.d.ts" />....
应该使用/// <reference types="..." />
代替
/// <reference types="typescript" />....
务必阅读[使用依赖](./Library Structures.md#consuming-dependencies)一节了解详情。
如果你的类型声明依赖于另一个包:
在发布声明文件包之后,确保在DefinitelyTyped外部包列表里面添加一条引用。 这可以让查找工具知道你的包提供了自己的声明文件。
@types下面的包是从DefinitelyTyped里自动发布的,通过 types-publisher工具。 如果想让你的包发布为@types包,提交一个pull request到https://github.com/DefinitelyTyped/DefinitelyTyped。 在这里查看详细信息contribution guidelines page。
在TypeScript 2.0,获取、使用和查找声明文件变得十分容易。 这篇文章将详细说明怎么做这三件事。
在TypeScript 2.0以上的版本,获取类型声明文件只需要使用npm。
比如,获取lodash库的声明文件,只需使用下面的命令:
npm install --save @types/lodash
下载完后,就可以直接在TypeScript里使用lodash了。 不论是在模块里还是全局代码里使用。
比如,你已经npm install
安装了类型声明,你可以使用导入:
import * as _ from "lodash";_.padStart("Hello TypeScript!", 20, " ");
或者如果你没有使用模块,那么你只需使用全局的变量_
。
_.padStart("Hello TypeScript!", 20, " ");
大多数情况下,类型声明包的名字总是与它们在npm
上的包的名字相同,但是有@types/
前缀, 但如果你需要的话,你可以在 https://aka.ms/types这里查找你喜欢的库。
注意:如果你要找的声明文件不存在,你可以贡献一份,这样就方便了下一位要使用它的人。 查看DefinitelyTyped 贡献指南页了解详情。
如果一个目录下存在一个tsconfig.json
文件,那么它意味着这个目录是TypeScript项目的根目录。tsconfig.json
文件中指定了用来编译这个项目的根文件和编译选项。 一个项目可以通过以下方式之一来编译:
tsc
,编译器会从当前目录开始去查找tsconfig.json
文件,逐级向上搜索父目录。tsc
,且使用命令行参数--project
(或-p
)指定一个包含tsconfig.json
文件的目录。当命令行上指定了输入文件时,tsconfig.json
文件会被忽略。
tsconfig.json
示例文件:
"files"
属性{ "compilerOptions": { "module": "commonjs", "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, "outFile": "../../built/local/tsc.js", "sourceMap": true }, "files": [ "core.ts", "sys.ts", "types.ts", "scanner.ts", "parser.ts", "utilities.ts", "binder.ts", "checker.ts", "emitter.ts", "program.ts", "commandLineParser.ts", "tsc.ts", "diagnosticInformationMap.generated.ts" ]}
使用"include"
和"exclude"
属性
{ "compilerOptions": { "module": "commonjs", "noImplicitAny": true, "removeComments": true, "preserveConstEnums": true, "outFile": "../../built/local/tsc.js", "sourceMap": true }, "include": [ "src/**/*" ], "exclude": [ "node_modules", "**/*.spec.ts" ]}
"compilerOptions"
可以被忽略,这时编译器会使用默认值。在这里查看完整的[编译器选项](./Compiler Options.md)列表。
"files"
指定一个包含相对或绝对文件路径的列表。 "include"
和"exclude"
属性指定一个文件glob匹配模式列表。 支持的glob通配符有:
*
匹配0或多个字符(不包括目录分隔符)?
匹配一个任意字符(不包括目录分隔符)**/
递归匹配任意子目录如果一个glob模式里的某部分只包含*
或.*
,那么仅有支持的文件扩展名类型被包含在内(比如默认.ts
,.tsx
,和.d.ts
, 如果allowJs
设置能true
还包含.js
和.jsx
)。
如果"files"
和"include"
都没有被指定,编译器默认包含当前目录和子目录下所有的TypeScript文件(.ts
,.d.ts
和 .tsx
),排除在"exclude"
里指定的文件。JS文件(.js
和.jsx
)也被包含进来如果allowJs
被设置成true
。 如果指定了 "files"
或"include"
,编译器会将它们结合一并包含进来。 使用 "outDir"
指定的目录下的文件永远会被编译器排除,除非你明确地使用"files"
将其包含进来(这时就算用exclude
指定也没用)。
使用"include"
引入的文件可以使用"exclude"
属性过滤。 然而,通过 "files"
属性明确指定的文件却总是会被包含在内,不管"exclude"
如何设置。 如果没有特殊指定, "exclude"
默认情况下会排除node_modules
,bower_components
,和jspm_packages
目录。
任何被"files"或
"include"指定的文件所引用的文件也会被包含进来。
A.ts引用了
B.ts,因此
B.ts不能被排除,除非引用它的
A.ts在
"exclude"`列表中。
tsconfig.json
文件可以是个空文件,那么所有默认的文件(如上面所述)都会以默认配置选项编译。
在命令行上指定的编译选项会覆盖在tsconfig.json
文件里的相应选项。
compileOnSave
在最顶层设置compileOnSave
标记,可以让IDE在保存文件的时候根据tsconfig.json
重新生成文件。
{ "compileOnSave": true, "compilerOptions": { "noImplicitAny" : true }}
要想支持这个特性需要Visual Studio 2015, TypeScript1.8.4以上并且安装atom-typescript插件。
到这里查看模式: http://json.schemastore.org/tsconfig.
选项 | 类型 | 默认值 | 描述 |
---|---|---|---|
--allowJs | boolean | true | 允许编译javascript文件。 |
--allowSyntheticDefaultImports | boolean | module === "system" | 允许从没有设置默认导出的模块中默认导入。这并不影响代码的显示,仅为了类型检查。 |
--allowUnreachableCode | boolean | false | 不报告执行不到的代码错误。 |
--allowUnusedLabels | boolean | false | 不报告未使用的标签错误。 |
--baseUrl | string | 解析非相对模块名的基准目录。查看模块解析文档了解详情。 | |
--charset | string | "utf8" | 输入文件的字符集。 |
--declaration -d | boolean | false | 生成相应的'.d.ts'文件。 |
--declarationDir | string | 生成声明文件的输出路径。 | |
--diagnostics | boolean | false | 显示诊断信息。 |
--disableSizeLimit | boolean | false | 禁用JavaScript工程体积大小的限制 |
--emitBOM | boolean | false | 在输出文件的开头加入BOM头(UTF-8 Byte Order Mark)。 |
--emitDecoratorMetadata [1] | boolean | false | 给源码里的装饰器声明加上设计类型元数据。查看issue #2577了解更多信息。 |
--experimentalDecorators [1] | boolean | false | 实验性启用ES7装饰器支持。 |
--forceConsistentCasingInFileNames | boolean | false | 不允许不一致包装引用相同的文件。 |
--help -h | 打印帮助信息。 | ||
--inlineSourceMap | boolean | false | 生成单个sourcemaps文件,而不是将每sourcemaps生成不同的文件。 |
--inlineSources | boolean | false | 将代码与sourcemaps生成到一个文件中,要求同时设置了--inlineSourceMap 或--sourceMap 属性。 |
--init | 初始化TypeScript项目并创建一个tsconfig.json 文件。 | ||
--isolatedModules | boolean | false | 无条件地给没有解析的文件生成imports。 |
--jsx | string | "Preserve" | 在'.tsx'文件里支持JSX:'React' 或 'Preserve'。查看JSX。 |
--lib | string[] | 编译过程中需要引入的库文件的列表。 可能的值为: ► es5 ► es6 ► es2015 ► es7 ► es2016 ► es2017 dom webworker scripthost ► es2015.core ► es2015.collection ► es2015.generator ► es2015.iterable ► es2015.promise ► es2015.proxy ► es2015.reflect ► es2015.symbol ► es2015.symbol.wellknown ► es2016.array.include ► es2017.object ► es2017.sharedmemory 注意:如果 --lib 没有指定默认库。默认库是 ► For --target ES5: dom,es5,scripthost ► For --target ES6: dom,es6,dom.iterable,scripthost | |
--listEmittedFiles | boolean | false | 打印出编译后生成文件的名字。 |
--listFiles | boolean | false | 编译过程中打印文件名。 |
--locale | string | (platform specific) | 显示错误信息时使用的语言,比如:en-us。 |
--mapRoot | string | 为调试器指定指定sourcemap文件的路径,而不是使用生成时的路径。当.map 文件是在运行时指定的,并不同于js 文件的地址时使用这个标记。指定的路径会嵌入到sourceMap 里告诉调试器到哪里去找它们。 | |
--maxNodeModuleJsDepth | number | 0 | node_modules下的最大依赖深度搜索并加载JavaScript文件。仅适用于使用--allowJs 。 |
--module -m | string | target === 'ES6' ? 'ES6' : 'commonjs' | 指定生成哪个模块系统代码:'commonjs','amd','system',或 'umd'或'es2015'。只有'amd'和'system'能和--outFile 一起使用。当目标是ES5或以下的时候不能使用'es2015'。 |
--moduleResolution | string | module === 'amd' | 'system' | 'ES6' ? 'classic' : 'node' | 决定如何处理模块。或者是'node'对于Node.js/io.js,或者是'classic'(默认)。查看模块解析文档了解详情。 |
--newLine | string | (platform specific) | 当生成文件时指定行结束符:'CRLF'(dos)或 'LF' (unix)。 |
--noEmit | boolean | false | 不生成输出文件。 |
--noEmitHelpers | boolean | false | 不在输出文件中生成用户自定义的帮助函数代码,如__extends 。 |
--noEmitOnError | boolean | false | 报错时不生成输出文件。 |
--noFallthroughCasesInSwitch | boolean | false | 报告switch语句的fallthrough错误。(即,不允许switch的case语句贯穿) |
--noImplicitAny | boolean | false | 在表达式和声明上有隐含的'any'类型时报错。 |
--noImplicitReturns | boolean | false | 不是函数的所有返回路径都有返回值时报错。 |
--noImplicitThis | boolean | false | 当this 表达式的值为any 类型的时候,生成一个错误。 |
--noImplicitUseStrict | boolean | false | 模块输出中不包含'use strict'指令。 |
--noLib | boolean | false | 不包含默认的库文件(lib.d.ts)。 |
--noResolve | boolean | false | 不把/// <reference``> 或模块导入的文件加到编译文件列表。 |
--noUnusedLocals | boolean | false | 若有未使用的局部变量则抛错。 |
--noUnusedParameters | boolean | false | 若有未使用的参数则抛错。 |
--out | string | 弃用。使用 --outFile 代替。 | |
--outDir | string | 重定向输出目录。 | |
--outFile | string | 将输出文件合并为一个文件。合并的顺序是根据传入编译器的文件顺序和///<reference``> 和import 的文件顺序决定的。查看输出文件顺序文件了解详情。 | |
paths [2] | Object | 模块名到基于baseUrl 的路径映射的列表。查看模块解析文档了解详情。 | |
--preserveConstEnums | boolean | false | 保留const 和enum 声明。查看const enums documentation了解详情。 |
--pretty [1] | boolean | false | 给错误和消息设置样式,使用颜色和上下文。 |
--project -p | string | 编译指定目录下的项目。这个目录应该包含一个tsconfig.json 文件来管理编译。查看tsconfig.json文档了解更多信息。 | |
--reactNamespace | string | "React" | 当目标为生成'react' JSX时,指定createElement 和__spread 的调用对象 |
--removeComments | boolean | false | 删除所有注释,除了以/!* 开头的版权信息。 |
--rootDir | string | (common root directory is computed from the list of input files) | 仅用来控制输出的目录结构--outDir 。 |
rootDirs [2] | string[] | 根(root)文件夹列表,联给了代表运行时表示工程结构的内容。查看模块解析文档了解详情。 | |
--skipLibCheck | boolean | false | 不检查默认库文件(lib.d.ts )的正确性。 |
--skipDefaultLibCheck | boolean | false | 不检查用户定义的库文件(*.d.ts )的正确性。 |
--sourceMap | boolean | false | 生成相应的'.map'文件。 |
--sourceRoot | string | 指定TypeScript源文件的路径,以便调试器定位。当TypeScript文件的位置是在运行时指定时使用此标记。路径信息会被加到sourceMap 里。 | |
--strictNullChecks | boolean | false | 在严格的null 检查模式下,null 和undefined 值不包含在任何类型里,只允许用它们自己和any 来赋值(有个例外,undefined 可以赋值到void )。 |
--stripInternal [1] | boolean | false | 不对具有/** @internal */ JSDoc注解的代码生成代码。 |
--suppressExcessPropertyErrors [1] | boolean | false | 阻止对对象字面量的额外属性检查。 |
--suppressImplicitAnyIndexErrors | boolean | false | 阻止--noImplicitAny 对缺少索引签名的索引对象报错。查看issue #1232了解详情。 |
--target -t | string | "ES5" | 指定ECMAScript目标版本'ES3' (默认),'ES5',或'ES6'[1] |
--traceResolution | boolean | false | 生成模块解析日志信息 |
--types | string[] | 要包含的类型声明文件名列表。 | |
--typeRoots | string[] | 要包含的类型声明文件路径列表。 | |
--version -v | 打印编译器版本号。 | ||
--watch -w | 在监视模式下运行编译器。会监视输出文件,在它们改变时重新编译。 |
tsconfig.json
里使用,不能在命令行使用。编译选项可以在使用MSBuild的项目里通过MSBuild属性指定。
<PropertyGroup Condition="'$(Configuration)' == 'Debug'"> <TypeScriptRemoveComments>false</TypeScriptRemoveComments> <TypeScriptSourceMap>true</TypeScriptSourceMap></PropertyGroup><PropertyGroup Condition="'$(Configuration)' == 'Release'"> <TypeScriptRemoveComments>true</TypeScriptRemoveComments> <TypeScriptSourceMap>false</TypeScriptSourceMap></PropertyGroup><Import Project="$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets" Condition="Exists('$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets')" />
编译选项 | MSBuild属性名称 | 可用值 |
---|---|---|
--declaration | TypeScriptGeneratesDeclarations | 布尔值 |
--module | TypeScriptModuleKind | AMD , CommonJs , UMD 或 System |
--target | TypeScriptTarget | ES3 , ES5 , or ES6 |
--charset | TypeScriptCharset | |
--emitBOM | TypeScriptEmitBOM | 布尔值 |
--emitDecoratorMetadata | TypeScriptEmitDecoratorMetadata | 布尔值 |
--experimentalDecorators | TypeScriptExperimentalDecorators | 布尔值 |
--inlineSourceMap | TypeScriptInlineSourceMap | 布尔值 |
--inlineSources | TypeScriptInlineSources | 布尔值 |
--locale | 自动的 | 自动设置成PreferredUILang的值 |
--mapRoot | TypeScriptMapRoot | 文件路径 |
--newLine | TypeScriptNewLine | CRLF 或 LF |
--noEmitOnError | TypeScriptNoEmitOnError | 布尔值 |
--noEmitHelpers | TypeScriptNoEmitHelpers | 布尔值 |
--noImplicitAny | TypeScriptNoImplicitAny | 布尔值 |
--noUnusedLocals | TypeScriptNoUnusedLocals | 布尔值 |
--noUnusedParameters | TypeScriptNoUnusedParameters | 布尔值 |
--noLib | TypeScriptNoLib | 布尔值 |
--noResolve | TypeScriptNoResolve | 布尔值 |
--out | TypeScriptOutFile | 文件路径 |
--outDir | TypeScriptOutDir | 文件路径 |
--preserveConstEnums | TypeScriptPreserveConstEnums | 布尔值 |
--removeComments | TypeScriptRemoveComments | 布尔值 |
--rootDir | TypeScriptRootDir | 文件路径 |
--isolatedModules | TypeScriptIsolatedModules | 布尔值 |
--sourceMap | TypeScriptSourceMap | 文件路径 |
--sourceRoot | TypeScriptSourceRoot | 文件路径 |
--strictNullChecks | TypeScriptStrictNullChecks | 布尔值 |
--suppressImplicitAnyIndexErrors | TypeScriptSuppressImplicitAnyIndexErrors | 布尔值 |
--suppressExcessPropertyErrors | TypeScriptSuppressExcessPropertyErrors | 布尔值 |
--moduleResolution | TypeScriptModuleResolution | Classic or Node |
--experimentalAsyncFunctions | TypeScriptExperimentalAsyncFunctions | 布尔值 |
--jsx | TypeScriptJSXEmit | React or Preserve |
--reactNamespace | TypeScriptReactNamespace | string |
--skipDefaultLibCheck | TypeScriptSkipDefaultLibCheck | 布尔值 |
--allowUnusedLabels | TypeScriptAllowUnusedLabels | 布尔值 |
--noImplicitReturns | TypeScriptNoImplicitReturns | 布尔值 |
--noFallthroughCasesInSwitch | TypeScriptNoFallthroughCasesInSwitch | 布尔值 |
--allowUnreachableCode | TypeScriptAllowUnreachableCode | 布尔值 |
--forceConsistentCasingInFileNames | TypeScriptForceConsistentCasingInFileNames | 布尔值 |
--allowSyntheticDefaultImports | TypeScriptAllowSyntheticDefaultImports | 布尔值 |
--noImplicitUseStrict | TypeScriptNoImplicitUseStrict | 布尔值 |
--project | VS不支持 | |
--watch | VS不支持 | |
--diagnostics | VS不支持 | |
--listFiles | VS不支持 | |
--noEmit | VS不支持 | |
--allowJs | VS不支持 | |
VS特有选项 | TypeScriptAdditionalFlags | 任意编译选项 |
查找 C:Program Files (x86)MSBuildMicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets
文件。 可用的MSBuild XML标签与相应的tsc
编译选项的映射都在那里。
工程文件里的<TypeScriptToolsVersion>1.7</TypeScriptToolsVersion>
属性值表明了构建时使用的编译器的版本号(这个例子里是1.7) 这样就允许一个工程在不同的机器上使用固定的版本去编译。
如果没有指定TypeScriptToolsVersion
,则会使用机器上安装的最新版本的编译器去构建。
如果用户使用的是更新版本的TypeScript,则会在首次加载工程的时候看到一个提示升级工程的对话框。
如果你使用其它的构建工具(比如,gulp, grunt等等)并且使用VS做为开发和调试工具,那么在工程里设置<TypeScriptCompileBlocked>true</TypeScriptCompileBlocked>
。 这样VS只会提供给你编辑的功能,而不会在你按F5的时候去构建。
npm install tsify
browserify main.ts -p [ tsify --noImplicitAny ] > bundle.js
var browserify = require("browserify");var tsify = require("tsify");browserify() .add('main.ts') .plugin('tsify', { noImplicitAny: true }) .bundle() .pipe(process.stdout);
更多详细信息:smrq/tsify
npm install duo-typescript
duo --use duo-typescript entry.ts
var Duo = require('duo');var fs = require('fs')var path = require('path')var typescript = require('duo-typescript');var out = path.join(__dirname, "output.js")Duo(__dirname) .entry('entry.ts') .use(typescript()) .run(function (err, results) { if (err) throw err; // Write compiled result to output file fs.writeFileSync(out, results.code); });
更多详细信息:frankwallis/duo-typescript
npm install grunt-ts
module.exports = function(grunt) { grunt.initConfig({ ts: { default : { src: ["**/*.ts", "!node_modules/**/*.ts"] } } }); grunt.loadNpmTasks("grunt-ts"); grunt.registerTask("default", ["ts"]);};
更多详细信息:TypeStrong/grunt-ts
npm install gulp-typescript
var gulp = require("gulp");var ts = require("gulp-typescript");gulp.task("default", function () { var tsResult = gulp.src("src/*.ts") .pipe(ts({ noImplicitAny: true, out: "output.js" })); return tsResult.js.pipe(gulp.dest('built/local'));});
更多详细信息:ivogabe/gulp-typescript
npm install -g jspm@beta
注意:目前jspm的0.16beta版本支持TypeScript
更多详细信息:TypeScriptSamples/jspm
npm install ts-loader --save-dev
module.exports = { entry: "./src/index.tsx", output: { filename: "bundle.js" }, resolve: { // Add '.ts' and '.tsx' as a resolvable extension. extensions: ["", ".webpack.js", ".web.js", ".ts", ".tsx", ".js"] }, module: { loaders: [ // all files with a '.ts' or '.tsx' extension will be handled by 'ts-loader' { test: /.tsx?$/, loader: "ts-loader" } ] }};
或者
更新工程文件,包含本地安装的Microsoft.TypeScript.Default.props
(在顶端)和Microsoft.TypeScript.targets
(在底部)文件:
<?xml version="1.0" encoding="utf-8"?><Project ToolsVersion="4.0" DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003"> <!-- Include default props at the bottom --> <Import Project="$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.Default.props" Condition="Exists('$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.Default.props')" /> <!-- TypeScript configurations go here --> <PropertyGroup Condition="'$(Configuration)' == 'Debug'"> <TypeScriptRemoveComments>false</TypeScriptRemoveComments> <TypeScriptSourceMap>true</TypeScriptSourceMap> </PropertyGroup> <PropertyGroup Condition="'$(Configuration)' == 'Release'"> <TypeScriptRemoveComments>true</TypeScriptRemoveComments> <TypeScriptSourceMap>false</TypeScriptSourceMap> </PropertyGroup> <!-- Include default targets at the bottom --> <Import Project="$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets" Condition="Exists('$(MSBuildExtensionsPath32)MicrosoftVisualStudiov$(VisualStudioVersion)TypeScriptMicrosoft.TypeScript.targets')" /></Project>
关于配置MSBuild编译器选项的更多详细信息,请参考:[在MSBuild里使用编译选项](./Compiler Options in MSBuild.md)
Microsoft.TypeScript.MSBuild
Install
更多详细信息请参考Package Manager Dialog和using nightly builds with NuGet
在太平洋标准时间每天午夜会自动构建TypeScript的master
分支代码并发布到NPM和NuGet上。下面将介绍如何获得并在工具里使用它们。
npm install -g typescript@next
注意:你需要配置工程来使用NuGet包。详细信息参考 配置MSBuild工程来使用NuGet。
有两个包:
Microsoft.TypeScript.Compiler
:仅包含工具(tsc.exe
,lib.d.ts
,等。)。Microsoft.TypeScript.MSBuild
:和上面一样的工具,还有的MSBuild的任务和目标(Microsoft.TypeScript.targets
, Microsoft.TypeScript.Default.props
。,等)你还可以配置IDE来使用每日构建。首先你要通过npm安装包。你可以进行全局安装或者安装到本地的node_modules
目录下。
下面的步骤里我们假设你已经安装好了typescript@next
。
更新.vscode/settings.json
如下:
"typescript.tsdk": "<path to your folder>/node_modules/typescript/lib"
详细信息参见VSCode文档。
更新Settings - User
如下:
"typescript_tsdk": "<path to your folder>/node_modules/typescript/lib"
详细信息参见如何在Sublime Text里安装TypeScript插件。
注意:大多数的改变不需要你安装新版本的VS TypeScript插件。
当前的每日构建不包含完整的插件安装包,但是我们正在试着提供每日构建的安装包。
下载VSDevMode.ps1脚本。
参考wiki文档:使用自定义语言服务文件。
在PowerShell命令行窗口里执行:
VS 2015:
VSDevMode.ps1 14 -tsScript <path to your folder>/node_modules/typescript/lib
VS 2013:
VSDevMode.ps1 12 -tsScript <path to your folder>/node_modules/typescript/lib
前往Preferences
> Languages & Frameworks
> TypeScript
:
TypeScript Version: 如果通过NPM安装:
/usr/local/lib/node_modules/typescript/lib