Gowhich

Durban's Blog

继续上篇文章[如何创建高质量的TypeScript声明文件(一)]

模块化库

有些库只能在模块加载器环境中工作。 例如,因为express仅适用于Node.js,必须使用CommonJS require函数加载。

ECMAScript 2015(也称为ES2015,ECMAScript 6和ES6),CommonJS和RequireJS具有类似的导入模块的概念。 例如,在JavaScript CommonJS(Node.js)中,您可以编写

1
var fs = require("fs");

在TypeScript或ES6中,import关键字用于相同的目的:

1
import fs = require("fs");

您通常会看到模块化库在其文档中包含以下行之一:

1
var someLib = require('someLib');

1
2
3
define(..., ['someLib'], function(someLib) {

});

与全局模块一样,您可能会在UMD模块的文档中看到这些示例,因此请务必查看代码或文档。

从代码中识别模块库

模块化库通常至少具有以下某些功能:

  • 无条件调用require或define
  • import * as a from ‘b’的声明,或export c;
  • 对exports或module.exports的赋值

他们很少会:

  • 分配window或global的属性

模块化库的示例

许多流行的Node.js库都在模块系列中,例如express,gulp和request。

UMD

UMD模块可以用作模块(通过导入),也可以用作全局模块(在没有模块加载器的环境中运行)。 许多流行的库,如Moment.js,都是以这种方式编写的。 例如,在Node.js中或使用RequireJS,您可以编写:

1
2
import moment = require("moment");
console.log(moment.format());

而在vanilla浏览器环境中,你会写:

1
console.log(moment.format());

识别UMD库

UMD模块检查是否存在模块加载器环境。 这是一个易于查看的模式,看起来像这样:

