Gowhich

Durban's Blog

装饰器求值

如何应用装饰器应用于类内的各种声明的顺序:

  1. 对每个实例成员应用参数装饰器,后跟Method,Accessor或Property Decorators。
  2. 对每个静态成员应用参数装饰器,后跟Method,Accessor或Property Decorators。
  3. 参数装饰器应用于构造函数。
  4. 类装饰器适用于该类。

类装饰器

类装饰器在类声明之前声明。
类装饰器应用于类的构造函数,可用于观察,修改或替换类定义。
类装饰器不能在声明文件中使用,也不能在任何其他环境上下文中使用(例如在声明类上)。

类装饰器的表达式将在运行时作为函数调用,装饰类的构造函数作为其唯一参数。

如果类装饰器返回一个值,它将使用提供的构造函数替换类声明。

注意: 如果您选择返回新的构造函数,则必须注意维护原始原型。在运行时应用装饰器的逻辑不会为您执行此操作。

以下是应用于Greeter类的类装饰器(@sealed)的示例:

1
2
3
4
5
6
7
8
9
10
@sealed
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}
greet() {
return "Hello, " + this.greeting;
}
}

我们可以使用以下函数声明定义@sealed装饰器:

1
2
3
4
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}

当执行@sealed时,它将密封构造函数及其原型。

接下来,我们有一个如何覆盖构造函数的示例。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function classDecorator<T extends {new(...args:any[]):{}}>(constructor:T) {
return class extends constructor {
newProperty = "new property";
hello = "override";
}
}

@classDecorator
class Greeter {
property = "property";
hello: string;
constructor(m: string) {
this.hello = m;
}
}

console.log(new Greeter("world"));

方法装饰器

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

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

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

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

如果方法装饰器返回一个值,它将用作方法的属性描述符。

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

以下是应用于Greeter类上的方法的方法装饰器(@enumerable)的示例:

1
2
3
4
5
6
7
8
9
10
11
class Greeter {
greeting: string;
constructor(message: string) {
this.greeting = message;
}

@enumerable(false)
greet() {
return "Hello, " + this.greeting;
}
}

我们可以使用以下函数声明定义@enumerable装饰器:

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

这里的@enumerable(false)装饰器是一个装饰工厂。
当调用@enumerable(false)装饰器时,它会修改属性描述符的可枚举属性。

未完待续…

介绍

随着TypeScript和ES6中Classes的引入,现在存在某些场景需要额外的功能来支持注释或修改类和类成员。
装饰器提供了一种为类声明和成员添加注释和元编程语法的方法。
装饰器是JavaScript的第2阶段提案,可作为TypeScript的实验性功能使用。

注意: 装饰器是一种实验性功能,可能在将来的版本中发生变化。

要为装饰器启用实验支持,必须在命令行或tsconfig.json中启用experimentalDecorators编译器选项:

命令行:

1
tsc --target ES5 --experimentalDecorators

tsconfig.json:

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

装饰器

装饰器是一种特殊的声明,可以附加到类声明,方法,访问器,属性或参数。
装饰器使用@expression形式,其中expression必须求值为一个函数,该函数将在运行时调用有关装饰声明的信息。

例如,给定装饰器@sealed,我们可以编写sealed函数,如下所示:

1
2
3
function sealed(target) {
// do something with 'target' ...
}

注意: 您可以在下面的类装饰器中看到更详细的装饰器示例。

装饰器工厂

如果我们想自定义装饰器如何应用于声明,我们可以编写一个装饰器工厂。
装饰器工厂只是一个函数,它返回装饰器在运行时调用的表达式。

我们可以用以下方式编写装饰工厂:

1
2
3
4
5
function color(value: string) { // this is the decorator factory
return function (target) { // this is the decorator
// do something with 'target' and 'value'...
}
}

注意: 您可以在下面的方法装饰器中看到装饰器工厂的更详细示例。

装饰器组成

可以将多个装饰器应用于声明,如以下示例所示:

  1. 单行:
1
@f @g x
  1. 多行:
1
2
3
@f
@g
x

当多个装饰器应用于单个声明时,它们的评估类似于数学中的函数组合。
在该模型中,当组成函数f和g时,得到的复合(f ∘ g)(x)等于f(g(x))。

