TypeScript官方文档手抄版一镜到底(上篇)-优快云博客
TypeScript官方文档手抄版一镜到底(中篇)-优快云博客
18.0.JSX
JSX是一种嵌入式的类似XML的语法。它可以被转换成合法的JavaScript,尽管转换的语义是依据不同的实现而定的。JSX因React框架而流行,但也存在其它的实现。TypeScript支持内嵌,类型检查以及将JSX直接编译为JavaScript。
18.1.基本用法
想要使用JSX必须做两件事:
1.给文件一个.tsx扩展名
2.启用jsx选项
TypeScript具有三种JSX模式:preserve,react和react-native。这些模式只在代码生成阶段起作用-类型检查并不受影响。在preserve模式下生成代码中会保留JSX以供后续的转换操作使用(比如:Babel)。 另外,输出文件会带有.jsx扩展名。 react模式会生成React.createElement,在使用前不需要再进行转换操作了,输出文件的扩展名为.js。 react-native相当于preserve,它也保留了所有的JSX,但是输出文件的扩展名是.js。
模式 | 输入 | 输出 | 输出文件扩展名 |
preserve | <div /> | <div /> | .jsx |
react | <div /> | React.createElement("div") | .js |
react-native | <div /> | <div /> | .js |
你可以通过在命令行里使用--jsx标记或tsconfig.json里的选项来指定模式。
注意:React标识符是写死的硬编码,所以你必须保证React(大写的R)是可用的。
18.2.as操作符
回想一下怎么写类型断言:
var foo = <foo>bar; |
这里断言bar变量是foo类型的。因为TypeScript也使用尖括号来表示类型断言,在结合JSX的语法后将带来解析上的困难。因此,TypeScript在.tsx文件里禁用了使用尖括号的类型断言。
由于不能够在.tsx文件里使用上述语法,因此我们应该使用另一个类型断言操作符:as。 上面的例子可以很容易地使用as操作符改写:
var foo = bar as foo; |
as操作符在.ts和.tsx里都可用,并且与尖括号类型断言行为是等价的。
18.3.类型检查
为了理解JSX的类型检查,你必须首先理解固有元素与基于值的元素之间的区别。假设有这样一个JSX表达式<expr />,expr可能引用环境自带的某些东西(比如,在DOM环境里的div或span)或者是你自定义的组件。这是非常重要的,原因有如下两点:
1.对于React,固有元素会生成字符串(React.createElement("div")),然而由你自定义的组件却不会生成(React.createElement(MyComponent))。
2.传入JSX元素里的属性类型的查找方式不同。固有元素属性本身就支持,然而自定义的组件会自己去指定它们具有哪个属性。
TypeScript使用与React相同的规范来区别它们。固有元素总是以一个小写字母开头,基于值的元素总是以一个大写字母开头。
18.3.1.固有元素
固有元素使用特殊的接口JSX.IntrinsicElements来查找。默认地,如果这个接口没有指定,会全部通过,不对固有元素进行类型检查。然而,如果这个接口存在,那么固有元素的名字需要在JSX.IntrinsicElements接口的属性里查找。例如:
declare namespace JSX { |
在上例中,<foo />没有问题,但是<bar />会报错,因为它没在JSX.IntrinsicElements里指定。
注意:你也可以在JSX.IntrinsicElements上指定一个用来捕获所有字符串索引:
declare namespace JSX { |
18.3.2.基于值的元素
基于值的元素会简单的在它所在的作用域里按标识符查找。
import MyComponent from "./myComponent"; |
有两种方式可以定义基于值的元素:
1.无状态函数组件(SFC)
2.类组件
由于这两种基于值的元素在JSX表达式里无法区分,因此TypeScript首先会尝试将表达式做为无状态函数组件进行解析。如果解析成功,那么TypeScript就完成了表达式到其声明的解析操作。如果按照无状态函数组件解析失败,那么TypeScript会继续尝试以类组件的形式进行解析。如果依旧失败,那么将输出一个错误。
无状态函数组件:
正如其名,组件被定义成JavaScript函数,它的第一个参数是props对象。TypeScript会强制它的返回值可以赋值给JSX.Element。
interface FooProp { |
由于无状态函数组件是简单的JavaScript函数,所以我们还可以利用函数重载。
interface ClickableProps { |
类组件:
我们可以定义类组件的类型。然而,我们首先最好弄懂两个新的术语:元素类的类型和元素实例的类型。
现在有<Expr />,元素类的类型为Expr的类型。 所以在上面的例子里,如果MyComponent是ES6的类,那么类类型就是类的构造函数和静态部分。如果MyComponent是个工厂函数,类类型为这个函数。
一旦建立起了类类型,实例类型由类构造器或调用签名(如果存在的话)的返回值的联合构成。 再次说明,在ES6类的情况下,实例类型为这个类的实例的类型,并且如果是工厂函数,实例类型为这个函数返回值类型。
class MyComponent { |
元素的实例类型很有趣,因为它必须赋值给JSX.ElementClass或抛出一个错误。默认的JSX.ElementClass为{},但是它可以被扩展用来限制JSX的类型以符合相应的接口。
declare namespace JSX { function MyFactoryFunction() { function NotAValidFactoryFunction() { |
18.3.3.属性类型检查
属性类型检查的第一步是确定元素属性类型。 这在固有元素和基于值的元素之间稍有不同。
对于固有元素,这是JSX.IntrinsicElements属性的类型。
declare namespace JSX { |
对于基于值的元素,就稍微复杂些。它取决于先前确定的在元素实例类型上的某个属性的类型。至于该使用哪个属性来确定类型取决于JSX.ElementAttributesProperty。它应该使用单一的属性来定义。这个属性名之后会被使用。TypeScript 2.8,如果未指定JSX.ElementAttributesProperty,那么将使用类元素构造函数或SFC调用的第一个参数的类型。
declare namespace JSX { |
元素属性类型用于的JSX里进行属性的类型检查。 支持可选属性和必须属性。
declare namespace JSX { |
注意:如果一个属性名不是个合法的JS标识符(像data-*属性),并且它没出现在元素属性类型里时不会当做一个错误。
另外,JSX还会使用JSX.IntrinsicAttributes接口来指定额外的属性,这些额外的属性通常不会被组件的props或arguments使用-比如React里的key。还有,JSX.IntrinsicClassAttributes<T>泛型类型也可以用来做同样的事情。这里的泛型参数表示类实例类型。在React里,它用来允许Ref<T>类型上的ref属性。通常来讲,这些接口上的所有属性都是可选的,除非你想要用户在每个JSX标签上都提供一些属性。
延展操作符也可以使用:
var props = { requiredProp: 'bar' }; |
18.3.4.子孙类型检查
从TypeScript 2.3开始,我们引入了children类型检查。children是元素属性(attribute)类型的一个特殊属性(property),子JSXExpression将会被插入到属性里。与使用JSX.ElementAttributesProperty来决定props名类似,我们可以利用JSX.ElementChildrenAttribute来决定children名。 JSX.ElementChildrenAttribute应该被声明在单一的属性(property)里。
declare namespace JSX { |
如不特殊指定子孙的类型,我们将使用React typings里的默认类型。
<div> |
<script setup lang="ts"> |
18.4.JSX结果类型
默认地JSX表达式结果的类型为any。 你可以自定义这个类型,通过指定JSX.Element接口。 然而,不能够从接口里检索元素,属性或JSX的子元素的类型信息。它是一个黑盒。
18.5.嵌入的表达式
JSX允许你使用{ }标签来内嵌表达式。
var a = <div> |
上面的代码产生一个错误,因为你不能用数字来除以一个字符串。输出如下,若你使用了preserve选项:
var a = <div> |
18.6.React整合
要想一起使用JSX和React,你应该使用React类型定义。这些类型声明定义了JSX合适命名空间来使用React。
/// <reference path="react.d.ts" /> |
18.7.工厂函数
jsx: react编译选项使用的工厂函数是可以配置的。可以使用jsxFactory命令行选项,或内联的@jsx注释指令在每个文件上设置。比如,给createElement设置jsxFactory,<div />会使用createElement("div")来生成,而不是React.createElement("div")。
注释指令可以像下面这样使用(在TypeScript 2.8里):
import preact = require("preact"); |
生成:
const preact = require("preact"); |
工厂函数的选择同样会影响JSX命名空间的查找(类型检查)。如果工厂函数使用React.createElement定义(默认),编译器会先检查React.JSX,之后才检查全局的JSX。如果工厂函数定义为h,那么在检查全局的JSX之前先检查h.JSX。
19.0.装饰器(Java注解?)
随着TypeScript和ES6里引入了类,在一些场景下我们需要额外的特性来支持标注或修改类及其成员。装饰器(Decorators)为我们在类的声明及成员上通过元编程语法添加标注提供了一种方式。Javascript里的装饰器目前处在建议征集的第二阶段,但在TypeScript里已做为一项实验性特性予以支持。
注意:装饰器是一项实验性特性,在未来的版本中可能会发生改变。
若要启用实验性的装饰器特性,你必须在命令行或tsconfig.json里启用experimentalDecorators编译器选项:
命令行:
tsc --target ES5 --experimentalDecorators |
tsconfig.json:
{ |
19.1.装饰器
装饰器是一种特殊类型的声明,它能够被附加到类声明,方法, 访问符,属性或参数上。装饰器使用 @expression这种形式,expression求值后必须为一个函数,它会在运行时被调用,被装饰的声明信息做为参数传入。
例如,有一个@sealed装饰器,我们会这样定义sealed函数:
function sealed(target) { |
注意:后面类装饰器小节里有一个更加详细的例子。
19.2.装饰器工厂
如果我们要定制一个修饰器如何应用到一个声明上,我们得写一个装饰器工厂函数。装饰器工厂就是一个简单的函数,它返回一个表达式,以供装饰器在运行时调用。
我们可以通过下面的方式来写一个装饰器工厂函数:
function color(value: string) { // 这是一个装饰器工厂 |
注意:下面方法装饰器小节里有一个更加详细的例子。
19.3.装饰器组合
多个装饰器可以同时应用到一个声明上,就像下面的示例:
书写在同一行上:
@first @go method() {} |
书写在多行上:
@first() |
当多个装饰器应用于一个声明上,它们求值方式与复合函数相似。在这个模型下,当复合first和go时,复合的结果(first∘ go)(method)等同于first(go(method))。
同样的,在TypeScript里,当多个装饰器应用在一个声明上时会进行如下步骤的操作:
1.由上至下依次对装饰器表达式求值。
2.求值的结果会被当作函数,由下至上依次调用。
如果我们使用装饰器工厂的话,可以通过下面的例子来观察它们求值的顺序:
function first() { |
在控制台里会打印出如下结果:
first(): evaluated go(): evaluated go(): called first(): called |
19.4.装饰器求值
类中不同声明上的装饰器将按以下规定的顺序应用:
1.参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个实例成员。
2.参数装饰器,然后依次是方法装饰器,访问符装饰器,或属性装饰器应用到每个静态成员。
3.参数装饰器应用到构造函数。
4.类装饰器应用到类。
19.5.类装饰器
类装饰器在类声明之前被声明(紧靠着类声明)。类装饰器应用于类构造函数,可以用来监视,修改或替换类定义。类装饰器不能用在声明文件中( .d.ts),也不能用在任何外部上下文中(比如declare的类)。
类装饰器表达式会在运行时当作函数被调用,类的构造函数作为其唯一的参数。
如果类装饰器返回一个值,它会使用提供的构造函数来替换类的声明。
注意:如果你要返回一个新的构造函数,你必须注意处理好原来的原型链。在运行时的装饰器调用逻辑中不会为你做这些。
下面是使用类装饰器(@sealed)的例子,应用在Greeter类:
function sealed(constructor: Function) { |
我们可以这样定义@sealed装饰器:
当@sealed被执行的时候,它将密封此类的构造函数和原型。(注:参见Object.seal)
下面是一个重载构造函数的例子。
function classDecorator<T extends { new(...args: any[]): {} }>(constructor: T) { |
19.6.方法装饰器
方法装饰器声明在一个方法的声明之前(紧靠着方法声明)。它会被应用到方法的属性描述符上,可以用来监视,修改或者替换方法定义。方法装饰器不能用在声明文件( .d.ts),重载或者任何外部上下文(比如declare的类)中。
方法装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.成员的属性描述符。
注意:如果代码输出目标版本小于ES5,属性描述符将会是undefined。如果方法装饰器返回一个值,它会被用作方法的属性描述符。
注意:如果代码输出目标版本小于ES5返回值会被忽略。
下面是一个方法装饰器(@enumerable)的例子,应用于Greeter类的方法上:
class Greeter { |
我们可以用下面的函数声明来定义@enumerable装饰器:
function enumerable(value: boolean) { |
这里的@enumerable(false)是一个装饰器工厂。当装饰器 @enumerable(false)被调用时,它会修改属性描述符的enumerable属性。
19.7.访问器装饰器
访问器装饰器声明在一个访问器的声明之前(紧靠着访问器声明)。访问器装饰器应用于访问器的属性描述符并且可以用来监视,修改或替换一个访问器的定义。访问器装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
注意:TypeScript不允许同时装饰一个成员的get和set访问器。取而代之的是,一个成员的所有装饰的必须应用在文档顺序的第一个访问器上。这是因为,在装饰器应用于一个属性描述符时,它联合了get和set访问器,而不是分开声明的。
访问器装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.成员的属性描述符。
注意:如果代码输出目标版本小于ES5,Property Descriptor将会是undefined。
如果访问器装饰器返回一个值,它会被用作方法的属性描述符。
注意:如果代码输出目标版本小于ES5返回值会被忽略。
下面是使用了访问器装饰器(@configurable)的例子,应用于Point类的成员上:
class Point { |
我们可以通过如下函数声明来定义@configurable装饰器:
function configurable(value: boolean) { |
19.8.属性装饰器
属性装饰器声明在一个属性声明之前(紧靠着属性声明)。属性装饰器不能用在声明文件中(.d.ts),或者任何外部上下文(比如 declare的类)里。
属性装饰器表达式会在运行时当作函数被调用,传入下列2个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
注意:属性描述符不会做为参数传入属性装饰器,这与TypeScript是如何初始化属性装饰器的有关。因为目前没有办法在定义一个原型对象的成员时描述一个实例属性,并且没办法监视或修改一个属性的初始化方法。返回值也会被忽略。因此,属性描述符只能用来监视类中是否声明了某个名字的属性。
我们可以用它来记录这个属性的元数据,如下例所示:
class Greeter { |
然后定义@format装饰器和getFormat函数:
import "reflect-metadata"; |
这个@format("Hello, %s")装饰器是个装饰器工厂。当 @format("Hello, %s")被调用时,它添加一条这个属性的元数据,通过reflect-metadata库里的Reflect.metadata函数。当 getFormat被调用时,它读取格式的元数据。
注意:这个例子需要使用reflect-metadata库。查看元数据了解reflect-metadata库更详细的信息。
19.9.参数装饰器
参数装饰器声明在一个参数声明之前(紧靠着参数声明)。参数装饰器应用于类构造函数或方法声明。参数装饰器不能用在声明文件(.d.ts),重载或其它外部上下文(比如 declare的类)里。
参数装饰器表达式会在运行时当作函数被调用,传入下列3个参数:
1.对于静态成员来说是类的构造函数,对于实例成员是类的原型对象。
2.成员的名字。
3.参数在函数参数列表中的索引。
注意:参数装饰器只能用来监视一个方法的参数是否被传入。
参数装饰器的返回值会被忽略。
下例定义了参数装饰器(@required)并应用于Greeter类方法的一个参数:
class Greeter { |
然后我们使用下面的函数定义 @required 和 @validate 装饰器:
import "reflect-metadata"; |
@required装饰器添加了元数据实体把参数标记为必需的。@validate装饰器把greet方法包裹在一个函数里在调用原先的函数前验证函数参数。
注意:这个例子使用了reflect-metadata库。 查看元数据了解reflect-metadata库的更多信息。
19.10.元数据
一些例子使用了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:
{ |
当启用后,只要reflect-metadata库被引入了,设计阶段添加的类型信息可以在运行时使用。
如下例所示:
import "reflect-metadata"; |
TypeScript编译器可以通过@Reflect.metadata装饰器注入设计阶段的类型信息。 你可以认为它相当于下面的TypeScript:
class Line { |
注意:装饰器元数据是个实验性的特性并且可能在以后的版本中发生破坏性的改变(breaking changes)。
20.0.Mixins
除了传统的面向对象继承方式,还流行一种通过可重用组件创建类的方式,就是联合另一个简单类的代码。 你可能在Scala等语言里对mixins及其特性已经很熟悉了,但它在JavaScript中也是很流行的。
20.1.混入示例
下面的代码演示了如何在TypeScript里使用混入。后面我们还会解释这段代码是怎么工作的。
// Disposable Mixin |
20.2.理解这个例子
代码里首先定义了两个类,它们将做为mixins。可以看到每个类都只定义了一个特定的行为或功能。稍后我们使用它们来创建一个新类,同时具有这两种功能。
// Disposable Mixin |
下面创建一个类,结合了这两个mixins。下面来看一下具体是怎么操作的:
class SmartObject implements Disposable, Activatable { |
首先应该注意到的是,没使用extends而是使用implements。把类当成了接口,仅使用Disposable和Activatable的类型而非其实现。这意味着我们需要在类里面实现接口。但是这是我们在用mixin时想避免的。
我们可以这么做来达到目的,为将要mixin进来的属性方法创建出占位属性。这告诉编译器这些成员在运行时是可用的。这样就能使用mixin带来的便利,虽说需要提前定义一些占位属性。
// Disposable // Activatable |
最后,把mixins混入定义的类,完成全部实现部分。
applyMixins(SmartObject, [Disposable, Activatable]); |
最后,创建这个帮助函数,帮我们做混入操作。它会遍历mixins上的所有属性,并复制到目标上去,把之前的占位属性替换成真正的实现代码。
function applyMixins(derivedCtor: any, baseCtors: any[]) { |
21.0.三斜线指令
三斜线指令是包含单个XML标签的单行注释。注释的内容会做为编译器指令使用。
三斜线指令仅可放在包含它的文件的最顶端。一个三斜线指令的前面只能出现单行或多行注释,这包括其它的三斜线指令。如果它们出现在一个语句或声明之后,那么它们会被当做普通的单行注释,并且不具有特殊的涵义。
/// <reference path="..." /> |
/// <reference path="..." />指令是三斜线指令中最常见的一种。 它用于声明文件间的依赖。三斜线引用告诉编译器在编译过程中要引入的额外的文件。
当使用--out或--outFile时,它也可以做为调整输出内容顺序的一种方法。文件在输出文件内容中的位置与经过预处理后的输入顺序一致。
预处理输入文件:
编译器会对输入文件进行预处理来解析所有三斜线引用指令。在这个过程中,额外的文件会加到编译过程中。
这个过程会以一些根文件开始;它们是在命令行中指定的文件或是在 tsconfig.json中的"files"列表里的文件。这些根文件按指定的顺序进行预处理。在一个文件被加入列表前,它包含的所有三斜线引用都要被处理,还有它们包含的目标。三斜线引用以它们在文件里出现的顺序,使用深度优先的方式解析。
一个三斜线引用路径是相对于包含它的文件的,如果不是根文件。
错误:
引用不存在的文件会报错。一个文件用三斜线指令引用自己会报错。
使用 --noResolve:
如果指定了--noResolve编译选项,三斜线引用会被忽略;它们不会增加新文件,也不会改变给定文件的顺序。
/// <reference types="..." /> |
与/// <reference path="..." />指令相似,这个指令是用来声明依赖的;一个 /// <reference types="..." />指令则声明了对某个包的依赖。
对这些包的名字的解析与在 import语句里对模块名的解析类似。可以简单地把三斜线类型引用指令当做 import声明的包。
例如,把/// <reference types="node" />引入到声明文件,表明这个文件使用了 @types/node/index.d.ts里面声明的名字;并且,这个包需要在编译阶段与声明文件一起被包含进来。
仅当在你需要写一个d.ts文件时才使用这个指令。
对于那些在编译阶段生成的声明文件,编译器会自动地添加/// <reference types="..." />;当且仅当结果文件中使用了引用的包里的声明时才会在生成的声明文件里添加/// <reference types="..." />语句。
若要在.ts文件里声明一个对@types包的依赖,使用--types命令行选项或在tsconfig.json里指定。查看 在tsconfig.json里使用@types,typeRoots和types了解详情。
/// <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指令允许给编译器传入一个可选的模块名:
amdModule.ts
///<amd-module name='NamedModule'/> |
这会将NamedModule传入到AMD define函数里:
amdModule.js
define("NamedModule", ["require", "exports"], function (require, exports) { |
/// <amd-dependency /> |
注意:这个指令被废弃了。使用import "moduleName";语句代替。
/// <amd-dependency path="x" />告诉编译器有一个非TypeScript模块依赖需要被注入,做为目标模块require调用的一部分。
amd-dependency指令也可以带一个可选的name属性;它允许我们为amd-dependency传入一个可选名字:
/// <amd-dependency path="legacy/moduleA" name="moduleA"/> |
生成的JavaScript代码:
define(["require", "exports", "legacy/moduleA"], function (require, exports, moduleA) { |
22.0.JavaScript文件类型检查
TypeScript 2.3以后的版本支持使用--checkJs对.js文件进行类型检查和错误提示。
你可以通过添加// @ts-nocheck注释来忽略类型检查;相反,你可以通过去掉--checkJs设置并添加一个// @ts-check注释来选则检查某些.js文件。你还可以使用// @ts-ignore来忽略本行的错误。如果你使用了tsconfig.json,JS检查将遵照一些严格检查标记,如noImplicitAny,strictNullChecks等。但因为JS检查是相对宽松的,在使用严格标记时可能会有些出乎意料的情况。
对比.js文件和.ts文件在类型检查上的差异,有如下几点需要注意:
22.1.用JSDoc类型表示类型信息
.js文件里,类型可以和在.ts文件里一样被推断出来。同样地,当类型不能被推断时,它们可以通过JSDoc来指定,就好比在.ts文件里那样。如同TypeScript,--noImplicitAny会在编译器无法推断类型的位置报错。(除了对象字面量的情况;后面会详细介绍)。
JSDoc注解修饰的声明会被设置为这个声明的类型。比如:
/** @type {number} */var x; |
你可以在这里找到所有JSDoc支持的模式,JSDoc文档。
属性的推断来自于类内的赋值语句:
ES2015没提供声明类属性的方法。属性是动态赋值的,就像对象字面量一样。
在.js文件里,编译器从类内部的属性赋值语句来推断属性类型。属性的类型是在构造函数里赋的值的类型,除非它没在构造函数里定义或者在构造函数里是undefined或null。若是这种情况,类型将会是所有赋的值的类型的联合类型。在构造函数里定义的属性会被认为是一直存在的,然而那些在方法,存取器里定义的属性被当成可选的。
class C { |
如果一个属性从没在类内设置过,它们会被当成未知的。
如果类的属性只是读取用的,那么就在构造函数里用JSDoc声明它的类型。如果它稍后会被初始化,你甚至都不需要在构造函数里给它赋值:
class C { |
22.2.构造函数等同于类
ES2015以前,Javascript使用构造函数代替类。编译器支持这种模式并能够将构造函数识别为ES2015的类。属性类型推断机制和上面介绍的一致。
function C() { |
22.3.支持CommonJS模块
在.js文件里,TypeScript能识别出CommonJS模块。对exports和module.exports的赋值被识别为导出声明。相似地,require函数调用被识别为模块导入。例如:
// same as `import module "fs"` |
对JavaScript文件里模块语法的支持比在TypeScript里宽泛多了。大部分的赋值和声明方式都是允许的。
22.4.类,函数和对象字面量是命名空间
.js文件里的类是命名空间。它可以用于嵌套类,比如:
class C { |
ES2015之前的代码,它可以用来模拟静态方法:
function Outer() { |
它还可以用于创建简单的命名空间:
var ns = {} |
同时还支持其它的变化:
// 立即调用的函数表达式 |
22.5.对象字面量是开放的
.ts文件里,用对象字面量初始化一个变量的同时也给它声明了类型。新的成员不能再被添加到对象字面量中。这个规则在.js文件里被放宽了;对象字面量具有开放的类型,允许添加并访问原先没有定义的属性。例如:
var obj = {a: 1}; |
对象字面量的表现就好比具有一个默认的索引签名[x:string]: any,它们可以被当成开放的映射而不是封闭的对象。
与其它JS检查行为相似,这种行为可以通过指定JSDoc类型来改变,例如:
/** @type {{a: number}} */ |
22.6.null,undefined,和空数组的类型是any或any[]
任何用null,undefined初始化的变量,参数或属性,它们的类型是any,就算是在严格null检查模式下。任何用[]初始化的变量,参数或属性,它们的类型是any[],就算是在严格null检查模式下。唯一的例外是像上面那样有多个初始化器的属性。
function Foo(i = null) { |
22.7.函数参数是默认可选的:
由于在ES2015之前无法指定可选参数,因此.js文件里所有函数参数都被当做是可选的。 使用比预期少的参数调用函数是允许的。
需要注意的一点是,使用过多的参数调用函数会得到一个错误。
例如:
function bar(a, b) { |
使用JSDoc注解的函数会被从这条规则里移除。 使用JSDoc可选参数语法来表示可选性。比如:
/** function sayHello(somebody) { |
22.8.由arguments推断出的var-args参数声明
如果一个函数的函数体内有对arguments的引用,那么这个函数会隐式地被认为具有一个var-arg参数(比如:(...arg: any[]) => any))。使用JSDoc的var-arg语法来指定arguments的类型。
/** @param {...number} args */ |
22.9.未指定的类型参数默认为any
由于JavaScript里没有一种自然的语法来指定泛型参数,因此未指定的参数类型默认为any。
在extends语句中:
例如,React.Component被定义成具有两个类型参数,Props和State。在一个.js文件里,没有一个合法的方式在extends语句里指定它们。默认地参数类型为any:
import {Component} from "react"; |
使用JSDoc的@augments来明确地指定类型。例如:
import {Component} from "react"; |
在JSDoc引用中:
JSDoc里未指定的类型参数默认为any:
/** @type{Array} */ |
在函数调用中:
泛型函数的调用使用arguments来推断泛型参数。有时候,这个流程不能够推断出类型,大多是因为缺少推断的源;在这种情况下,类型参数类型默认为any。例如:
var p = new Promise((resolve, reject) => { |
22.10.支持的JSDoc
下面的列表列出了当前所支持的JSDoc注解,你可以用它们在JavaScript文件里添加类型信息。
注意,没有在下面列出的标记(例如@async)都是还不支持的。
• @type
• @param (or @arg or @argument)
• @returns (or @return)
• @typedef
• @callback
• @template
• @class (or @constructor)
• @this
• @extends (or @augments)
• @enum
它们代表的意义与usejsdoc.org上面给出的通常是一致的或者是它的超集。下面的代码描述了它们的区别并给出了一些示例。
22.11.@type
可以使用@type标记并引用一个类型名称(原始类型,TypeScript里声明的类型,或在JSDoc里@typedef标记指定的)可以使用任何TypeScript类型和大多数JSDoc类型。
/** |
@type可以指定联合类型—例如,string和boolean类型的联合。
/** |
注意,括号是可选的。
/** |
有多种方式来指定数组类型:
/** @type {number[]} */ |
还可以指定对象字面量类型。 例如,一个带有a(字符串)和b(数字)属性的对象,使用下面的语法:
/** @type {{ a: string, b: number }} */ |
可以使用字符串和数字索引签名来指定map-like和array-like的对象,使用标准的JSDoc语法或者TypeScript语法。
/** |
这两个类型与TypeScript里的{ [x: string]: number }和{ [x: number]: any }是等同的。编译器能识别出这两种语法。
可以使用TypeScript或Closure语法指定函数类型。
/** @type {function(string, boolean): number} Closure syntax */ |
或者直接使用未指定的Function类型:
/** @type {Function} */ |
Closure的其它类型也可以使用:
/** |
转换:
TypeScript借鉴了Closure里的转换语法。在括号表达式前面使用@type标记,可以将一种类型转换成另一种类型。
/** |
导入类型:
可以使用导入类型从其它文件中导入声明。 这个语法是TypeScript特有的,与JSDoc标准不同:
/** function walk(p) { |
导入类型也可以使用在类型别名声明中:
/** |
导入类型可以用在从模块中得到一个值的类型。
/** |
22.12.@param和@returns
@param语法和@type相同,但增加了一个参数名。使用[]可以把参数声明为可选的:
// Parameters may be declared in a variety of syntactic forms |
函数的返回值类型也是类似的:
/** |
22.13.@typedef, @callback, 和 @param
@typedef可以用来声明复杂类型。和@param类似的语法。
/** |
可以在第一行上使用object或Object。
/** |
@param允许使用相似的语法。注意,嵌套的属性名必须使用参数名做为前缀:
/** |
@callback与@typedef相似,但它指定函数类型而不是对象类型:
/** |
当然,所有这些类型都可以使用TypeScript的语法@typedef在一行上声明:
/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */ |
22.14.@template
使用@template声明泛型:
/** |
用逗号或多个标记来声明多个类型参数:
/** |
还可以在参数名前指定类型约束。 只有列表的第一项类型参数会被约束:
/** |
22.15.@constructor
编译器通过this属性的赋值来推断构造函数,但你可以让检查更严格提示更友好,你可以添加一个@constructor标记:
/** |
通过@constructor,this将在构造函数C里被检查,因此你在initialize方法里得到一个提示,如果你传入一个数字你还将得到一个错误提示。如果你直接调用C而不是构造它,也会得到一个错误。
不幸的是,这意味着那些既能构造也能直接调用的构造函数不能使用@constructor。
22.16.@this
编译器通常可以通过上下文来推断出this的类型。但你可以使用@this来明确指定它的类型:
/** |
22.17.@extends
当JavaScript类继承了一个基类,无处指定类型参数的类型。而@extends标记提供了这样一种方式:
/** |
注意@extends只作用于类。当前,无法实现构造函数继承类的情况。
22.18.@enum
@enum标记允许你创建一个对象字面量,它的成员都有确定的类型。不同于JavaScript里大多数的对象字面量,它不允许添加额外成员。
/** @enum {number} */ const JSDocState = { |
注意@enum与TypeScript的@enum大不相同,它更加简单。然而,不同于TypeScript的枚举,@enum可以是任何类型:
/** @enum {function(number): number} */ |
更多示例:
var someObj = { |
已知不支持的模式:
在值空间中将对象视为类型是不可以的,除非对象创建了类型,如构造函数。
function aNormalFunction() { |
对象字面量属性上的=后缀不能指定这个属性是可选的:
/** |
Nullable类型只在启用了strictNullChecks检查时才启作用:
/** |
Non-nullable类型没有意义,以其原类型对待:
/** |
不同于JSDoc类型系统,TypeScript只允许将类型标记为包不包含null。 没有明确的Non-nullable -- 如果启用了strictNullChecks,那么number是非null的。如果没有启用,那么number是可以为null的。
声明文件
23.0.结构
一般来讲,你组织声明文件的方式取决于库是如何被使用的。在JavaScript中一个库有很多使用方式,这就需要你书写声明文件去匹配它们。这篇指南涵盖了如何识别常见库的模式,和怎样书写符合相应模式的声明文件。
针对每种主要的库的组织模式,在模版一节都有对应的文件。 你可以利用它们帮助你快速上手。
23.1.识别库的类型
首先,我们先看一下TypeScript声明文件能够表示的库的类型。这里会简单展示每种类型的库的使用方式,如何去书写,还有一些真实案例。
识别库的类型是书写声明文件的第一步。我们将会给出一些提示,关于怎样通过库的使用方法及其源码来识别库的类型。根据库的文档及组织结构不同,这两种方式可能一个会比另外的那个简单一些。我们推荐你使用任意你喜欢的方式。
23.1.1.全局库
全局库是指能在全局命名空间下访问的(例如:不需要使用任何形式的import)。许多库都是简单的暴露出一个或多个全局变量。比如,如果你使用过 jQuery,$变量可以被够简单的引用:
$(() => { console.log('hello!'); } ); |
你经常会在全局库的指南文档上看到如何在HTML里用脚本标签引用库:
<script src="http://a.great.cdn.for/someLib.js"></script> |
目前,大多数流行的全局访问型库实际上都以UMD库的形式进行书写(见后文)。UMD库的文档很难与全局库文档两者之间难以区分。在书写全局声明文件前,一定要确认一下库是否真的不是UMD。
23.1.2.从代码上识别全局库
全局库的代码通常都十分简单。一个全局的“Hello, world”库可能是这样的:
function createGreeting(s) { |
或这样:
window.createGreeting = function (s) { |
当你查看全局库的源代码时,你通常会看到:
• 顶级的var语句或function声明
• 一个或多个赋值语句到window.someName
• 假设DOM原始值像document或window是存在的
你不会看到:
• 检查是否使用或如何使用模块加载器,比如require或define
• CommonJS/Node.js风格的导入如var fs = require("fs");
• define(...)调用
• 文档里说明了如何去require或导入这个库
全局库的例子:
由于把一个全局库转变成UMD库是非常容易的,所以很少流行的库还再使用全局的风格。然而,小型的且需要DOM(或没有依赖)的库可能还是全局类型的。
全局库模版:
模版文件global.d.ts定义了myLib库作为例子。一定要阅读 "防止命名冲突"补充说明。
23.1.3.模块化库
一些库只能工作在模块加载器的环境下。比如,像 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。
23.2.UMD
UMD模块是指那些既可以作为模块使用(通过导入)又可以作为全局(在没有模块加载器的环境里)使用的模块。许多流行的库,比如 Moment.js,就是这样的形式。比如,在Node.js或RequireJS里,你可以这样写:
import moment = require("moment"); |
然而在纯净的浏览器环境里你也可以这样写:
console.log(moment.format()); |
识别UMD库:
UMD模块会检查是否存在模块加载器环境。 这是非常形容观察到的模块,它们会像下面这样:
(function (root, factory) { |
如果你在库的源码里看到了typeof define,typeof window,或typeof module这样的测试,尤其是在文件的顶端,那么它几乎就是一个UMD库。
UMD库的文档里经常会包含通过require“在Node.js里使用”例子,和“在浏览器里使用”的例子,展示如何使用 <script>标签去加载脚本。
UMD库的例子:
大多数流行的库现在都能够被当成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"); |
一定要阅读补充说明:“ES6模块调用签名的影响”
使用module-class.d.ts如果模块能够使用new来构造:
var x = require("bar"); |
相同的补充说明作用于这些模块。
如果模块不能被调用或构造,使用module.d.ts文件。
23.2.1.模块插件或UMD插件
一个模块插件可以改变一个模块的结构(UMD或模块)。例如,在Moment.js里, moment-range添加了新的range方法到monent对象。
对于声明文件的目标,我们会写相同的代码不论被改变的模块是一个纯粹的模块还是UMD模块。
模版:使用module-plugin.d.ts模版。
23.2.2.全局插件
一个全局插件是全局代码,它们会改变全局对象的结构。 对于 全局修改的模块,在运行时存在冲突的可能。
比如,一些库往Array.prototype或String.prototype里添加新的方法。
识别全局插件:
全局通常很容易地从它们的文档识别出来。
你会看到像下面这样的例子:
var x = "hello, world"; |
模版:使用global-plugin.d.ts模版。
23.2.3.全局修改的模块
当一个全局修改的模块被导入的时候,它们会改变全局作用域里的值。比如,存在一些库它们添加新的成员到 String.prototype当导入它们的时候。这种模式很危险,因为可能造成运行时的冲突,但是我们仍然可以为它们书写声明文件。
识别全局修改的模块:
全局修改的模块通常可以很容易地从它们的文档识别出来。通常来讲,它们与全局插件相似,但是需要 require调用来激活它们的效果。
你可能会看到像下面这样的文档:
// 'require' call that doesn't use its return value |
模版:使用global-modifying-module.d.ts模版。
23.3.使用依赖
可能会有以下几种依赖。
依赖全局库:
如果你的库依赖于某个全局库,使用/// <reference types="..." />指令:
/// <reference types="someLib" /> |
依赖模块:
如果你的库依赖于模块,使用import语句:
import * as moment from "moment"; |
依赖UMD库:
从全局库:
如果你的全局库依赖于某个UMD模块,使用/// <reference types指令:
/// <reference types="moment" /> |
从一个模块或UMD库
如果你的模块或UMD库依赖于一个UMD库,使用import语句:
import * as someLib from 'someLib'; |
不要使用/// <reference指令去声明UMD库的依赖!
23.4.补充说明
注意,在书写全局声明文件时,允许在全局作用域里定义很多类型。我们十分不建义这样做,当一个工程里有许多声明文件时,它会导致无法处理的命名冲突。
一个简单的规则是使用库定义的全局变量名来声明命名空间类型。 比如,库定义了一个全局的值 cats,你可以这样写
declare namespace cats { |
不要
// at top-level |
这样也保证了库在转换成UMD的时候没有任何的破坏式改变,对于声明文件用户来说。
一些插件添加或修改已存在的顶层模块的导出部分。当然这在CommonJS和其它加载器里是允许的,ES模块被当作是不可改变的因此这种模式就不可行了。因为TypeScript是能不预知加载器类型的,所以没没在编译时保证,但是开发者如果要转到ES6模块加载器上应该注意这一点。
很多流行库,比如Express,暴露出自己作为可以调用的函数。比如,典型的Express使用方法如下:
import exp = require("express");var app = exp(); |
在ES6模块加载器里,顶层的对象(这里以exp导入)只能具有属性; 顶层的模块对象 永远不能被调用。 十分常见的解决方法是定义一个 default导出到一个可调用的/可构造的对象; 一会模块加载器助手工具能够自己探测到这种情况并且使用 default导出来替换顶层对象。
24.0.举例
24.1.全局变量
全局变量foo包含了存在组件总数。
console.log("Half the number of widgets is " + (foo / 2)); |
使用declare var声明变量。如果变量是只读的,那么可以使用 declare const。你还可以使用 declare let如果变量拥有块级作用域。
/** 组件总数 */ |
24.2.全局函数
用一个字符串参数调用greet函数向用户显示一条欢迎信息。
greet("hello, world"); |
使用declare function声明函数。
declare function greet(greeting: string): void; |
24.3.带属性的对象
全局变量myLib包含一个makeGreeting函数,还有一个属性 numberOfGreetings指示目前为止欢迎数量。
let result = myLib.makeGreeting("hello, world");
|
使用declare namespace描述用点表示法访问的类型或值。
declare namespace myLib { |
24.4.函数重载
getWidget函数接收一个数字,返回一个组件,或接收一个字符串并返回一个组件数组。
let x: Widget = getWidget(43); |
声明
declare function getWidget(n: number): Widget; |
24.5.可重用类型(接口)
当指定一个欢迎词时,你必须传入一个GreetingSettings对象。 这个对象具有以下几个属性:
1- greeting:必需的字符串
2- duration: 可靠的时长(毫秒表示)
3- color: 可选字符串,比如‘#ff00ff’
greet({ |
使用interface定义一个带有属性的类型。
interface GreetingSettings { |
24.6.可重用类型(类型别名)
在任何需要欢迎词的地方,你可以提供一个string,一个返回string的函数或一个Greeter实例。
function getGreeting() { |
你可以使用类型别名来定义类型的短名:
type GreetingLike = string | (() => string) | MyGreeter; |
24.7.组织类型
greeter对象能够记录到文件或显示一个警告。你可以为 .log(...)提供LogOptions和为.alert(...)提供选项。
const g = new Greeter("Hello"); |
使用命名空间组织类型。
declare namespace GreetingLib { |
你也可以在一个声明中创建嵌套的命名空间:
declare namespace GreetingLib.Options { |
24.8.类
你可以通过实例化Greeter对象来创建欢迎词,或者继承Greeter对象来自定义欢迎词。
const myGreeter = new Greeter("hello, world"); |
使用declare class描述一个类或像类一样的对象。类可以有属性和方法,就和构造函数一样。
declare class Greeter { |
25.0.规范
25.1.普通类型
Number,String,Boolean和Object
不要使用如下类型Number,String,Boolean或Object。这些类型指的是非原始的装盒对象,它们几乎没在JavaScript代码里正确地使用过。
/* 错误 */ |
应该使用类型number,string,and boolean。
/* OK */ |
使用非原始的object类型来代替Object (TypeScript 2.2新增特性)
泛型:要定义一个从来没使用过其类型参数的泛型类型。了解详情 TypeScript FAQ page。
25.2.回调函数类型
回调函数返回值类型:
不要为返回值被忽略的回调函数设置一个any类型的返回值类型:
/* 错误 */ |
应该给返回值被忽略的回调函数设置void类型的返回值类型:
/* OK */ |
为什么:使用void相对安全,因为它防止了你不小心使用x的返回值:
function fn(x: () => void) { |
回调函数里的可选参数:
不要在回调函数里使用可选参数除非你真的要这么做:
/* 错误 */ |
这里有一种特殊的意义:done回调函数可能以1个参数或2个参数调用。代码大概的意思是说这个回调函数不在乎是否有 elapsedTime参数, 但是不需要把这个参数当成可选参数来达到此目的--因为总是允许提供一个接收较少参数的回调函数。
应该写出回调函数的非可选参数:
/* OK */ |
重载与回调函数:
不要因为回调函数参数个数不同而写不同的重载:
/* 错误 */ |
应该只使用最大参数个数写一个重载:
/* OK */ |
为什么:回调函数总是可以忽略某个参数的,因此没必要为参数少的情况写重载。参数少的回调函数首先允许错误类型的函数被传入,因为它们匹配第一个重载。
25.3.函数重载
顺序:
不要把一般的重载放在精确的重载前面:
/* 错误 */ |
应该排序重载令精确的排在一般的之前:
/* OK */ |
为什么:TypeScript会选择第一个匹配到的重载当解析函数调用的时候。当前面的重载比后面的“普通”,那么后面的被隐藏了不会被调用。
使用可选参数:
不要为仅在末尾参数不同时写不同的重载:
/* 错误 */ |
应该尽可能使用可选参数:
/* OK */ |
注意这在所有重载都有相同类型的返回值时会不好用。
为什么:有以下两个重要原因。
TypeScript解析签名兼容性时会查看是否某个目标签名能够使用源的参数调用,且允许外来参数。下面的代码暴露出一个bug,当签名被正确的使用可选参数书写时:
function fn(x: (a: string, b: number, c: number) => void) { } |
第二个原因是当使用了TypeScript“严格检查null”特性时。因为没有指定的参数在JavaScript里表示为 undefined,通常显示地为可选参数传入一个undefined。这段代码在严格null模式下可以工作:
var x: Example; |
使用联合类型:
不要为仅在某个位置上的参数类型不同的情况下定义重载:
/* WRONG */ |
应该尽可能地使用联合类型:
/* OK */ |
注意我们没有让b成为可选的,因为签名的返回值类型不同。
为什么:这对于将值”传递”给函数的人来说非常重要。
function fn(x: string): void; |
26.0.深入声明文件原理
组织模块以提供你想要的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 }; |
这样使用它:
import * as foo from './foo'; |
这可以很好地工作,但是我们知道SomeType和SomeVar很相关因此我们想让他们有相同的名字。我们可以使用组合通过相同的名字 Bar表示这两种不同的对象(值和对象):
export var Bar: { a: Bar }; |
这提供了解构使用的机会:
import {Bar} from './foo'; |
再次地,这里我们使用Bar做为类型和值。注意我们没有声明 Bar值为Bar类型--它们是独立的。
高级组合:有一些声明能够通过多个声明组合。比如, class C { }和interface C { }可以同时存在并且都可以做为C类型的属性。
只要不产生冲突就是合法的。 一个普通的规则是值总是会和同名的其它值产生冲突除非它们在不同命名空间里,类型冲突则发生在使用类型别名声明的情况下(type s = string),命名空间永远不会发生冲突。
让我们看看如何使用。
利用interface添加:
我们可以使用一个interface往别一个interface声明里添加额外成员:
interface Foo { |
这同样作用于类:
class Foo { |
注意我们不能使用接口往类型别名里添加成员(type s = string;)
使用namespace添加:
namespace声明可以用来添加新类型,值和命名空间,只要不出现冲突。
比如,我们可能添加静态成员到一个类:
class C { |
注意在这个例子里,我们添加一个值到C的静态部分(它的构造函数)。这里因为我们添加了一个值,且其它值的容器是另一个值(类型包含于命名空间,命名空间包含于另外的命名空间)。
我们还可以给类添加一个命名空间类型:
class C { |
在这个例子里,直到我们写了namespace声明才有了命名空间C。做为命名空间的 C不会与类创建的值C或类型C相互冲突。
最后,我们可以进行不同的合并通过namespace声明。这不是一个特别现实的例子,但显示了各种有趣的行为:
namespace X { |
在这个例子里,第一个代码块创建了以下名字与含义:
• 一个值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声明会导出或导入目标的所有含义。
27.0.发布
现在我们已经按照指南里的步骤写好一个声明文件,是时候把它发布到npm了。有两种主要方式用来发布声明文件到npm:
1.与你的npm包捆绑在一起,或
2.发布到npm上的@types organization。
如果你能控制要使用你发布的声明文件的那个npm包的话,推荐第一种方式。这样的话,你的声明文件与JavaScript总是在一起传递。
27.1.包含声明文件到你的npm包
如果你的包有一个主.js文件,你还是需要在package.json里指定主声明文件。设置 types属性指向捆绑在一起的声明文件。 比如:
{ |
注意"typings"与"types"具有相同的意义,也可以使用它。
同样要注意的是如果主声明文件名是index.d.ts并且位置在包的根目录里(与index.js并列),你就不需要使用"types"属性指定了。
依赖:
所有的依赖是由npm管理的。确保所依赖的声明包都在 package.json的"dependencies"里指明了。比如,假设我们写了一个包它依赖于Browserify和TypeScript。
{ |
这里,我们的包依赖于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" /> |
务必阅读使用依赖一节了解详情。
打包所依赖的声明:
如果你的类型声明依赖于另一个包:
• 不要把依赖的包放进你的包里,保持它们在各自的文件里。
• 不要将声明拷贝到你的包里。
• 应该依赖于npm类型声明包,如果依赖包没包含它自己的声明的话。
公布你的声明文件:
在发布声明文件包之后,确保在DefinitelyTyped外部包列表里面添加一条引用。 这可以让查找工具知道你的包提供了自己的声明文件。
27.2.发布到@types
@types下面的包是从DefinitelyTyped里自动发布的,通过 types-publisher工具。如果想让你的包发布为@types包,提交一个pull request到https://github.com/DefinitelyTyped/DefinitelyTyped。在这里查看详细信息 contribution guidelines page。
28.0.发布
在TypeScript 2.0,获取、使用和查找声明文件变得十分容易。 这篇文章将详细说明怎么做这三件事。
下载:
在TypeScript 2.0以上的版本,获取类型声明文件只需要使用npm。
比如,获取lodash库的声明文件,只需使用下面的命令:
npm install --save @types/lodash |
如果一个npm包像发布里所讲的一样已经包含了它的声明文件,那就不必再去下载相应的@types包了。
使用:
下载完后,就可以直接在TypeScript里使用lodash了。不论是在模块里还是全局代码里使用。
比如,你已经npm install安装了类型声明,你可以使用导入:
import * as _ from "lodash"; _.padStart("Hello TypeScript!", 20, " "); |
或者如果你没有使用模块,那么你只需使用全局的变量_。
_.padStart("Hello TypeScript!", 20, " "); |
查找:
大多数情况下,类型声明包的名字总是与它们在npm上的包的名字相同,但是有@types/前缀,但如果你需要的话,你可以在 TypeScript: Search for typed packages这里查找你喜欢的库。
注意:如果你要找的声明文件不存在,你可以贡献一份,这样就方便了下一位要使用它的人。 查看DefinitelyTyped 贡献指南页了解详情。
项目配置
29.0.tsconfig
如果一个目录下存在一个tsconfig.json文件,那么它意味着这个目录是TypeScript项目的根目录。tsconfig.json文件中指定了用来编译这个项目的根文件和编译选项。一个项目可以通过以下方式之一来编译:
使用tsconfig.json:
不带任何输入文件的情况下调用tsc,编译器会从当前目录开始去查找tsconfig.json文件,逐级向上搜索父目录。
不带任何输入文件的情况下调用tsc,且使用命令行参数--project(或-p)指定一个包含tsconfig.json文件的目录。
当命令行上指定了输入文件时,tsconfig.json文件会被忽略。
示例:tsconfig.json示例文件。
使用"files"属性
{ |
使用"include"和"exclude"属性
{ |
细节:
"compilerOptions"可以被忽略,这时编译器会使用默认值。在这里查看完整的编译器选项列表。
"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和<outDir>目录。
任何被"files"或"include"指定的文件所引用的文件也会被包含进来。A.ts引用了B.ts,因此B.ts不能被排除,除非引用它的A.ts在"exclude"列表中。
需要注意编译器不会去引入那些可能做为输出的文件;比如,假设我们包含了index.ts,那么index.d.ts和index.js会被排除在外。通常来讲,不推荐只有扩展名的不同来区分同目录下的文件。
tsconfig.json文件可以是个空文件,那么所有默认的文件(如上面所述)都会以默认配置选项编译。
在命令行上指定的编译选项会覆盖在tsconfig.json文件里的相应选项。
@types,typeRoots和types:
默认所有可见的"@types"包会在编译过程中被包含进来。node_modules/@types文件夹下以及它们子文件夹下的所有包都是可见的;也就是说,./node_modules/@types/,../node_modules/@types/和../../node_modules/@types/等等。
如果指定了typeRoots,只有typeRoots下面的包才会被包含进来。比如:
{ |
这个配置文件会包含所有./typings下面的包,而不包含./node_modules/@types里面的包。
如果指定了types,只有被列出来的包才会被包含进来。比如:
{ |
这个tsconfig.json文件将仅会包含 ./node_modules/@types/node,./node_modules/@types/lodash和./node_modules/@types/express。/@types/。node_modules/@types/*里面的其它包不会被引入进来。
指定"types": []来禁用自动引入@types包。
注意,自动引入只在你使用了全局的声明(相反于模块)时是重要的。如果你使用 import "foo"语句,TypeScript仍然会查找node_modules和node_modules/@types文件夹来获取foo包。
使用extends继承配置:
tsconfig.json文件可以利用extends属性从另一个配置文件里继承配置。
extends是tsconfig.json文件里的顶级属性(与compilerOptions,files,include,和exclude一样)。 extends的值是一个字符串,包含指向另一个要继承文件的路径。
在原文件里的配置先被加载,然后被来至继承文件里的配置重写。如果发现循环引用,则会报错。
来至所继承配置文件的files,include和exclude覆盖源配置文件的属性。
配置文件里的相对路径在解析时相对于它所在的文件。
比如:configs/base.json。
{ |
tsconfig.json:
{ |
tsconfig.nostrictnull.json:
{ |
compileOnSave:
在最顶层设置compileOnSave标记,可以让IDE在保存文件的时候根据tsconfig.json重新生成文件。
{ |
要想支持这个特性需要Visual Studio 2015, TypeScript1.8.4以上并且安装atom-typescript插件。
模式:到这里查看模式: http://json.schemastore.org/tsconfig.
30.0.项目引用
工程引用是TypeScript 3.0的新特性,它支持将TypeScript程序的结构分割成更小的组成部分。
这样可以改善构建时间,强制在逻辑上对组件进行分离,更好地组织你的代码。
TypeScript 3.0还引入了tsc的一种新模式,即--build标记,它与工程引用协同工作可以加速TypeScript的构建。
30.1.一个工程示例
让我们来看一个非常普通的工程,并瞧瞧工程引用特性是如何帮助我们更好地组织代码的。 假设这个工程具有两个模块:converter和unites,以及相应的测试代码:
/src/converter.ts /src/units.ts /test/converter-tests.ts /test/units-tests.ts /tsconfig.json |
测试文件导入相应的实现文件并进行测试:
// converter-tests.ts |
在之前,这种使用单一tsconfig文件的结构会稍显笨拙:
• 实现文件也可以导入测试文件
• 无法同时构建test和src,除非把src也放在输出文件夹中,但通常并不想这样做
• 仅对实现文件的内部细节进行改动,必需再次对测试进行类型检查,尽管这是根本不必要的
• 仅对测试文件进行改动,必需再次对实现文件进行类型检查,尽管其实什么都没有变
你可以使用多个tsconfig文件来解决部分问题,但是又会出现新问题:
• 缺少内置的实时检查,因此你得多次运行tsc
• 多次调用tsc会增加我们等待的时间
• tsc -w不能一次在多个配置文件上运行
工程引用可以解决全部这些问题,而且还不止。
30.2.何为工程引用
tsconfig.json增加了一个新的顶层属性references。它是一个对象的数组,指明要引用的工程:
{ |
每个引用的path属性都可以指向到包含tsconfig.json文件的目录,或者直接指向到配置文件本身(名字是任意的)。
当你引用一个工程时,会发生下面的事:
• 导入引用工程中的模块实际加载的是它输出的声明文件(.d.ts)。
• 如果引用的工程生成一个outFile,那么这个输出文件的.d.ts文件里的声明对于当前工程是可见的。
• 构建模式(后文)会根据需要自动地构建引用的工程。
当你拆分成多个工程后,会显著地加速类型检查和编译,减少编辑器的内存占用,还会改善程序在逻辑上进行分组。
30.3.composite
引用的工程必须启用新的composite设置。 这个选项用于帮助TypeScript快速确定引用工程的输出文件位置。若启用composite标记则会发生如下变动:
• 对于rootDir设置,如果没有被显式指定,默认为包含tsconfig文件的目录
• 所有的实现文件必须匹配到某个include模式或在files数组里列出。如果违反了这个限制,tsc会提示你哪些文件未指定。
• 必须开启declaration选项。
30.4.declarationMaps
我们增加了对declaration source maps的支持。 如果启用--declarationMap,在某些编辑器上,你可以使用诸如“Go to Definition”,重命名以及跨工程编辑文件等编辑器特性。
30.5.带prepend的outFile
你可以在引用中使用prepend选项来启用前置某个依赖的输出:
"references": [ |
前置工程会将工程的输出添加到当前工程的输出之前。它对.js文件和.d.ts文件都有效,source map文件也同样会正确地生成。
tsc永远只会使用磁盘上已经存在的文件来进行这个操作,因此你可能会创建出一个无法生成正确输出文件的工程,因为有些工程的输出可能会在结果文件中重覆了多次。 例如:
A ^ ^ / \ B C ^ ^ \ / D |
这种情况下,不能前置引用,因为在D的最终输出里会有两份A存在 - 这可能会发生未知错误。
30.6.关于工程引用的说明
工程引用在某些方面需要你进行权衡.
因为有依赖的工程要使用它的依赖生成的.d.ts,因此你必须要检查相应构建后的输出或在下载源码后进行构建,然后才能在编辑器里自由地导航。 我们是在操控幕后的.d.ts生成过程,我们应该减少这种情况,但是目前还们建议提示开发者在下载源码后进行构建。
此外,为了兼容已有的构建流程,tsc不会自动地构建依赖项,除非启用了--build选项。 下面让我们看看--build。
30.7.TypeScript构建模式
在TypeScript工程里支持增量构建是个期待已久的功能。在TypeScrpt 3.0里,你可以在tsc上使用--build标记。它实际上是个新的tsc入口点,它更像是一个构建的协调员而不是简简单单的编译器。
运行tsc --build(简写tsc -b)会执行如下操作:
• 找到所有引用的工程
• 检查它们是否为最新版本
• 按顺序构建非最新版本的工程
可以给tsc -b指定多个配置文件地址(例如:tsc -b src test)。如同tsc -p,如果配置文件名为tsconfig.json,那么文件名则可省略。
tsc -b命令行:
你可以指令任意数量的配置文件:
> tsc -b # Build the tsconfig.json in the current directory > tsc -b src # Build src/tsconfig.json > tsc -b foo/release.tsconfig.json bar # Build foo/release.tsconfig.json and bar/tsconfig.json |
不需要担心命令行上指定的文件顺序 - tsc会根据需要重新进行排序,被依赖的项会优先构建。
tsc -b还支持其它一些选项:
• --verbose:打印详细的日志(可以与其它标记一起使用)
• --dry: 显示将要执行的操作但是并不真正进行这些操作
• --clean: 删除指定工程的输出(可以与--dry一起使用)
• --force: 把所有工程当作非最新版本对待
• --watch: 观察模式(可以与--verbose一起使用)
30.8.说明
一般情况下,就算代码里有语法或类型错误,tsc也会生成输出(.js和.d.ts),除非你启用了noEmitOnError选项。这在增量构建系统里就不好了-如果某个过期的依赖里有一个新的错误,那么你只能看到它一次,因为后续的构建会跳过这个最新的工程。正是这个原因,tsc -b的作用就好比在所有工程上启用了noEmitOnError。
如果你想要提交所有的构建输出(.js, .d.ts, .d.ts.map等),你可能需要运行--force来构建,因为一些源码版本管理操作依赖于源码版本管理工具保存的本地拷贝和远程拷贝的时间戳。
30.9.MSBuild
如果你的工程使用msbuild,你可以用下面的方式开启构建模式。
<TypeScriptBuildMode>true</TypeScriptBuildMode> |
将这段代码添加到proj文件。它会自动地启用增量构建模式和清理工作。
注意,在使用tsconfig.json / -p时,已存在的TypeScript工程属性会被忽略-因此所有的设置需要在tsconfig文件里进行。
一些团队已经设置好了基于msbuild的构建流程,并且tsconfig文件具有和它们匹配的工程一致的隐式图序。若你的项目如此,那么可以继续使用msbuild和tsc -p以及工程引用;它们是完全互通的。
30.10.指导
整体结构:
当tsconfig.json多了以后,通常会使用配置文件继承来集中管理公共的编译选项。这样你就可以在一个文件里更改配置而不必在多个文件中进行修改。
另一个最佳实践是有一个solution级别的tsconfig.json文件,它仅仅用于引用所有的子工程。 它用于提供一个简单的入口;比如,在TypeScript源码里,我们可以简单地运行tsc -b src来构建所有的节点,因为我们在src/tsconfig.json文件里列出了所有的子工程。注意从3.0开始,如果tsconfig.json文件里有至少一个工程引用reference,那么files数组为空的话也不会报错。
你可以在TypeScript源码仓库里看到这些模式-阅读src/tsconfig_base.json,src/tsconfig.json和src/tsc/tsconfig.json。
相对模块的结构:
通常地,将代码转成使用相对模块并不需要改动太多。只需在某个给定父目录的每个子目录里放一个tsconfig.json文件,并相应添加reference。然后将outDir指定为输出目录的子目录或将rootDir指定为所有工程的某个公共根目录。
outFile的结构:
使用了outFile的编译输出结构十分灵活,因为相对路径是无关紧要的。要注意的是,你通常不需要使用prepend -因为这会改善构建时间并结省I/O。TypeScript项目本身是一个好的参照 - 我们有一些“library”的工程和一些“endpoint”工程,“endpoint”工程会确保足够小并仅仅导入它们需要的“library”。
31.0.构建工具集成
31.1.Browserify
安装:
npm install tsify |
使用命令行交互:
browserify main.ts -p [ tsify --noImplicitAny ] > bundle.js |
使用API:
var browserify = require("browserify"); |
更多详细信息:smrq/tsify
31.2.Duo
安装:
npm install duo-typescript |
使用命令行交互:
duo --use duo-typescript entry.ts |
使用API:
var Duo = require('duo'); |
更多详细信息:frankwallis/duo-typescript
31.3.Grunt
安装:
npm install grunt-ts |
基本Gruntfile.js:
module.exports = function(grunt) { |
更多详细信息:TypeStrong/grunt-ts
31.4.Gulp
安装:
npm install gulp-typescript |
基本gulpfile.js:
var gulp = require("gulp"); |
更多详细信息:ivogabe/gulp-typescript
31.5.Jspm
安装:
npm install -g jspm@beta |
注意:目前jspm的0.16beta版本支持TypeScript
更多详细信息:TypeScriptSamples/jspm
31.6.Webpack
安装:
npm install ts-loader --save-dev |
基本webpack.config.js
module.exports = { |
31.7.MSBuild
更新工程文件,包含本地安装的Microsoft.TypeScript.Default.props(在顶端)和Microsoft.TypeScript.targets(在底部)文件:
<?xml version="1.0" encoding="utf-8"?> |
关于配置MSBuild编译器选项的更多详细信息,请参考:在MSBuild里使用编译选项
31.8.NuGet
• 右键点击 -> Manage NuGet Packages
• 查找Microsoft.TypeScript.MSBuild
• 点击Install
• 安装完成后,Rebuild。
更多详细信息请参考Package Manager Dialog和using nightly builds with NuGet
32.0.每日构建
在太平洋标准时间每天午夜会自动构建TypeScript的master分支代码并发布到NPM和NuGet上。下面将介绍如何获得并在工具里使用它们。
使用npm:
npm install -g typescript@next |
使用NuGet和MSBuild:
注意:你需要配置工程来使用NuGet包。详细信息参考配置MSBuild工程来使用NuGet。
有两个包:
• Microsoft.TypeScript.Compiler: 仅包含工具 (tsc.exe,lib.d.ts,等。) 。
• Microsoft.TypeScript.MSBuild: 和上面一样的工具,还有MSBuild的任务和目标(Microsoft.TypeScript.targets,Microsoft.TypeScript.Default.props,等。)
更新IDE来使用每日构建:
你还可以配置IDE来使用每日构建。首先你要通过npm安装包。你可以进行全局安装或者安装到本地的 node_modules目录下。
下面的步骤里我们假设你已经安装好了typescript@next。
Visual Studio Code
更新.vscode/settings.json如下:
"typescript.tsdk": "<path to your folder>/node_modules/typescript/lib" |
详细信息参见VSCode文档。
Sublime Text
更新Settings - User如下:
"typescript_tsdk": "<path to your folder>/node_modules/typescript/lib" |
详细信息参见如何在Sublime Text里安装TypeScript插件。
Visual Studio 2013 and 2015
注意:大多数的改变不需要你安装新版本的VS TypeScript插件。
当前的每日构建不包含完整的插件安装包,但是我们正在试着提供每日构建的安装包。
1.下载VSDevMode.ps1脚本。参考wiki文档:使用自定义语言服务文件。
2.在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 |
IntelliJ IDEA (Mac)
前往Preferences > Languages & Frameworks > TypeScript:TypeScript Version: 如果通过NPM安装:/usr/local/lib/node_modules/typescript/lib