1
2
3
4
5
6
7
8
9
(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库的文档通常还会演示一个”Using in Node.js”示例,其中显示了require,以及一个”Using in the browser”示例,该示例显示了使用<script>标记加载脚本。

UMD库的示例

大多数流行的库现在都可以作为UMD包使用。 示例包括jQuery,Moment.js,lodash等等。

模板

模块有三个模板,module.d.ts,module-class.d.ts和module-function.d.ts。

如果您的模块可以像函数一样被调用,请使用module-function.d.ts:

1
2
3
var x = require("foo");
// Note: calling 'x' as a function
var y = x(42);

请务必阅读脚注”ES6对模块调用签名的影响

如果您的模块可以使用new构建,请使用module-class.d.ts:

1
2
3
var x = require("bar");
// Note: using 'new' operator on the imported variable
var y = new x("hello");

同样的脚注适用于这些模块。

如果您的模块不可调用或可构造,请使用module.d.ts文件。

未完待续…

库结构

“库结构”可帮助您了解常用库格式以及如何为每种格式编写正确的声明文件。 如果您正在编辑现有文件,则可能不需要阅读这篇文章。 新声明文件的作者必须阅读本篇文章以正确理解库的格式如何影响声明文件的写入。

介绍

从广义上讲,构造声明文件的方式取决于库的使用方式。 有许多方法可以在JavaScript中提供供消费的库,你需要编写你的声明文件来匹配它。 本篇文章介绍了如何识别公共库模式,以及如何编写与该模式对应的声明文件。

每种类型的主要库结构模式在“模板”部分中都有相应的文件。 您可以从这些模板开始,以帮助您更快地前进。

识别各种库的类型

首先,我们将回顾TypeScript声明文件可以表示的库类型。 我们将简要介绍如何使用各种库,如何编写,以及列出现实世界中的一些示例库。

识别库的结构是编写其声明文件的第一步。 我们将根据其用法和代码给出关于如何识别结构的提示。 根据图书馆的文档和组织,一个可能比另一个更容易。 我们建议您使用哪种方式更舒适。

全局库

全局库是可以从全局范围访问的库(即不使用任何形式的导入)。 许多库只是公开一个或多个全局变量以供使用。 例如,如果您使用的是jQuery,只需引用它就可以使用$变量:

您通常会在全局库的文档中看到如何在HTML脚本标记中使用库的指导:

1
<script src="http://a.great.cdn.for/someLib.js"></script>

今天,大多数流行的全球可访问库实际上都是作为UMD库编写的(见下文)。 UMD库文档很难与全局库文档区分开来。 在编写全局声明文件之前,请确保该库实际上不是UMD。

从代码中识别全局库

全局库代码通常非常简单。 全局“Hello,world”库可能如下所示:

1
2
3
function createGreeting(s) {
return "Hello, " + s;
}

或者像这样:

1
2
3
window.createGreeting = function(s) {
return "Hello, " + s;
}

查看全局库的代码时,您通常会看到:

  • 顶级var语句或函数声明
  • window.someName的一个或多个赋值
  • 存在DOM原语(如文档或窗口)的假设

你不会看到:

  • 检查或使用模块加载器,如require或define
  • CommonJS/Node.js样式导入形式var fs = require(”fs”);
  • 调用define(…)
  • 描述如何require或import库的文档

全局库的例子

因为将全局库转换为UMD库通常很容易,所以很少有流行的库仍然以全局风格编写。 但是,小而且需要DOM(或没有依赖关系)的库可能仍然是全局的。

全局库模板

模板文件global.d.ts定义了一个示例库myLib。 请务必阅读”防止名称冲突”脚注。

未完待续…

继续上篇文章【TypeScript基础入门之Javascript文件类型检查(四)

@constructor

编译器根据此属性赋值推断构造函数,但如果添加@constructor标记,则可以更好地检查更严格和更好的建议:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
/**
* @constructor
* @param {number} data
*/
function C(data) {
this.size = 0;
this.initialize(data); // Should error, initializer expects a string
}
/**
* @param {string} s
*/
C.prototype.initialize = function (s) {
this.size = s.length
}

var c = new C(0);
var result = C(1); // C should only be called with new

使用@constructor,在构造函数C中检查它,因此您将获得初始化方法的建议,如果您传递一个数字,则会出错。 如果您调用C而不是构造它,您也会收到错误。

不幸的是,这意味着也可以调用的构造函数不能使用@constructor。

@this

当编译器有一些上下文可以使用时,它通常可以找出它的类型。 如果没有,您可以使用@this显式指定此类型:

1
2
3
4
5
6
7
/**
* @this {HTMLElement}
* @param {*} e
*/
function callbackForLater(e) {
this.clientHeight = parseInt(e) // should be fine!
}

@extends

当Javascript类扩展通用基类时,无处可指定类型参数应该是什么。 @extends标记为该类型参数提供了一个位置:

1
2
3
4
5
6
7
/**
* @template T
* @extends {Set<T>}
*/
class SortableSet extends Set {
// ...
}

请注意,@ extends仅适用于类。 目前,构造函数没有办法扩展一个类。

@enum

@enum标记允许您创建一个对象文字,其成员都是指定的类型。 与Javascript中的大多数对象文字不同,它不允许其他成员。

1
2
3
4
5
6
/** @enum {number} */
const JSDocState = {
BeginningOfLine: 0,
SawAsterisk: 1,
SavingComments: 2,
}

请注意,@enum与Typescript的枚举完全不同,并且简单得多。 但是,与Typescript的枚举不同,@enum可以有任何类型:

1
2
3
4
5
6
/** @enum {function(number): number} */
const Math = {
add1: n => n + 1,
id: n => -n,
sub1: n => n - 1,
}

更多示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var someObj = {
/**
* @param {string} param1 - Docs on property assignments work
*/
x: function(param1){}
};

/**
* As do docs on variable assignments
* @return {Window}
*/
let someFunc = function(){};

/**
* And class methods
* @param {string} greeting The greeting to use
*/
Foo.prototype.sayHi = (greeting) => console.log("Hi!");

/**
* And arrow functions expressions
* @param {number} x - A multiplier
*/
let myArrow = x => x * x;

/**
* Which means it works for stateless function components in JSX too
* @param {{a: string, b: number}} test - Some param
*/
var sfc = (test) => <div>{test.a.charAt(0)}</div>;

/**
* A parameter can be a class constructor, using Closure syntax.
*
* @param {{new(...args: any[]): object}} C - The class to register
*/
function registerClass(C) {}

/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn10(p1){}

/**
* @param {...string} p1 - A 'rest' arg (array) of strings. (treated as 'any')
*/
function fn9(p1) {
return p1.join();
}

已知的模式不受支持

引用值空间中的对象,因为类型不起作用,除非对象也创建类型,如构造函数。

1
2
3
4
5
6
7
8
9
10
11
12
function aNormalFunction() {

}
/**
* @type {aNormalFunction}
*/
var wrong;
/**
* Use 'typeof' instead:
* @type {typeof aNormalFunction}
*/
var right;

对象文字类型中的属性类型的Postfix等于未指定可选属性:

1
2
3
4
5
6
7
8
9
/**
* @type {{ a: string, b: number= }}
*/
var wrong;
/**
* Use postfix question on the property name instead:
* @type {{ a: string, b?: number }}
*/
var right;

如果启用了strictNullChecks,则可空类型仅具有意义:

1
2
3
4
5
6
/**
* @type {?number}
* With strictNullChecks: true -- number | null
* With strictNullChecks: off -- number
*/
var nullable;

非可空类型没有任何意义,并且被视为原始类型:

1
2
3
4
5
/**
* @type {!number}
* Just has type number
*/
var normal;

与JSDoc的类型系统不同,Typescript只允许您将类型标记为包含null或不包含null。 没有明确的非可空性 - 如果启用了strictNullChecks,则number不可为空。 如果它关闭,则number可以为空。

继续上篇文章【TypeScript基础入门之Javascript文件类型检查(三)

@param 和 @returns

@param使用与@type相同的类型语法,但添加了参数名称。 通过用方括号括起名称,也可以声明该参数是可选的:

1
2
3
4
5
6
7
8
9
10
11
// Parameters may be declared in a variety of syntactic forms
/**
* @param {string} p1 - A string param.
* @param {string=} p2 - An optional param (Closure syntax)
* @param {string} [p3] - Another optional param (JSDoc syntax).
* @param {string} [p4="test"] - An optional param with a default value
* @return {string} This is the result
*/
function stringsStringStrings(p1, p2, p3, p4){
// TODO
}

同样,对于函数的返回类型:

1
2
3
4
5
6
7
8
9
/**
* @return {PromiseLike<string>}
*/
function ps(){}

/**
* @returns {{ a: string, b: number }} - May use '@returns' as well as '@return'
*/
function ab(){}

@typedef, @callback, 和 @param

@typedef可用于定义复杂类型。 类似的语法适用于@param。

1
2
3
4
5
6
7
8
9
10
/**
* @typedef {Object} SpecialType - creates a new type named 'SpecialType'
* @property {string} prop1 - a string property of SpecialType
* @property {number} prop2 - a number property of SpecialType
* @property {number=} prop3 - an optional number property of SpecialType
* @prop {number} [prop4] - an optional number property of SpecialType
* @prop {number} [prop5=42] - an optional number property of SpecialType with default
*/
/** @type {SpecialType} */
var specialTypeObject;

您可以在第一行使用对象或对象。

1
2
3
4
5
6
7
8
/**
* @typedef {object} SpecialType1 - creates a new type named 'SpecialType'
* @property {string} prop1 - a string property of SpecialType
* @property {number} prop2 - a number property of SpecialType
* @property {number=} prop3 - an optional number property of SpecialType
*/
/** @type {SpecialType1} */
var specialTypeObject1;

@param允许一次性类型规范使用类似的语法。 请注意,嵌套属性名称必须以参数名称为前缀:

1
2
3
4
5
6
7
8
9
10
11
/**
* @param {Object} options - The shape is the same as SpecialType above
* @param {string} options.prop1
* @param {number} options.prop2
* @param {number=} options.prop3
* @param {number} [options.prop4]
* @param {number} [options.prop5=42]
*/
function special(options) {
return (options.prop4 || 1001) + options.prop5;
}

@callback类似于@typedef,但它指定了一个函数类型而不是一个对象类型:

1
2
3
4
5
6
7
8
/**
* @callback Predicate
* @param {string} data
* @param {number} [index]
* @returns {boolean}
*/
/** @type {Predicate} */
const ok = s => !(s.length % 2);

当然,任何这些类型都可以在单行@typedef中使用Typescript语法声明:

1
2
/** @typedef {{ prop1: string, prop2: string, prop3?: number }} SpecialType */
/** @typedef {(data: string, index?: number) => boolean} Predicate */

@template

您可以使用@template标记声明泛型类型:

1
2
3
4
5
6
/**
* @template T
* @param {T} p1 - A generic parameter that flows through to the return type
* @return {T}
*/
function id(x){ return x }

使用逗号或多个标签声明多个类型参数:

1
2
3
4
/**
* @template T,U,V
* @template W,X
*/

您还可以在类型参数名称之前指定类型约束。 只限列表中的第一个类型参数:

1
2
3
4
5
6
7
8
9
/**
* @template {string} K - K must be a string or string literal
* @template {{ serious(): string }} Seriousalizable - must have a serious method
* @param {K} key
* @param {Seriousalizable} object
*/
function seriousalize(key, object) {
// ????
}

未完待续…

继续上篇文章【TypeScript基础入门之Javascript文件类型检查(二)

支持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上给出的标记含义相同或超​​集。下面的代码描述了这些差异,并给出了每个标记的一些示例用法。

@type

您可以使用”@type“标记并引用类型名称(原语,在TypeScript声明中定义,或在JSDoc”@typedef“标记中)。您可以使用任何Typescript类型和大多数JSDoc类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
/**
* @type {string}
*/
var s;

/** @type {Window} */
var win;

/** @type {PromiseLike<string>} */
var promisedString;

// You can specify an HTML Element with DOM properties
/** @type {HTMLElement} */
var myElement = document.querySelector(selector);
element.dataset.myData = '';

@type可以指定联合类型 - 例如,某些东西可以是字符串或布尔值。

1
2
3
4
/**
* @type {(string | boolean)}
*/
var sb;

请注意,括号对于联合类型是可选的。

1
2
3
4
/**
* @type {string | boolean}
*/
var sb;

您可以使用各种语法指定数组类型:

1
2
3
4
5
6
/** @type {number[]} */
var ns;
/** @type {Array.<number>} */
var nds;
/** @type {Array<number>} */
var nas;

您还可以指定对象文字类型。例如,具有属性’a’(字符串)和’b’(数字)的对象使用以下语法:

1
2
/** @type {{ a: string, b: number }} */
var var9;

您可以使用标准JSDoc语法或Typescript语法,使用字符串和数字索引签名指定类似地图和类似数组的对象。

1
2
3
4
5
6
7
8
9
/**
* A map-like object that maps arbitrary `string` properties to `number`s.
*
* @type {Object.<string, number>}
*/
var stringToNumber;

/** @type {Object.<number, object>} */
var arrayLike;

前两种类型等同于Typescript类型{ [x: string]: number }和{ [x: number]: any }。编译器理解这两种语法。

您可以使用Typescript或Closure语法指定函数类型:

1
2
3
4
/** @type {function(string, boolean): number} Closure syntax */
var sbn;
/** @type {(s: string, b: boolean) => number} Typescript syntax */
var sbn2;

或者您可以使用未指定的函数类型:

1
2
3
4
/** @type {Function} */
var fn7;
/** @type {function} */
var fn6;

Closure的其他类型也有效:

1
2
3
4
5
6
7
8
/**
* @type {*} - can be 'any' type
*/
var star;
/**
* @type {?} - unknown type (same as 'any')
*/
var question;

类型转换

Typescript借用了Closure的强制语法。这允许您通过在任何带括号的表达式之前添加@type标记将类型转换为其他类型。

1
2
3
4
5
/**
* @type {number | string}
*/
var numberOrString = Math.random() < 0.5 ? "hello" : 100;
var typeAssertedNumber = /** @type {number} */ (numberOrString)

导入类型

您还可以使用导入类型从其他文件导入声明。此语法是特定于Typescript的,与JSDoc标准不同:

1
2
3
4
5
6
/**
* @param p { import("./a").Pet }
*/
function walk(p) {
console.log(`Walking ${p.name}...`);
}

导入类型也可以在类型别名声明中使用:

1
2
3
4
5
6
7
8
9
/**
* @typedef Pet { import("./a").Pet }
*/

/**
* @type {Pet}
*/
var myPet;
myPet.name;

如果你不知道类型,或者它有一个令人讨厌的大型类型,可以使用import类型从模块中获取值的类型:

1
2
3
4
/**
* @type {typeof import("./a").x }
*/
var x = require("./a").x;

未完待续…

继续上篇文章【TypeScript基础入门之Javascript文件类型检查(一)

对象文字是开放式的

在.ts文件中,初始化变量声明的对象文字将其类型赋予声明。不能添加未在原始文本中指定的新成员。此规则在.js文件中放宽;对象文字具有开放式类型(索引签名),允许添加和查找最初未定义的属性。例如:

1
2
var obj = { a: 1 };
obj.b = 2; // Allowed

对象文字的行为就像它们具有索引签名[x:string]:任何允许它们被视为开放映射而不是封闭对象的任何东西。

与其他特殊的JS检查行为一样,可以通过为变量指定JSDoc类型来更改此行为。例如:

1
2
3
/** @type {{a: number}} */
var obj = { a: 1 };
obj.b = 2; // Error, type {a: number} does not have property b

null,undefined和empty数组初始值设定项的类型为any或any[]

使用null或undefined初始化的任何变量,参数或属性都将具有类型any,即使打开了严格的空检查。使用[]初始化的任何变量,参数或属性都将具有类型any[],即使打开了严格的空检查。唯一的例外是具有如上所述的多个初始值设定项的属性。

1
2
3
4
5
6
7
8
9
function Foo(i = null) {
if (!i) i = 1;
var j = undefined;
j = 2;
this.l = [];
}
var foo = new Foo();
foo.l.push(foo.i);
foo.l.push("end");

功能参数默认是可选的

由于无法在ES2015之前的Javascript中指定参数的可选性,因此.js文件中的所有函数参数都被视为可选参数。允许使用参数少于声明的参数数量的调用。

重要的是要注意,调用具有太多参数的函数是错误的。

例如:

1
2
3
4
5
6
7
function bar(a, b) {
console.log(a + " " + b);
}

bar(1); // OK, second argument considered optional
bar(1, 2);
bar(1, 2, 3); // Error, too many arguments

JSDoc注释函数被排除在此规则之外。使用JSDoc可选参数语法来表示可选性。例如:

1
2
3
4
5
6
7
8
9
10
11
/**
* @param {string} [somebody] - Somebody's name.
*/
function sayHello(somebody) {
if (!somebody) {
somebody = 'John Doe';
}
console.log('Hello ' + somebody);
}

sayHello();

由arguments推断出的var-args参数声明

其主体具有对参数引用的引用的函数被隐式地认为具有var-arg参数(即(…arg: any[]) => any)。使用JSDoc var-arg语法指定参数的类型。

1
2
3
4
5
6
7
8
/** @param {...number} args */
function sum(/* numbers */) {
var total = 0
for (var i = 0; i < arguments.length; i++) {
total += arguments[i]
}
return total
}

未指定的类型参数默认为any

由于在Javascript中没有用于指定泛型类型参数的自然语法,因此未指定的类型参数默认为any。

在extends子句中:

例如,React.Component被定义为具有两个类型参数,Props和State。在.js文件中,没有合法的方法在extends子句中指定它们。默认情况下,类型参数将是any:

1
2
3
4
5
6
7
import { Component } from "react";

class MyComponent extends Component {
render() {
this.props.b; // Allowed, since this.props is of type any
}
}

使用JSDoc @augments明确指定类型。例如:

1
2
3
4
5
6
7
8
9
10
import { Component } from "react";

/**
* @augments {Component<{a: number}, State>}
*/
class MyComponent extends Component {
render() {
this.props.b; // Error: b does not exist on {a:number}
}
}

在JSDoc引用中

JSDoc中的未指定类型参数默认为any:

1
2
3
4
5
6
7
8
9
10
11
/** @type{Array} */
var x = [];

x.push(1); // OK
x.push("string"); // OK, x is of type Array<any>

/** @type{Array.<number>} */
var y = [];

y.push(1); // OK
y.push("string"); // Error, string is not assignable to number

在函数调用中

对泛型函数的调用使用参数来推断类型参数。有时这个过程无法推断任何类型,主要是因为缺乏推理源;在这些情况下,类型参数将默认为any。例如:

1
2
3
var p = new Promise((resolve, reject) => { reject() });

p; // Promise<any>;

未完待续…

TypeScript 2.3及更高版本支持使用–checkJs在.js文件中进行类型检查和报告错误。

您可以通过添加//@ts-nocheck注释来跳过检查某些文件; 相反,您可以通过在不设置–checkJs的情况下向其添加//@ts-check注释来选择仅检查几个.js文件。 您还可以通过在前一行添加//@ts-ignore来忽略特定行上的错误。 请注意,如果你有一个tsconfig.json,JS检查将遵循严格的标志,如noImplicitAny,strictNullChecks等。但是,由于JS检查相对松散,将严格的标志与它结合可能会令人惊讶。

以下是.js文件中检查与.ts文件相比如何工作的一些显着差异:

JSDoc类型用于类型信息

在.js文件中,通常可以像.ts文件一样推断类型。 同样,当无法推断类型时,可以使用JSDoc指定它们,就像在.ts文件中使用类型注释一样。 就像Typescript一样, –noImplicitAny会给你编译器无法推断类型的地方的错误。 (开放式对象文字除外;有关详细信息,请参见下文。) 装饰声明的JSDoc注释将用于设置该声明的类型。 例如:

1
2
3
4
5
/** @type {number} */
var x;

x = 0; // OK
x = false; // Error: boolean is not assignable to number

您可以在JavaScript文档中的JSDoc支持中找到受支持的JSDoc模式的完整列表。

属性是从类体中的赋值推断出来的

ES2015没有在类上声明属性的方法。 属性是动态分配的,就像对象文字一样。

在.js文件中,编译器从类主体内的属性赋值中推断属性。 属性的类型是构造函数中给出的类型,除非它没有在那里定义,或者构造函数中的类型是undefined或null。 在这种情况下,类型是这些赋值中所有右侧值的类型的并集。 始终假定构造函数中定义的属性存在,而仅在方法,getter或setter中定义的属性被视为可选。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
class C {
constructor() {
this.constructorOnly = 0
this.constructorUnknown = undefined
}
method() {
this.constructorOnly = false // error, constructorOnly is a number
this.constructorUnknown = "plunkbat" // ok, constructorUnknown is string | undefined
this.methodOnly = 'ok' // ok, but y could also be undefined
}
method2() {
this.methodOnly = true // also, ok, y's type is string | boolean | undefined
}
}

如果从未在类体中设置属性,则将它们视为未知。 如果您的类具有仅从中读取的属性,请使用JSDoc在构造函数中添加然后注释声明以指定类型。 如果稍后将初始化,您甚至不必提供值:

1
2
3
4
5
6
7
8
9
10
11
12
class C {
constructor() {
/** @type {number | undefined} */
this.prop = undefined;
/** @type {number | undefined} */
this.count;
}
}

let c = new C();
c.prop = 0; // OK
c.count = "string"; // Error: string is not assignable to number|undefined

构造函数等同于类

在ES2015之前,Javascript使用构造函数而不是类。 编译器支持此模式,并将构造函数理解为与ES2015类等效。 上述属性推理规则的工作方式完全相同。

1
2
3
4
5
6
7
8
function C() {
this.constructorOnly = 0
this.constructorUnknown = undefined
}
C.prototype.method = function() {
this.constructorOnly = false // error
this.constructorUnknown = "plunkbat" // OK, the type is string | undefined
}

支持CommonJS模块

在.js文件中,Typescript了解CommonJS模块格式。 对exports和module.exports的赋值被识别为导出声明。 同样,require函数调用被识别为模块导入。 例如:

1
2
3
4
5
6
7
// same as `import module "fs"`
const fs = require("fs");

// same as `export function readFile`
module.exports.readFile = function(f) {
return fs.readFileSync(f);
}

Javascript中的模块支持比Typescript的模块支持更具语法上的宽容。 支持大多数分配和声明组合。

类,函数和对象文字是名称空间 类是.js文件中的命名空间。 这可以用于嵌套类,例如:

1
2
3
4
class C {
}
C.D = class {
}

并且,对于ES2015之前的代码,它可以用于模拟静态方法:

1
2
3
4
5
6
function Outer() {
this.y = 2
}
Outer.Inner = function() {
this.yy = 2
}

它还可以用于创建简单的命名空间:

1
2
3
4
5
var ns = {}
ns.C = class {
}
ns.func = function() {
}
1
2
3
4
5
6
7
8
9
10
11
// IIFE
var ns = (function (n) {
return n || {};
})();
ns.CONST = 1

// defaulting to global
var assign = assign || function() {
// code goes here
}
assign.extra = 1

未完待续…

三斜杠指令是包含单个XML标记的单行注释。 注释的内容用作编译器指令。

三斜杠指令仅在其包含文件的顶部有效。 三重斜杠指令只能在单行或多行注释之前,包括其他三重斜杠指令。 如果在声明或声明之后遇到它们,则将它们视为常规单行注释,并且没有特殊含义。

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

/// <reference path ="..."/>指令是该组中最常见的。 它充当文件之间的依赖声明。

三斜杠引用指示编译器在编译过程中包含其他文件。

它们还可以作为在使用–out或–outFile时对输出进行排序的方法。 在预处理传递之后,文件以与输入相同的顺序发送到输出文件位置。

该过程从一组根文件开始; 这些是在命令行或tsconfig.json文件的”files”列表中指定的文件名。 这些根文件按照指定的顺序进行预处理。 在将文件添加到列表之前,将处理其中的所有三重斜杠引用,并包括其目标。 三重斜杠引用按照它们在文件中看到的顺序以深度优先方式解析。

如果无根则,则相对于包含文件解析三斜杠参考路径。

错误

引用不存在的文件是错误的。 如果文件具有对自身的三斜杠引用,则会出错。

使用–noResolve

如果指定了编译器标志–noResolve,则忽略三次斜杠引用; 它们既不会导致添加新文件,也不会更改所提供文件的顺序。

///

类似于/// <reference path ="..."/>指令,该指令用作依赖声明; 但是,/// <references types ="..."/>指令声明了对包的依赖性。

解析这些包名称的过程类似于在import语句中解析模块名称的过程。 考虑三重斜杠引用类型指令的简单方法是作为声明包的导入。

例如,在声明文件中包含/// <references types ="node"/>声明此文件使用在types/node/index.d.ts中声明的名称; 因此,此包需要与声明文件一起包含在编译中。

只有在手动创建d.ts文件时才使用这些指令。

对于编译期间生成的声明文件,编译器会自动为您添加/// <references types ="..."/>; 当且仅当生成的文件使用引用包中的任何声明时,才会添加生成的声明文件中的/// <reference types ="..."/>

///

该指令允许文件显式包含现有的内置lib文件。

内置的lib文件以与tsconfig.json中的”lib”编译器选项相同的方式引用(例如,使用lib=”es2015”而不是lib=”lib.es2015.d.ts”等)。

对于在内置类型上进行中继的声明文件作者,例如 建议使用DOM API或内置的JS运行时构造函数(如Symbol或Iterable,三斜杠引用lib指令)。 以前这些.d.ts文件必须添加此类型的前向/重复声明。

例如,将/// <reference lib="es2017.string"/>添加到编译中的一个文件等效于使用–lib es2017.string进行编译。

1
2
3
/// <reference lib="es2017.string" />

"foo".padStart(4);

///

该指令将文件标记为默认库。 您将在lib.d.ts及其不同变体的顶部看到此注释。

该指令指示编译器不在编译中包含默认库(即lib.d.ts)。 这里的影响类似于在命令行上传递–noLib。

另请注意,在传递–skipDefaultLibCheck时,编译器将仅跳过使用/// <reference no-default-lib ="true"/>检查文件。

///

默认情况下,AMD模块是匿名生成的。 当使用其他工具处理结果模块(例如捆绑器(例如r.js))时,这会导致问题。

amd-module指令允许将可选模块名称传递给编译器:

amdModule.ts

1
2
3
///<amd-module name="NamedModule"/>
export class C {
}

将导致将名称NamedModule分配给模块作为调用AMD定义的一部分:

amdModule.js

1
2
3
4
5
6
7
8
define("NamedModule", ["require", "exports"], function (require, exports) {
var C = (function () {
function C() {
}
return C;
})();
exports.C = C;
});

///

注意:此指令已被弃用。使用import”moduleName”;而是声明。

/// <amd-dependency path ="x"/>通知编译器需要在结果模块的require调用中注入的非TS模块依赖项。

amd-dependency指令也可以有一个可选的name属性; 这允许传递amd依赖的可选名称:

1
2
3
/// <amd-dependency path="legacy/moduleA" name="moduleA"/>
declare var moduleA:MyType
moduleA.callStuff()

生成的JS代码:

1
2
3
define(["require", "exports", "legacy/moduleA"], function (require, exports, moduleA) {
moduleA.callStuff()
});

介绍

与传统的OO层次结构一起,另一种从可重用组件构建类的流行方法是通过组合更简单的部分类来构建它们。
您可能熟悉Scala等语言的mixin或traits的概念,并且该模式在JavaScript社区中也已经普及。

Mixin示例

在下面的代码中,我们将展示如何在TypeScript中对mixin进行建模。
在代码之后,我们将分解其工作原理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}

}