因此,在TypeScript中评估单个声明上的多个装饰器时,将执行以下步骤:

  1. 每个装饰器的表达式都是从上到下进行评估的。
  2. 然后将结果从底部到顶部称为函数。

如果我们要使用装饰器工厂,我们可以通过以下示例观察此评估顺序:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
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() {}
}

运行后输出到控制台如下:

1
2
3
4
f(): evaluated
g(): evaluated
g(): called
f(): called

未完待续…

属性类型检查

键入检查属性的第一步是确定元素属性类型。
内在元素和基于价值的元素之间略有不同。

对于内部元素,它是JSX.IntrinsicElements上的属性类型

1
2
3
4
5
6
7
8
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean }
}
}

// element attributes type for 'foo' is '{bar?: boolean}'
<foo bar />;

对于基于价值的元素,它有点复杂。
它由先前确定的元素实例类型上的属性类型确定。
使用哪个属性由JSX.ElementAttributesProperty确定。
它应该用单个属性声明。
然后使用该属性的名称。
从TypeScript 2.8开始,如果未提供JSX.ElementAttributesProperty,则将使用类元素的构造函数或SFC调用的第一个参数的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
declare namespace JSX {
interface ElementAttributesProperty {
props; // specify the property name to use
}
}

class MyComponent {
// specify the property on the element instance type
props: {
foo?: string;
}
}

// element attributes type for 'MyComponent' is '{foo?: string}'
<MyComponent foo="bar" />

元素属性类型用于键入检查JSX中的属性。
支持可选和必需的属性。

1
2
3
4
5
6
7
8
9
10
11
12
declare namespace JSX {
interface IntrinsicElements {
foo: { requiredProp: string; optionalProp?: number }
}
}

<foo requiredProp="bar" />; // ok
<foo requiredProp="bar" optionalProp={0} />; // ok
<foo />; // error, requiredProp is missing
<foo requiredProp={0} />; // error, requiredProp should be a string
<foo requiredProp="bar" unknownProp />; // error, unknownProp does not exist
<foo requiredProp="bar" some-unknown-prop />; // ok, because 'some-unknown-prop' is not a valid identifier

注意:如果属性名称不是有效的JS标识符(如data-*属性),则如果在元素属性类型中找不到它,则不会将其视为错误。

此外,JSX.IntrinsicAttributes接口可用于指定JSX框架使用的额外属性,这些属性通常不被组件的props或参数使用 - 例如React中的键。
进一步说,通用JSX.IntrinsicClassAttributes 类型也可用于为类组件(而不是SFC)指定相同类型的额外属性。
在此类型中,泛型参数对应于类实例类型。
在React中,这用于允许类型为Ref 的ref属性。
一般来说,这些接口上的所有属性都应该是可选的,除非您打算让JSX框架的用户需要在每个标记上提供一些属性。
扩展操作符也是有效的:

1
2
3
4
5
var props = { requiredProp: "bar" };
<foo {...props} />; // ok

var badProps = {};
<foo {...badProps} />; // error

子类型检查

在TypeScript 2.3中,TS引入了子类型检查。
children是元素属性类型中的特殊属性,其中子JSXExpressions被插入到属性中。
类似于TS使用JSX.ElementAttributesProperty来确定props的名称,TS使用JSX.ElementChildrenAttribute来确定这些props中的子项名称。
应使用单个属性声明JSX.ElementChildrenAttribute。

1
2
3
4
5
declare namespace JSX {
interface ElementChildrenAttribute {
children: {}; // specify children name to use
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
<div>
<h1>Hello</h1>
</div>;

<div>
<h1>Hello</h1>
World
</div>;

const CustomComp = (props) => <div>props.children</div>
<CustomComp>
<div>Hello World</div>
{"This is just a JS expression..." + 1000}
</CustomComp>

您可以像任何其他属性一样指定子类型。
这将覆盖默认类型,例如React typings(如果您使用它们)。

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
interface PropsType {
children: JSX.Element
name: string
}

class Component extends React.Component<PropsType, {}> {
render() {
return (
<h2>
{this.props.children}
</h2>
)
}
}

// OK
<Component>
<h1>Hello World</h1>
</Component>

// Error: children is of type JSX.Element not array of JSX.Element
<Component>
<h1>Hello World</h1>
<h2>Hello World</h2>
</Component>

// Error: children is of type JSX.Element not array of JSX.Element or string.
<Component>
<h1>Hello</h1>
World
</Component>

JSX结果类型

默认情况下,JSX表达式的结果键入为any。您可以通过指定JSX.Element接口来自定义类型。但是,无法从此接口检索有关JSX的元素,属性或子级的类型信息。这是一个黑盒子。

嵌入表达式

JSX允许您通过用大括号({})包围表达式来在标记之间嵌入表达式。

1
2
3
var a = <div>
{["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>

上面的代码将导致错误,因为您不能将字符串除以数字。
使用preserve选项时,输出如下所示:

1
2
3
var a = <div>
{["foo", "bar"].map(function (i) { return <span>{i / 2}</span>; })}
</div>

React整合

要将JSX与React一起使用,您应该使用React类型。
这些类型适当地定义了JSX名称空间以与React一起使用。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
/// <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" />; // ok
<MyComponent foo={0} />; // error

工厂函数

jsx:react编译器选项使用的确切工厂函数是可配置的。
可以使用jsxFactory命令行选项或内联@jsx注释编译指示来设置它以基于每个文件进行设置。
例如,如果将jsxFactory设置为createElement,则

将作为createElement(“div”)而不是React.createElement(“div”)来编译。

注释pragma版本可以像这样使用(在TypeScript 2.8中):

1
2
3
import preact = require("preact");
/* @jsx preact.h */
const x = <div />;

编译为

1
2
const preact = require("preact");
const x = preact.h("div", null);

选择的工厂也将影响JSX命名空间的查找位置(用于类型检查信息),然后再回退到全局命名空间。
如果工厂定义为React.createElement(默认值),编译器将在检查全局JSX之前检查React.JSX。
如果工厂定义为h,它将在全局JSX之前检查h.JSX。

介绍

JSX是一种可嵌入的类似XML的语法。 它旨在转换为有效的JavaScript,尽管该转换的语义是特定于实现的。 JSX在React框架中越来越受欢迎,但此后也看到了其他实现。 TypeScript支持嵌入,类型检查和直接编译JSX到JavaScript。

基本用法

要使用JSX,您必须做两件事。 1. 使用.tsx扩展名命名您的文件 2. 启用jsx选项

TypeScript附带三种JSX模式:preserve, react 和 react-native。 这些模式仅影响编译阶段 - 类型检查不受影响。 preserve模式将保持JSX作为输出的一部分,以便由另一个变换步骤(例如Babel)进一步编译。 此外,输出将具有.jsx文件扩展名。 react模式将编译React.createElement,在使用之前不需要经过JSX转换,输出将具有.js文件扩展名。 react-native模式相当于保留,因为它保留了所有JSX,但输出将具有.js文件扩展名。

Mode Input Output Output File Extension
preserve .jsx
react React.createElement(“div”) .js
react-native .js

您可以使用–jsx命令行标志或tsconfig.json文件中的相应选项指定此模式。

注意:标识符React是硬编码的,因此必须使用大写的R使React可用

as 操作符

回想一下如何写一个类型断言:

1
var foo = <foo>bar;

声明变量bar的类型为foo。 由于TypeScript还对类型断言使用尖括号,因此将其与JSX的语法结合会引入某些解析困难。 因此,TypeScript不允许.tsx文件中的尖括号类型断言。

由于上述语法不能在.tsx文件中使用,因此应使用备用类型断言运算符:as。 可以使用as运算符轻松重写该示例。

1
var foo = bar as foo;

as运算符在.ts和.tsx文件中均可用,并且行为与尖括号类型断言样式相同。

类型检查

为了理解使用JSX进行类型检查,您必须首先了解内部元素和基于值的元素之间的区别。 给定JSX表达式,expr可以引用环境固有的东西(例如DOM环境中的div或span)或者您创建的自定义组件。 这有两个重要原因:

  1. 对于React,内部元素以字符串形式发出(React.createElement(“div”)),而您创建的组件则不是(React.createElement(MyComponent))。
  2. 应该以不同的方式查找在JSX元素中传递的属性的类型。内在元素属性本质上应该是已知的,而组件可能想要指定它们自己的属性集。

TypeScript使用与React相同的约定来区分它们。内部元素始终以小写字母开头,而基于值的元素始终以大写字母开头。

内在元素 在特殊接口JSX.IntrinsicElements上查找内部元素。 默认情况下,如果未指定此接口,则会执行任何操作,并且不会对内部元素进行类型检查。 但是,如果存在此接口,则将内部元素的名称作为JSX.IntrinsicElements接口上的属性进行查找。 例如:

1
2
3
4
5
6
7
8
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}

<foo />; // ok
<bar />; // error

在上面的示例中,将正常工作,但将导致错误,因为它尚未在JSX.IntrinsicElements上指定。

注意:您还可以在JSX.IntrinsicElements上指定catch-all字符串索引器,如下所示:

1
2
3
4
5
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}

基于值的要素

基于值的元素只需通过范围内的标识符进行查找。

1
2
3
4
import MyComponent from "./myComponent";

<MyComponent />; // ok
<SomeOtherComponent />; // error

有两种方法可以定义基于值的元素:

  1. 无状态功能组件(SFC)
  2. 类组件

因为这两种类型的基于值的元素在JSX表达式中无法区分,所以首先TS尝试使用重载解析将表达式解析为无状态功能组件。 如果该过程成功,则TS完成将表达式解析为其声明。 如果该值无法解析为SFC,则TS将尝试将其解析为类组件。 如果失败,TS将报告错误。

无状态功能组件

顾名思义,该组件被定义为JavaScript函数,其第一个参数是props对象。 TS强制其返回类型必须可分配给JSX.Element。

1
2
3
4
5
6
7
8
9
10
11
12
interface FooProp {
name: string;
X: number;
Y: number;
}

declare function AnotherComponent(prop: {name: string});
function ComponentFoo(prop: FooProp) {
return <AnotherComponent name={prop.name} />;
}

const Button = (prop: {value: string}, context: { color: string }) => <button>

因为SFC只是一个JavaScript函数,所以这里也可以使用函数重载:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface ClickableProps {
children: JSX.Element[] | JSX.Element
}

interface HomeProps extends ClickableProps {
home: JSX.Element;
}

interface SideProps extends ClickableProps {
side: JSX.Element | string;
}

function MainButton(prop: HomeProps): JSX.Element;
function MainButton(prop: SideProps): JSX.Element {
...
}

类组件

可以定义类组件的类型。 但是,要这样做,最好理解两个新术语:元素类类型和元素实例类型。

给定,元素类类型是Expr的类型。 因此,在上面的示例中,如果MyComponent是ES6类,则类类型将是该类的构造函数和静态。 如果MyComponent是工厂函数,则类类型将是该函数。

一旦建立了类类型,实例类型就由类类型构造的返回类型或调用签名(无论哪个存在)的并集决定。 同样,在ES6类的情况下,实例类型将是该类的实例的类型,并且在工厂函数的情况下,它将是从函数返回的值的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
class MyComponent {
render() {}
}

// use a construct signature
var myComponent = new MyComponent();

// element class type => MyComponent
// element instance type => { render: () => void }

function MyFactoryFunction() {
return {
render: () => {
}
}
}

// use a call signature
var myComponent = MyFactoryFunction();

// element class type => FactoryFunction
// element instance type => { render: () => void }

元素实例类型很有趣,因为它必须可以赋值给JSX.ElementClass,否则会导致错误。 默认情况下,JSX.ElementClass是{},但可以对其进行扩充,以将JSX的使用仅限于那些符合正确接口的类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
declare namespace JSX {
interface ElementClass {
render: any;
}
}

class MyComponent {
render() {}
}
function MyFactoryFunction() {
return { render: () => {} }
}

<MyComponent />; // ok
<MyFactoryFunction />; // ok

class NotAValidComponent {}
function NotAValidFactoryFunction() {
return {};
}

<NotAValidComponent />; // error
<NotAValidFactoryFunction />; // error

声明合并

将命名空间与类,函数和枚举合并

命名空间足够灵活,也可以与其他类型的声明合并。
为此,命名空间声明必须遵循它将与之合并的声明。
生成的声明具有两种声明类型的属性。
TypeScript使用此功能来模拟JavaScript以及其他编程语言中的某些模式。

将命名空间与类合并

这为用户提供了一种描述内部类的方法。

1
2
3
4
5
6
7
class Album {
label: Album.AlbumLabel;
}

namespace Album {
export class AlbumLabel { }
}

合并成员的可见性规则与”合并命名空间”部分中描述的相同,因此我们必须导出合并类的AlbumLabel类才能看到它。
最终结果是在另一个类内部管理的类。
您还可以使用命名空间向现有类添加更多静态成员。

除了内部类的模式之外,您还可能熟悉创建函数的JavaScript实践,然后通过向函数添加属性来进一步扩展函数。
TypeScript使用声明合并以类型安全的方式构建这样的定义。

1
2
3
4
5
6
7
8
9
10
function buildLabel(name: string): string {
return buildLabel.prefix + name + buildLabel.suffix;
}

namespace buildLabel {
export let suffix = "";
export let prefix = "Hello, ";
}

console.log(buildLabel("Sam Smith"));

同样,名称空间可用于扩展具有静态成员的枚举:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
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中的Mixins部分。

模块扩展

虽然JavaScript模块不支持合并,但您可以通过导入然后更新它们来修补现有对象。
让我们看一下玩具Observable示例:

1
2
3
4
5
6
7
8
9
10
// observable.js
export class Observable<T> {
// ... implementation left as an exercise for the reader ...
}

// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}

这在TypeScript中也可以正常工作,但编译器不了解Observable.prototype.map。
您可以使用模块扩充来告诉编译器:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// observable.ts stays the same
// map.ts
import { Observable } from "./observable";
declare module "./observable" {
interface Observable<T> {
map<U>(f: (x: T) => U): Observable<U>;
}
}
Observable.prototype.map = function (f) {
// ... another exercise for the reader
}

// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());

模块名称的解析方式与导入/导出中的模块说明符相同。
有关更多信息,请参阅模块
然后合并扩充中的声明,就好像它们在与原始文件相同的文件中声明一样。
但是,您无法在扩充中声明新的顶级声明 - 只是现有声明的补丁。

全局扩展您还可以从模块内部向全局范围添加声明:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// observable.ts
export class Observable<T> {
// ... still no implementation ...
}

declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}

Array.prototype.toObservable = function () {
// ...
}

全局扩展与模块扩展具有相同的行为和限制。

声明合并

合并命名空间

与接口类似,同名的命名空间也将合并其成员。
由于名称空间同时创建了名称空间和值,因此我们需要了解它们是如何合并的。

要合并命名空间,每个命名空间中声明的导出接口的类型定义本身已合并,形成一个内部具有合并接口定义的命名空间。

要合并命名空间值,在每个声明站点,如果已存在具有给定名称的命名空间,则通过获取现有命名空间并将第二个命名空间的导出成员添加到第一个命名空间来进一步扩展它。

在此示例中,Animals的声明合并:

1
2
3
4
5
6
7
8
namespace Animals {
export class Zebra { }
}

namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog { }
}