// Activatable Mixin
class 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开始。
您可以看到每个人都专注于特定的活动或能力。
我们稍后将它们混合在一起,形成两种功能的新类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Disposable Mixin
class Disposable {
isDisposed: boolean;
dispose() {
this.isDisposed = true;
}

}

// Activatable Mixin
class Activatable {
isActive: boolean;
activate() {
this.isActive = true;
}
deactivate() {
this.isActive = false;
}
}

接下来,我们将创建将处理两个mixin组合的类。
让我们更详细地看一下它是如何做到的:

1
class SmartObject implements Disposable, Activatable {

您可能在上面注意到的第一件事是,我们使用工具而不是使用扩展。
这将类视为接口,并且仅使用Disposable和Activatable后面的类型而不是实现。
这意味着我们必须在课堂上提供实现。
除此之外,这正是我们想要通过使用mixins避免的。

为了满足这一要求,我们为来自mixin的成员创建了替身属性及其类型。
这使编译器满足这些成员在运行时可用。
这让我们仍然可以获得mixins的好处,尽管有一些簿记开销。

1
2
3
4
5
6
7
// Disposable
isDisposed: boolean = false;
dispose: () => void;
// Activatable
isActive: boolean = false;
activate: () => void;
deactivate: () => void;

最后,我们将mixin混合到类中,创建完整的实现。

1
applyMixins(SmartObject, [Disposable, Activatable]);

最后,我们创建了一个辅助函数,它将为我们进行混合。
这将贯穿每个mixin的属性并将它们复制到mixins的目标,并使用它们的实现填充替代属性。

1
2
3
4
5
6
7
function applyMixins(derivedCtor: any, baseCtors: any[]) {
baseCtors.forEach(baseCtor => {
Object.getOwnPropertyNames(baseCtor.prototype).forEach(name => {
derivedCtor.prototype[name] = baseCtor.prototype[name];
});
});
}

继续上篇文章[TypeScript基础入门之装饰器(二)]

访问器装饰器

Accessor Decorator在访问器声明之前声明。 访问器装饰器应用于访问器的属性描述符,可用于观察,修改或替换访问者的定义。 访问器装饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在声明类中)。