相当于:

1
2
3
4
5
6
namespace Animals {
export interface Legged { numberOfLegs: number; }

export class Zebra { }
export class Dog { }
}

这种命名空间合并模型是一个有用的起点,但我们还需要了解非导出成员会发生什么。
非导出成员仅在原始(未合并)命名空间中可见。
这意味着在合并之后,来自其他声明的合并成员无法看到未导出的成员。

在这个例子中我们可以更清楚地看到这一点:

1
2
3
4
5
6
7
8
9
10
11
12
13
namespace Animal {
let haveMuscles = true;

export function animalsHaveMuscles() {
return haveMuscles;
}
}

namespace Animal {
export function doAnimalsHaveMuscles() {
return haveMuscles; // Error, because haveMuscles is not accessible here
}
}

由于未导出hasMuscles,因此只有共享相同未合并命名空间的animalsHaveMuscles函数才能看到该符号。
doAnimalsHaveMuscles函数,即使它是合并的Animal命名空间的一部分,也无法看到此未导出的成员。

未完待续…

声明合并

介绍

TypeScript中的一些独特概念描述了类型级别的JavaScript对象的形状。
TypeScript特别独特的一个例子是”声明合并”的概念。
在使用现有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

了解每个声明创建的内容将帮助您了解执行声明合并时合并的内容。

合并接口

最简单,也许是最常见的声明合并类型是接口合并。
在最基本的层面上,合并机械地将两个声明的成员连接到具有相同名称的单个接口。

1
2
3
4
5
6
7
8
9
10
interface Box {
height: number;
width: number;
}

interface Box {
scale: number;
}

let box: Box = {height: 5, width: 6, scale: 10};

接口的非功能成员应该是唯一的。
如果它们不是唯一的,则它们必须属于同一类型。
如果接口都声明了具有相同名称但具有不同类型的非函数成员,则编译器将发出错误。

对于函数成员,同名的每个函数成员都被视为描述同一函数的重载。
值得注意的是,在接口A与后面的接口A合并的情况下,第二接口将具有比第一接口更高的优先级。

也就是说,在示例中:

1
2
3
4
5
6
7
8
9
10
11
12
interface Cloner {
clone(animal: Animal): Animal;
}

interface Cloner {
clone(animal: Sheep): Sheep;
}

interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}

三个接口将合并以创建单个声明,如下所示:

1
2
3
4
5
6
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}

请注意,每个组的元素保持相同的顺序,但组本身与稍后排序的后续重载集合在一起。

此规则的一个例外是专门签名。
如果签名的参数类型是单个字符串文字类型(例如,不是字符串文字的并集),那么它将被冒泡到其合并的重载列表的顶部。

例如,以下接口将合并在一起:

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

由此产生的合并声明文件将如下:

1
2
3
4
5
6
7
interface Document {
createElement(tagName: "canvas"): HTMLCanvasElement;
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}

未完待续…

继续上文[TypeScript基础入门之模块解析(二)]

跟踪模块解析

如前所述,编译器可以在解析模块时访问当前文件夹之外的文件。
在诊断模块未解析的原因或解析为错误定义时,这可能很难。
使用–traceResolution启用编译器模块分辨率跟踪可以深入了解模块解析过程中发生的情况。

假设我们有一个使用typescript模块的示例应用程序。
app.ts有一个导入,比如import * as ts from “typescript”。

1
2
3
4
5
6
7
│   tsconfig.json
├───node_modules
│ └───typescript
│ └───lib
│ typescript.d.ts
└───src
└───app.ts

使用–traceResolution调用编译器

1
tsc --traceResolution

输出结果如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
======== Resolving module 'typescript' from 'src/app.ts'. ========
Module resolution kind is not specified, using 'NodeJs'.
Loading module 'typescript' from 'node_modules' folder.
File 'src/node_modules/typescript.ts' does not exist.
File 'src/node_modules/typescript.tsx' does not exist.
File 'src/node_modules/typescript.d.ts' does not exist.
File 'src/node_modules/typescript/package.json' does not exist.
File 'node_modules/typescript.ts' does not exist.
File 'node_modules/typescript.tsx' does not exist.
File 'node_modules/typescript.d.ts' does not exist.
Found 'package.json' at 'node_modules/typescript/package.json'.
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
File 'node_modules/typescript/lib/typescript.d.ts' exist - use it as a module resolution result.
======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========

值得关注的事情

  1. 导入的名称和位置
1
======== Resolving module 'typescript' from 'src/app.ts'. ========
  1. 编译器遵循的策略
1
Module resolution kind is not specified, using 'NodeJs'.
  1. 从npm包加载类型
1
'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
  1. 最后结果
1
======== Module name ‘typescript’ was successfully resolved to ‘node_modules/typescript/lib/typescript.d.ts’. ========

使用–noResolve

通常,编译器将在启动编译过程之前尝试解析所有模块导入。
每次成功解析导入到文件时,该文件都会添加到编译器稍后将处理的文件集中。

–noResolve编译器选项指示编译器不要将任何文件”add”到未在命令行上传递的编译中。
它仍将尝试将模块解析为文件,但如果未指定该文件,则不会包含该文件。