注意: TypeScript不允许为单个成员装饰get和set访问器。相反,该成员的所有装饰器必须应用于按文档顺序指定的第一个访问器。这是因为装饰器适用于属性描述符,它结合了get和set访问器,而不是单独的每个声明。

访问器装饰器的表达式将在运行时作为函数调用,具有以下三个参数:

  1. 静态成员的类的构造函数,或实例成员的类的原型。
  2. 成员的名字。
  3. 会员的财产描述。

注意: 如果脚本目标小于ES5,则属性描述符将不确定。

如果访问器装饰器返回一个值,它将用作该成员的属性描述符。

注意: 如果脚本目标小于ES5,则忽略返回值。

以下是应用于Point类成员的访问器装饰器(@configurable)的示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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装饰器:

1
2
3
4
5
function configurable(value: boolean) {
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
descriptor.configurable = value;
};
}

属性装饰器

Property Decorator在属性声明之前声明。 属性修饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在声明类中)。

属性装饰器的表达式将在运行时作为函数调用,具有以下两个参数:

  1. 静态成员的类的构造函数,或实例成员的类的原型。
  2. 成员的名字。

注意: 由于在TypeScript中如何初始化属性装饰器,因此不提供属性描述符作为属性装饰器的参数。这是因为在定义原型的成员时,当前没有机制来描述实例属性,也无法观察或修改属性的初始化程序。返回值也会被忽略。因此,属性装饰器只能用于观察已为类声明特定名称的属性。