例如:

app.ts

1
2
import * as A from "moduleA" // OK, 'moduleA' passed on the command-line
import * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
1
tsc app.ts moduleA.ts --noResolve

使用–noResolve编译app.ts应该导致:

  1. 正确查找在命令行上传递的moduleA。
  2. 找不到未通过的moduleB时出错。

常见问题

为什么排除列表中的模块仍然被编译器拾取?

tsconfig.json将文件夹转换为”project”。如果不指定任何”exclude”或”files”条目,则包含tsconfig.json及其所有子目录的文件夹中的所有文件都包含在编译中。
如果要排除某些文件使用”exclude”,如果您希望指定所有文件而不是让编译器查找它们,请使用”files”。

那是tsconfig.json自动包含。
这并没有嵌入上面讨论的模块解析。
如果编译器将文件标识为模块导入的目标,则无论它是否在前面的步骤中被排除,它都将包含在编译中。

因此,要从编译中排除文件,您需要将其和所有具有import或/// <reference path ="..."/>指令的文件排除在外。

在node项目中,经常会有遇到需要获取访问URL地址的时候,同时也会遇到协议的问题,有时候,当我们的网站是https的时候,也希望在express中或者其他的node框架中获取到的URL地址协议也是https。
但是奇怪的是express通过req.protocol获取到的仍然是http,经过试验通过nginx的配合能够很好的解决此方案。

示例如下:

1
2
3
4
5
6
7
location / {
proxy_pass http://127.0.0.1:8080/;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_set_header X-Forwarded-Proto https;
}

希望对看到的你有一些帮助。

类似的,在其他node框架中,比如koajs中,也会遇到类似的问题,配置方式也可以参考此方式,具体是否见效,需要读者自己去实践了,果然是实践出真知,按照正常来说,这些逻辑本不应该这么复杂,但是从代理的角度的来考虑的话,既然做了代理,就需要做的完整一些,需要我们对nginx代理有更多的了解。 本次的分享主要是结合了上次TypeScript + Express的一篇文章,我在进行线上部署的时候遇到的一些小知识点。

继续上文[TypeScript基础入门之模块解析(一)]

模块解析

Base URL

使用baseUrl是使用AMD模块加载器的应用程序中的常见做法,其中模块在运行时”deployed”到单个文件夹。
这些模块的源代码可以存在于不同的目录中,但构建脚本会将它们放在一起。

设置baseUrl通知编译器在哪里可以找到模块。
假定所有具有非相对名称的模块导入都相对于baseUrl

baseUrl的值确定为:

  1. baseUrl命令行参数的值(如果给定的路径是相对的,则根据当前目录计算)
  2. ‘tsconfig.json’中baseUrl属性的值(如果给定的路径是相对的,则根据’tsconfig.json’的位置计算)

请注意,设置baseUrl不会影响相对模块导入,因为它们始终相对于导入文件进行解析。
您可以在RequireJS和SystemJS文档中找到有关baseUrl的更多文档。

路径映射(Path mapping)

有时模块不直接位于baseUrl下。
例如,对模块”jquery”的导入将在运行时转换为”node_modules/jquery/dist/jquery.slim.min.js”。
加载程序使用映射配置在运行时将模块名称映射到文件,请参阅RequireJs文档和SystemJS文档

TypeScript编译器支持使用tsconfig.json文件中的”paths”属性声明此类映射。
以下是如何为jquery指定”paths”属性的示例。

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"baseUrl": ".", // This must be specified if "paths" is.
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // This mapping is relative to "baseUrl"
}
}
}

请注意,相对于”baseUrl”解析”paths”。
当将”baseUrl”设置为”.”之外的另一个值,即tsconfig.json的目录时,必须相应地更改映射。
比如说,你在上面的例子中设置了”baseUrl”: “./src”,然后jquery应该映射到”../node_modules/jquery/dist/jquery”

使用”paths”还允许更复杂的映射,包括多个后退位置。
考虑一个项目配置,其中只有一些模块在一个位置可用,其余模块在另一个位置。
构建步骤将它们放在一个地方。
项目布局可能如下所示:

1
2
3
4
5
6
7
8
9
projectRoot
├── folder1
│ ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json

相应的tsconfig.json如下所示:

1
2
3
4
5
6
7
8
9
10
11
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"*",
"generated/*"
]
}
}
}

这告诉编译器任何与模式”*“(即所有值)匹配的模块导入,以查看两个位置:

  1. “*“表示同名不变,因此map <moduleName> => <baseUrl>/<moduleName>
  2. “generated/*“表示带有附加前缀”generated”的模块名称,因此map <moduleName> => <baseUrl>/generated/<moduleName>

遵循此逻辑,编译器将尝试解析这两个导入:

  1. import ‘folder1/file2’

    1. 模式’*‘匹配,通配符捕获整个模块名称
    2. 尝试列表中的第一个替换:’*‘ -> folder1/file2
    3. 替换结果是非相对名称 - 将其与baseUrl -> projectRoot/folder1/file2.ts结合使用。
    4. 文件已存在。完成。
  2. import ‘folder2/file3’

    1. 模式’*‘匹配,通配符捕获整个模块名称
    2. 尝试列表中的第一个替换:’*‘ -> folder2/file3
    3. 替换结果是非相对名称 - 将其与baseUrl -> projectRoot/folder2/file3.ts结合使用。
    4. 文件不存在,移动到第二个替换
    5. 第二次替换’generated/*‘ -> generated/folder2/file3
    6. 替换结果是非相对名称 - 将它与baseUrl -> projectRoot/generated/folder2/file3.ts结合使用
    7. 文件已存在。完成。

使用rootDirs的虚拟目录

有时,编译时来自多个目录的项目源都被组合在一起以生成单个输出目录。
这可以看作是一组源目录创建一个”虚拟”目录。

使用’rootDirs’,您可以通知编译器构成此”虚拟”目录的根;
因此编译器可以解析这些”虚拟”目录中的相关模块导入,就像在一个目录中合并在一起一样。

例如,考虑这个项目结构:

1
2
3
4
5
6
7
8
9
src
└── views
└── view1.ts (imports './template1')
└── view2.ts

generated
└── templates
└── views
└── template1.ts (imports './view2')

src/views中的文件是某些UI控件的用户代码。
生成/模板中的文件是由模板生成器自动生成的UI模板绑定代码,作为构建的一部分。
构建步骤会将/src/views和/generated/templates/views中的文件复制到输出中的同一目录。
在运行时,视图可以期望其模板存在于其旁边,因此应使用相对名称”./template”将其导入。

要指定与编译器的此关系,请使用”rootDirs”。
“rootDirs”指定一个根列表,其内容应在运行时合并。
因此,按照我们的示例,tsconfig.json文件应如下所示:

1
2
3
4
5
6
7
8
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}

每次编译器在其中一个rootDirs的子文件夹中看到相对模块导入时,它将尝试在rootDirs的每个条目中查找此导入。

rootDirs的灵活性不仅限于指定逻辑合并的物理源目录列表。
所提供的阵列可以包括任意数量的ad hoc,任意目录名,而不管它们是否存在。
这允许编译器以类型安全的方式捕获复杂的捆绑和运行时功能,例如条件包含和项目特定的加载器插件。

考虑一种国际化场景,其中构建工具通过插入特殊路径令牌(例如#{locale})自动生成特定于语言环境的包,作为相对模块路径的一部分,例如./#{locale}/messages。
在此假设设置中,该工具枚举支持的语言环境,将抽象的路径映射到./zh/messages,./de/messages等。

假设每个模块都导出一个字符串数组。
例如./zh/messages可能包含:

1
2
3
4
export default [
"您好吗",
"很高兴认识你"
];

通过利用rootDirs,我们可以通知编译器这个映射,从而允许它安全地解析./#{locale}/messages,即使该目录永远不存在。
例如,使用以下tsconfig.json配置:

1
2
3
4
5
6
7
8
9
{
"compilerOptions": {
"rootDirs": [
"src/zh",
"src/de",
"src/#{locale}"
]
}
}

编译器现在将解析来自’./#{locale}/messages’的导入消息,以便从工具中导入来自’./zh/messages’的消息,允许以区域设置无关的方式进行开发,而不会影响设计时支持。

未完待续…

0%