我们可以使用此信息来记录有关属性的元数据,如以下示例所示:

1
2
3
4
5
6
7
8
9
10
11
12
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函数:

1
2
3
4
5
6
7
8
9
10
11
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库的更多信息,请参阅元数据。

参数装饰器

参数装饰器在参数声明之前声明。 参数装饰器应用于类构造函数或方法声明的函数。 参数装饰器不能用于声明文件,重载或任何其他环境上下文(例如声明类中)。

参数装饰器的表达式将在运行时作为函数调用,具有以下三个参数:

  1. 静态成员的类的构造函数,或实例成员的类的原型。
  2. 成员的名字。
  3. 函数参数列表中参数的序数索引。

注意: 参数装饰器只能用于观察已在方法上声明参数。

将忽略参数装饰器的返回值。

以下是应用于Greeter类成员参数的参数装饰器(@required)的示例:

1
2
3
4
5
6
7
8
9
10
11
12
class Greeter {
greeting: string;

constructor(message: string) {
this.greeting = message;
}

@validate
greet(@required name: string) {
return "Hello " + name + ", " + this.greeting;
}
}

然后我们可以使用以下函数声明定义@required和@validate装饰器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
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库,它为实验元数据API添加了polyfill。 该库尚未成为ECMAScript(JavaScript)标准的一部分。 但是,一旦装饰器被正式采用为ECMAScript标准的一部分,这些扩展将被提议采用。

您可以通过npm安装此库:

1
npm i reflect-metadata --save

TypeScript包含实验支持,用于为具有装饰器的声明发出某些类型的元数据。 要启用此实验性支持,必须在命令行或tsconfig.json中设置emitDecoratorMetadata编译器选

命令行:

1
tsc --target ES5 --experimentalDecorators --emitDecoratorMetadata

tsconfig.json:

1
2
3
4
5
6
7
{
"compilerOptions": {
"target": "ES5",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}

启用后,只要导入了reflect-metadata库,就会在运行时公开其他设计时类型信息。

我们可以在以下示例中看到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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.");
}
set(value);
}
}

TypeScript编译器将使用@ Reflect.metadata装饰器注入设计时类型信息。 你可以认为它相当于以下TypeScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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; }
}

注意: 装饰器元数据是一个实验性功能,可能会在将来的版本中引入重大更改。

0%