Contents added.

This commit is contained in:
gnu4cn 2019-03-27 11:20:25 +08:00
parent be58d69249
commit 8f7b0a4bd1
29 changed files with 4283 additions and 49 deletions

276
07_enums.md Normal file
View File

@ -0,0 +1,276 @@
# 枚举Enums
Enum [en^m]是源自Enumerate, 意思是一一列举出来。
枚举特性令到定义一个命名常量的集合可行。使用枚举可使得意图表达或创建差异案例更为容易Using enums can make it easier to document intent, or create a set of distinct cases。TypeScript同时支持基于数字与字符串这两种枚举。
## 数字的枚举Numeric enums
这里将首先以数字枚举开始,如有着其它语言的经验,那么这种枚举可能更为熟悉。使用`enum`关键字,就可以定义出一个枚举。
```typescript
enum Direction {
Up = 1,
Down,
Left,
Right,
}
```
上面的示例有着一个数字的枚举,其中`Up`以`1`进行了初始化。其后的所有成员,都被从那个点自动增加。也就是说,`Direction.Up`的值为`1``Down`为`2``Left`为`3``Right`为`4`。
如有需要,亦可将初始值完全留空:
```typescript
enum Direction {
Up,
Down,
Left,
Right,
}
```
此时,`Up`的值将为`0``Down`将为`1`,等等。对于那些不会考虑成员值本身的案例,这种自动增加的行为是有用的,不过无需担心在同一枚举中各个值与其它值是各异的。
使用枚举很简单只要以枚举本身属性的方式并使用枚举的名称来声明类型来访问其任何成员即可Using an enum is simple: just access any member as a property off of the enum itself, and declare types using the name of the enum
```typescript
enum Response {
No = 0,
Yes,
}
function respond (recipient: string, message: Response): void {
// ...
}
respond ("Princess Caroline", Response.Yes);
```
数字枚举可混合计算的与常量成员见后。简单的说没有初始值的枚举成员要么需放在第一个或必须在那些以数值常量或其它常量枚举成员初始化过的数字枚举成员之后Numberic enums can be mixed in computed and constant members(see below). The short story is, enums without initializers either need to be first, or have to come after numberic enums initialized with numberic constants or other constant enum members。也就是说下面这种方式是不允许的
```typescript
enum E {
A = getSomeValue (),
B, // Enum member must have initializer. (1061)
}
```
## 字符串枚举String enums
字符串枚举的概念相同但有一些细微的运行时上的不同runtime differences后面会有说明。在字符串枚举中每个成员都必须使用字符串字面值或其它字符串枚举成员加以初始化。
```typescript
enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
```
虽然字符串枚举不具有自动增加行为,它们却仍然受益于其良好的“连续性”。换句话说,加入正在对程序进行调试,而不得不读取某个数字枚举的运行时值,该值通常是不透明的 -- 该值并不能提供到任何其本身有用的意义尽管反向映射通常有所帮助但字符串枚举却允许在代码运行时独立于枚举成员本身赋予有意义且可读的值While string enums don't have auto-incrementing behavior, string enums have the benefit that they "serialize" well. In other words, if you are debugging and had to read the runtime value of a numeric enum, the value is ofter opaque - it doesn't convey any useful meaning on its own(though reverse mapping can often help), string enums allow you to give a meaningful and readable value when your code runs, independent of the name of the enum member itself
## 异质枚举Heterogeneous enums
技术上枚举是可以混合字符串与数字成员的,但这么做似乎没有什么理由:
```typescript
enum BooleanLikeHeterogeneousEnum {
No = 0,
Yes = "YES",
}
```
除非要以某种明智的方式来利用JavaScript的运行时行为否则建议不要这样做Unless you're really trying to take advantage of JavaScript's runtime behavior in a clever way, it's advised that you don't do this
## 计算的与常量成员Computed and constant members
枚举的每个成员,都有着一个与其关联的值,该值可以是 *常量或计算值(constant or computed)*。在以下情况下,枚举成员将被看着是常量:
- 其作为枚举中的第一个成员且没有初始值,这种情况下其就被赋予值`0`
```typescript
// E.X 是常量
enum E { X }
```
- 没有初始值,且前一个枚举成员是一个 *数字* 常量。这种情况下当前枚举成员的值将是其前一个枚举成员加一。
```typescript
// `E1`与`E2`中的所有枚举成员都是常量。
enum E1 { X, Y, Z }
enum E2 { A = 1, B, C }
```
+ 以常量枚举表达式a constant enum expression初始化的成员。常量枚举表达式是TypeScript表达式的一个子集在运行时可被完整执行。在满足以下条件是表达式就是常量枚举表达式
1. 字面的枚举表达式(基本的字符串表达式或数字表达式, a literal enum expression(basically a string literal or a numeric literal)
2. 对先前定义的常量枚举成员(可以来自不同枚举)的引用 a reference to previously defined constant enum member(which can originate from a different enum)
3. 一个用括号包围的常量枚举表达式a parentthesized constant enum expression
4. 运用到常量枚举表达式的`+`、`-`及`~`三个一元运算符之一one of the `+`, `-`, `~` unary operators applied to constant enum expression
5. 与将常量枚举表达式作为操作数一起的`+`、`-`、`*`、`/`、`%`、`>>`、`<<`、`>>>`、`&`、`|`、`^`等二元运算符
对于结果为`NaN`Not a Number, 非数值)或`Infinity`无穷将作为编译时错误加以对待It is compile time error for constant enum expressions to be evaluated to `NaN` or `Infinity`)。
那么其它所有情况下枚举成员都将被看作是计算的In all other cases enum member is considered computed
```typescript
enum FileAccess {
// 常量成员
None,
Read = 1 << 1,
Write = 1 << 2,
ReadWrite = Read | Write,
// 计算的成员
G = "123".length,
}
```
## 联合枚举与枚举成员类型Union enums and enum member types
存在这么一个非计算的常量枚举成员的特殊子集: **字面的枚举成员**。字面枚举成员是不带有初始值的或有着被初始化为以下值的常量枚举成员There is a special subset of constant enum members that aren't calculated: literal enum members. **A literal enum member** is a constant enum member with no initialized value, or with values that are initialized to:
- 任意字符串字面值(比如`"foo"`、`"bar"`、`"baz"`
- 任意数字的字面值(比如`1`、`100`
- 应用到任意数字字面值的一元减号运算符(比如`-1`、`-100`
在某个枚举中所有成员都有着字面枚举值时,某些特别的语法就会生效。
第一就是枚举成员还成为了类型!比如,这里可以说某些成员 *只* 能具有某个枚举成员的值The first is that enum members also become types as well! For example, we can say that certain members can *only* have the value of an enum member:
```typescript
enum ShapeKind {
Circle,
Square,
}
interface Circle {
kind: ShapeKind.Circle;
radius: number;
}
interface Square {
kind: ShapeKind.Square;
sideLength: number;
}
let c: Circle = {
kind: ShapeKind.Square,
// Type '{ kind: ShapeKind.Square; radius: number; }' is not assignable to type 'Circle'.
// Types of property 'kind' are incompatible.
// Type 'ShapeKind.Square' is not assignable to type 'ShapeKind.Circle'. (2322)
radius: 100,
}
```
另一改变,就是枚举类型本身,有效地成为各枚举成员的 *联合* 。虽然到这里还没有讨论到 **联合类型** **union types**只需知道有了联合枚举TypeScript的类型系统就能够利用其对存在于枚举本身中的那些确切值的知悉这一事实。而正由于这一点TypeScript就能捕捉到那些可能进行不正确地值比较等愚蠢程序错误The other change is that enum types themselves effectively become a *union* of each enum member. While we havn't discussed **union types** yet, all that you need to know is that with union enums, the type system is able to leverage the fact that it knows the exact set of values that exist in the enum itself. Because of that, TypeScript can catch silly bugs where we might be comparing values incorrectly。比如
```typescript
enum E {
Foo,
Bar,
}
function f (x: E) {
if ( x !== E.Foo || x !== E.Bar ) {
// ~~~~~~~~~~
// Operator '!==' cannot be applied to types 'E.Foo' and 'E.Bar'. (2365)
}
}
```
在该示例中,首先检查了`x`是否不是`E.Foo`。如此检查成功,那么这里的`||`将短路,同时`if`的语句体将得到运行。但是若那个检查不成功,那么`x`就只能是`E.Foo`,因此再来判断其是否等于`E.Bar`就没有意义了In that example, we first checked whether `x` was *not* `E.Foo`. If that check succeeds, then our `||` will *short-circuit*, and the body of the `if` will get run. However, if the check didn't succed, then `x` can *only* be `E.Foo`, so it doesn't make sense to see whether it's equal to `E.Bar`)。
## 运行时的枚举Enums at runtime
运行时存在的枚举,都是真实的对象。比如,下面的这个枚举:
```typescript
enum E {
X, Y, Z
}
```
就能被确切地传递给函数:
```typescript
function f(obj: { X: number }) {
return obj.X;
}
f(E);
```
## 反向映射Reverse mappings
除了创建出一个带有属性名称成员的对象之外,数字枚举成员,还可以得到一个枚举值到枚举名称的 *反向映射* In addition to creating an object with property names for members, numeric enums members also get a *reverse mapping* from enum values to enum names。比如在下面的示例中
```typescript
enum Enum {
A
}
let a = Enum.A;
let nameOfA = Enum[a]; // "A"
```
TypeScript 会将此编译到类似下面的JavaScript代码
```javascript
var Enum;
(function (Enum) {
Enum[Enum["A"] = 0] = "A";
})( Enum || (Enum = {}) );
var a = Enum.A;
var nameOfA = Enum[a]; // "A"
```
在生成的代码中,枚举就被编译成一个同时存储了正向(`name` -> `value`)与逆向(`value` -> `name`映射的对象中。对其它枚举成员的引用总是作为属性访问而被省略且绝不会被内联In this generated code, an enum is compiled into an object that stores both forward (`name` -> `value`) and reverse (`value` -> `name`) mappings. References to other enum members are always emitted as property accesses and never inlined
请记住字符串的枚举成员并不会得到一个生成的反向映射Keep in mind that string enum members *do not* get a reverse mapping generated at all
## 常量枚举(`const` enums
大多数情况下,枚举都是一种相当有效的方案。不过某些时候需求更为紧致。为避免付出额外生成的代码与在访问枚举值时多余的间接性这两个代价,就可以使用常量枚举。所谓常量枚举,就是在枚举上使用`const`这个修饰器所定义的枚举In most cases, enums are a perfectly valid solution. However sometimes requirements are tighter. To avoid paying the cost of extra generated code and additional indirection when accessing enum values, it's possible to use `const` enums. Const enums are defined using the `const` modifier on our enums
```typescript
const enum Enum {
A = 1,
B = A * 2
}
```
常量枚举只能使用常量枚举表达式而与常规枚举不一样它们在编译期间就被完全移除了。在使用到常量枚举的地方其成员完全是内联的。这可能是因为常量枚举不能拥有计算的成员的关系Const enums can only use constant enum expressions and unlike regular enums they are completely removed during compilation. Const enum members are inlined at use sites. This is possible since const enums cannot have computed members
```typescript
const enum Directions {
Up,
Down,
Left,
Right
}
let directions = [Directions.Up, Directions.Down, Directions.Left, Directions.Right];
```
这段代码所对应的编译生成的JavaScript代码将成为
```javascript
var directions = [0 /* Up */, 1 /* Down */, 2 /* Left */, 3 /* Right */];
```
## 环境枚举Ambient enums
环境枚举用于描述已存在枚举类型的形状Ambient enums are used to describe the shape of already existing enum types
```typescript
declare enum Enum {
A = 1,
B,
C = 2
}
```
环境枚举与非环境枚举的一个重要的不同,就是在常规枚举中,不带有初始器的成员,在其前导枚举成员被认为是常量时,将被看作是常量。而与此相比,不带有初始器的环境(且非常量)枚举成员, *始终* 被认为是计算的成员One important difference between ambient and non-ambient enums is that, in regular enums, members that don't have an initializer will be considered constant if its preceding enum member is considered constant. In contrast, an ambient(and non-const) enum member that does not have initializer is *always* considered computed

75
08_type_inference.md Normal file
View File

@ -0,0 +1,75 @@
# 类型推导
**Type Inference**
## 简介
本章节将涵盖TypeScript中的类型推导。也就是说这里将讨论类型在何处及如何被推导。
## 类型推导基础
在TypeScript中有好几个地方都使用到类型推导来处理那些没有显式类型注解explicit type annotation用于提供类型的信息。比如在下面的代码中
```typescript
let x = 3;
```
变量`i`的类型,就被推导为`number`。这种推导是在对变量及成员进行初始化、参数默认值的设置setting parameter default values以及确定函数返回值类型等期间发生的。
大多数情况下,类型推导都是直截了当的。在下面部分中,将对类型是如何进行推导的细微之处,进行探讨。
## 最优通用类型Best common type
当类型推导是从几个表达式生成的时,这些表达式的类型,就被用作计算出一个“最优通用类型”。比如:
```typescript
let x = [0, 1, null];
```
为推导出上面示例中`x`的类型,就必须考虑各个数组元素的类型。这里给到的该数组类型有两个选择:`number`与`null`。 **最优通用类型算法** 就对各个候选类型加以考虑,并选取那个兼容所有其它候选项的类型( **the best common type algorithm** considers each candidate type, and picks the type that is compatible with all the other candidates
因为必须要从所提供的候选类型选出最优通用类型,那么就有着某些候选类型共享一个通用结构,却并存在一个作为所有候选类型超集的类型的情况。比如:
```typescript
let zoo = [new Rhino(), new Elephant(), new Snake()];
```
理想情况下,可能希望将`zoo`推导为一个`Animal[]`,但因为该数组中没有对象是严格的`Animal`类型,所以无法做出有关该数组元素类型的推导。为了纠正这一点,就要在没有一种类型是其它候选类型的超类型时,提供显式地提供一个类型:
```typescript
let zoo: Animal[] = [new Rhino(), new Elephant(), new Snake()];
```
而在找不到最佳通用类型时推导结果就是联合数组类型the union array type`(Rhino | Elephant | Snake)[]`。
## 上下文的类型Contextual Type
在TypeScript中类型推导在某些情况下还以“其它方向”起作用Type inference also works in "the other direction" in some cases in TypeScript。这就是所谓的“上下文的赋予类型contextual typing”。上下文类型赋予是在某个表达式的类型由其所处位置所决定时发生的。比如
```typescript
window.onmousedown = function (mouseEvent) {
console.log(mouseEvent.button); //<- Error
};
```
为了从上面的代码中检查出错误TypeScript的类型检查器使用了`window.onmousedown`函数的类型类推导该赋值语句右侧的函数表达式的类型For the code above to give the type error, the TypeScript type checker used the type of the `window.onmousedown` function to infer the type of the function expression on the right hand side of the assignment。在其这样做的时候就能够推导出`mouseEvent`参数的类型。而假如该函数表达式并不是在一个上下文类型赋予位置not in a contextually typed position那么参数`mouseEvent`将有着类型`any`,从而不会出现任何错误。
而如果上下文类型赋予的表达式the contextually typed expression包含了显式的类型信息那么上下文类型将被忽略。也就是像下面这样写上面的示例
```typescript
window.onmousedown = function (mouseEvent: any) {
console.log(mouseEvent.button); // <- Now, no error is given
};
```
参数上带有显式类型注记的函数表达式,将覆盖上下文类型。而一旦这样做,就不会报出错误了,因为应用没有上下文类型特性。
在很多情况下上下文类型赋予都得以应用。常见的包括函数调用的参数、赋值语句的右侧、类型断言、对象的成员与数组字面值以及返回语句等Contextual typing applies in many cases. Common cases include arguments to function calls, right hand sides of assignments, type assertions, members of object and array literals, and return statements。在最佳通用类型中上下文类型也扮演了一种候选类型。比如
```typescript
function createZoo(): Animal[] {
return [new Rhino(), new Elephant(), new Snake()];
}
```
在此示例中,最佳通用类型有着四种候选类型的集合:`Animal`、`Rhino`、`Elephant`以及`Snake`。其中`Animal`可能会被最佳通用类型算法选中。

272
09_type_compatibility.md Normal file
View File

@ -0,0 +1,272 @@
# 类型兼容性
**Type Compatibility**
## 简介
TypeScript中的类型兼容性是基于结构化子类型赋予的。结构化的类型赋予是一种仅依靠类型的成员而将这些类型联系起来的方式。这一点与名义上的类型赋予有所不同Type compatibility in TypeScript is based on structural subtyping. Structural typing is a way of relating types based solely on their members. This is in contrast with nominal typing。请考虑以下代码
```typescript
interface Named {
name: string;
}
class Person {
name: string;
}
let p: Named;
// 没有问题,因为这里的结构化类型赋予
p = new Person();
```
在诸如C#或Java这样的 **名义类型语言** 中,等效代码将报出错误,因为类`Person`并未显式地将其描述为是`Named`接口的一个 **实现器** In **nominally-typed languages** like C# or Java, the equivalent code would be an error because the `Person` class does not explicity describe itself as being an **an implementor** of the `Named` interface
TypeScript的结构化类型系统是基于JavaScript代码的一般编写方式而设计的。因为JavaScript广泛用到诸如函数表达式及对象字面值这样的匿名对象因此使用结构化类型系统而非名义类型系统对于表示JavaScript的那些库中所发现的关系种类就更加自然一些TypeScript's structural type system was designed based on how JavaScript code is typically written. Because JavaScript widely uses anonymous objects like function expressions and object literals, it's much more natural to represent the kinds of relationships found in JavaScript libraries with a structural type system instead of a nominal one
### 关于可靠性/健全性的说明A Note on Soundness
TypeScript的类型系统令到一些在编译时无法知晓的操作是安全的。当某个类型系统具备了此种属性时就说其不是“健全的”。至于TypeScript允许在哪里存在不健全行为则是被仔细考虑过的贯穿本文这里将对这些特性于何处发生以及它们背后的动机场景加以解释TypeScript's type system allows certain operations that can't be known at compile-time to be safe. When a type system has this property, it is said to not be "sound". The places where TypeScript allows unsound behavior were carefully considered, and throughout this document we'll explain where these happen and the motivating scenarios behind them
## 开始Starting out
TypeScript的结构化类型系统的基本规则就是在`y`具备与`x`相同成员时,`x`就兼容`y`。比如:
```typescript
interface Named {
name: string;
}
let x: Named;
// y 所引用的类型是 { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
x = y;
```
编译器要检查这里的`y`是否可以被赋值给`x`,就会对`x`的各个属性进行检查,以在`y`中找到相应的兼容属性。在本例中,`y`必须具有一个名为`name`的字符串成员。而它确实有这样的一个成员,因此该赋值是允许的。
```typescript
interface Named {
name: string;
age: number;
}
let x: Named;
// y 所引用的类型是 { name: string; location: string; }
let y = { name: "Alice", location: "Seattle" };
// TSError: Unable to compile TypeScript
// src/main.ts (12,1): Type '{ name: string; location: string; }' is not assignable to type 'Named'.
// Property 'age' is missing in type '{ name: string; location: string; }'. (2322)
x = y;
```
在对函数调用参数进行检查时也使用到通用的赋值规则The same rule for assignment is used when checking function call arguments
```typescript
function greet (n: Named) {
alert ("Hello, " + n.name);
}
greet(y); // 没有问题
```
注意这里的`y`有着一个额外的`location`属性,但这并不会造成错误。在对兼容性进行检查时,仅会考虑目标类型(这里也就是`Named`)的那些成员。
该比较过程是递归进行的对每个成员及子成员进行遍历This comparison process proceeds recursively, exploring the type of each member and sub-member
## 两个函数的比较Comparing two functions
可以看出对原生类型与对象类型的比较是相对直接的而何种函数应被看着是兼容的这个问题就牵扯到更多方面了While comparing primitive types and object types is relatively straightforward, the question of what kinds of functions should be considered is a bit more involved。下面就以两个仅在参数清单上不同的函数的基本示例开始
```typescript
let x = (a: number) => 0;
let y = (b: number, s: string) => 0;
y = x; // 没有问题
// TSError: Unable to compile TypeScript
// src/main.ts (9,1): Type '(b: number, s: string) => number' is not assignable to type '(a: number) => number'. (2322)
x = y; // 错误
```
为检查`x`是否可被赋值给`y`,首先要看看参数清单。`x`中的每个参数,在`y`中都必须有一个类型兼容的参数与其对应。注意参数名称是不考虑的,考虑的仅是它们的类型。在本示例中,函数`x`的每个参数,在`y`中都有一个兼容的参数与其对应,因此该赋值是允许的。
第二个赋值是错误的赋值,因为`y`有着必要的第二个参数,`x`并没有,因此该赋值是不允许的。
对于示例中`y = x`之所以允许“丢弃”参数的原因在JavaScript中此种忽略额外函数参数的赋值实际上是相当常见的。比如`Array#forEach`方法就提供了3个参数给回调函数数组元素、数组元素的索引以及所位处的数组。不过给其一个仅使用首个参数的回调函数仍然是很有用的
```typescript
let items = [1, 2, 3];
// Don't force these extra parameters
items.forEach((item, index, array) => console.log(item));
// 这样也是可以的
items.forEach(item => console.log(item));
```
现在来看看返回值类型是如何加以对待的,下面使用两个仅在放回值类型上有所区别的函数:
```typescript
let x = () => ({name: "Alice"});
let y = () => ({name: "Alice", location: "Seattle"});
x = y; // 没有问题
// TSError: Unable to compile TypeScript
// src/main.ts (6,1): Type '() => { name: string; }' is not assignable to type '() => { name: string; location: string; }'.
// Type '{ name: string; }' is not assignable to type '{ name: string; location: string; }'.
// Property 'location' is missing in type '{ name: string; }'. (2322)
y = x; // 错误,因为`x`缺少一个location属性
```
类型系统强制要求 **源函数** 的返回值类型,是 **目标函数** 返回值类型的一个子集The type system enforces that the source function's return type be a subtype of the target type's return type
### 函数参数的双向协变Funtion Parameter Bi-variance
在对函数参数的类型进行比较时加入源参数可被赋值给目标参数或目标参数可赋值给源参数那么函数间的赋值将成功。这是不完备的因为某个调用器可能以被给予一个取更为具体类型的函数却以不那么具体类型来触发该函数而结束。在实践中此类错误很少见同时此特性带来了很多常见的JavaScript模式When comparing the types of function parameters, assignment succeeds if either the source parameter is assignable to the target parameter, or vice versa. This is unsound because a caller might end up being given a function that takes a more specialized type, but invokes the funtion with a less specialized type. In practice, this sort of error is rare, and allowing this enables many common JavaScript patterns。下面是一个简要的示例
```typescript
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) {
//...
}
//不完备,却是有用且常见的做法
listenEvent(EventType.Mouse, (e.MouseEvent) => console.log(e.x + "," + e.y));
// 具备完备性的不可取做法
listenEvent(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 erro). Type safety enforced for wholly incompatible types
listenEvent(EventType.Mouse, (e: number) => console.log(e));
```
### 可选参数与其余参数Optional Parameters and Rest Parameters
在出于兼容性而对函数加以比较时可选参数与必需参数是通用的。源类型的额外可选参数并不是一个错误同时目标类型的、在源类型中没有对应参数的可选参数也不是一个错误When comparing functions for compatibility, optional and required parameters are interchangeable. Extra optional parameters of the source type are not an error, and optional parameters of the target type without corresponding parameters in the source type are not an error
在某个函数具有其余参数时其余参数就被当成是有无限个可选参数加以对待When a function has a rest parameter, it is treated as if it were an infinite series of optional parameters
这一点从类型系统角度看是不完备的,但因为对于大多数函数来数,在那个位置传递`undefined`都是等效的因此从运行时角度可选参数这一概念通常并不是精心构思的This is unsound from a type system perspective, but from a runtime point of view the idea of an optional parameter is generally not well-enforced since passing `undefined` in that position is equivalent for most functions
下面的示例就是某个取一个回调函数并以可预测对于程序员却未知对于类型系统数量的参数来触发该回调函数的函数的常见模式The motivating example is the common pattern of a function that takes a callback and invokes it with some predictable(to the programmer) but unknown(to the type system) number of arguments:
```typescript
function invokeLater(args: any[], callback: (...args: any[]) => void) {
/* 以 args 来触发回调函数 */
}
// 不完备 -- invokeLater "可能" 提供任意数量的参数
invokeLater([1, 2], (x, y) => console.log(x + ", " + y));
// 混乱x与y实际上是必需的且无法发现Confusing ( x and y are actually required ) and undiscoverable
invokeLater([1, 2], (x?, y?) => console.log(x + ", " + y));
```
### 带过载的函数Functions with overloads
当函数有着过载时那么源类型中的每个过载在目标类型上都必须有一个兼容的签名与其匹配。这样才能确保目标函数可与源函数所在的同样场合进行调用When a function has overloads, each overload in the source type must be matched by a compatible signature on the target type. This ensures that the target function can be called in all the same situation as the source function
## 枚举的兼容性
枚举与数字兼容,同时数字也与枚举兼容。不同枚举类型的枚举值,被看着是兼容的。比如:
```typescript
enum Status { Ready, Waiting };
enum Color { Red, Blue, Green };
let status = Status.Ready;
status = Color.Green; // 没毛病
```
## 类的兼容性Classes
类的兼容性与对象字面值及接口类似但有一个例外类同时有着静态与实例类型Classes have both a static and an instance type。在对某个类类型的两个对象进行比较时仅比较实例的成员。静态成员与构造器并不影响兼容性。
```typescript
class Animal {
feet: number;
constructor (name: string, numFeet: number) {}
}
class Size {
feet: number;
constructor (numFeet: number) {}
}
let a: Animal;
let s: Size;
a = s; //OK
s = a; //OK
```
### 类中的私有与受保护成员
类中的私有与受保护成员,对类的兼容性有影响。在对类的某个实例进行兼容性检查时,如目标类型包含了一个私有成员,那么源类型也必须要有一个从同样类继承的私有成员。与此类似,同样的规则也适用与有着受保护成员的实例。这就令到类可被兼容的赋值给其超类,但却 **不能** 兼容的赋值给那些来自不同继承层次、除此之外有着同样外形的类This allows a class to be assignment compatible with its super class, but *not* with classes from a different inheritance hierarchy which otherwise have the same shape
## 泛型Generics
因为TypeScript是一个结构化的类型系统a structural type system所以类型参数在作为某成员类型一部分而被消费是其仅影响最终类型。比如
```typescript
interface Empty<T> {}
let x: Empty<number>;
let y: Empty<string>;
x = y; //没有问题y 与 x 的解构匹配
```
在上面的代码中,`x`与`y`是兼容的,因为它们的解构没有以各异的方式来使用那个类型参数。如通过加入一个成员到`Empty<T>`中,而对此示例进行修改,就可以反映出这一点:
```typescript
interface NotEmpty<T> {
data: T;
}
let x: NotEmpty<number>;
let y: NotEmpty<string>;
x = y; //错误x 与 y 不兼容
```
这种情况下有着上面这种指定类型参数的泛型与一个非通用类型的表现一致In this way, a generic type that has its type arguments specified acts just like a non-generic type
对于没有指定类型参数的泛型,兼容性的检查,是通过在所有未指定类型参数的地方指定`any`进行的。随后对最终类型进行兼容性检查就跟非通用类型一样For generic types that do not have their type arguments specified, compatibility is checked by specifying `any` in place of all unspecified type arguments. Then resulting types are then checked for compatibility, just as in the non-generic case
比如,
```typescript
let identity = function<T>(x: T): T {
//...
}
let reverse = function<U>(y: U): U {
//...
}
identity = reverse; //没有问题,因为(x: any)=>any 与(y: any)=>any是匹配的
```
## 高级话题Advanced Topics
### 子类型与赋值语句Subtype vs Assignment
到目前为止都使用的是“兼容性”一词但这个说法在语言规格中并没有对其进行定义。在TypeScript中兼容有两种子类型与赋值。它们的不同仅在于赋值以允许赋值给与从`any`以及赋值给及从有着对应的数字值的枚举这两个规则对子类型进行了拓展In TypeScript, there are two kinds of compatibility: subtype and assignment. These differ only in that assignment extends subtype compatibility with rules to allow assignment to and from `any` and to and from enum with corresponding numeric values
根据不同情况,语言中的不同地方会使用这两种兼容性机制之一。实际来看,就算有着`implements`及`extends`关键字类型兼容性仍按赋值兼容性看待Different places in the language use one of the two compatibility mechanisms, depending on the situation. For practical purposes, type compatibility is dicated by assignment compatibility even in the cases of the `implements` and `extends` clauses。更多信息请查阅TypeScript规格。

872
10_advanced_types.md Normal file
View File

@ -0,0 +1,872 @@
#复杂类型
**Advanced Types**
##交集类型Intersection Types
交集类型将多个类型结合为一个。该特性令到将既有类型加在一起,从而得到一个有着所有所需特性的单个类型。比如,`Person & Serializable & Loggable`就是一个`Person` *与* `Serializable` *与* `Loggable`。那就意味着此类型的对象,将有着所有三个类型的所有成员。
多数情况下交集类型都用在混入及其它一些无法放入到经典的面向对象模子中的一些概念。JavaScript中可是有很多这种概念You will mostly see intersection types used for mixins and other concepts that don't fit in the classic object-oriented mold. (There are a lot of these in JavaScript!))下面是一个演示如何创建混入的示例:
```typescript
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();
```
## 联合类型Union Types
联合类型与交集类型密切相关,但二者的用法却截然不同。少数情况下,将遇到某些需要一个可能是`number`,也可能是`string`参数的库。请看下面这个函数的实例:
```typescript
/**
* 取得一个字符串并将`padding`添加到其左侧。
* 如果`padding`是一个字符串,那么`paddin`就被追加到左侧。
* 如果`padding`是一个数字,那么该数目个的空格就被追加到左侧。
*/
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);
```
`padLeft`函数的问题在于,它的`padding`参数所赋予的类型是`any`。那就意味着可以一个既不是`number`也不是`string`的参数对其进行调用TypeScript也不会检查到问题。
```typescript
let indentedString = padLeft("Hello world", true); // 在编译时可通过,但运行时失败。
```
在传统的面向对象代码中,可能会通过创建出类型的层次,来对这两种类型进行抽象。虽然这样做相对比较显式,但其也有些矫枉过正了。上面那个最初版本的`padLeft`有一个好的方面就是可仅传入原生类型的参数One of the nice things about the original version of `padLeft` was that we were able to just pass in primitives。那意味着其用法是较为简洁的。而如仅尝试使用一个已在其它地方存在的函数新方法也没有用。
对于`padding`参数,可使用 *联合* 类型类来代替`any`:
```typescript
/**
* 取得一个字符串并将`padding`添加到其左侧。
* 如果`padding`是一个字符串,那么`paddin`就被追加到左侧。
* 如果`padding`是一个数字,那么该数目个的空格就被追加到左侧。
*/
function padLeft (value: string, padding: string | number) {
// ...
}
let indentedString = padLeft("Hello world", true); // 这时在编译时就会报错了。
```
联合类型将某个值描述为可以是多个类型的某一种。使用竖杠`|`来将各个类型分开,那么`number | string | boolean`就是说某个值的类型,可以是一个`number`、`string`或者`boolean`。
加入有着一个带有联合类型的值那么就只能访问那些在联合中所有类型都具备的成员If we have a value that has a union type, we can only access members that are common to all types in the union
```typescript
interface Bird {
fly();
layEggs();
}
interface Fish {
swim();
layEggs();
}
function getSmallPet(): Fish | Bird {
// ...
}
let pet = getSmallPet();
pet.layEggs(); // 没有问题
pet.swim(); // 错误
```
这里联合类型就有些摸不着头脑了,不过只需要一些直觉,就可以习惯它。加入某个值有着类型`A | B`,那就唯一能 **明确** 的是,它有着`A` **与** `B` 都有的成员。在本示例中,`Bird`有一个名为`fly`的成员。这里无法确定某个类型为`Bird | Fish`的变量具有`fly`的方法。如果运行时该变量实际上是`Fish`,那么调用`pet.fly()`就将失败。
## 类型保护与区分类型Type Guards and Differentiating Types
当某些值可能在它们所承载的类型上出现重叠时,联合类型对于这些情况下的建模是有用的。那么当需要明确知道是否有着一个`Fish`时会发生什么呢JavaScript中区分两个可能的值的习惯做法就是对是否存在某个成员进行检查。如上面所提到的只能访问到那些保证位于联合类型的所有构成类型中成员Union types are useful for modeling situations when values can overlap in the types they can take on. What happens when we need to know specifically whether we have a `Fish`? A common idiom in JavaScript to differentiate between two possible values is to check for the presence of a member. As we mentioned, you can only access members that are guaranteed to be in all the constituents of a union type
```typescript
let pet = getSmallPet();
// 这些属性访问都将引发错误
if (pet.swim) {
pet.swim();
}
else if (pet.fly) {
pet.fly();
}
```
为让同样的代码工作就需要使用类型断言a type assertion:
```typescript
let pet = getSmallPet();
if ((<Fish>pet).swim) {
(<Fish>pet).swim();
}
else {
(<Bird>pet).fly();
}
```
### 用户定义的类型保护User-Defined Type Guards
请注意上面必须要使用多次的类型断言。如果在一旦完成检查,就可以知晓各个分支中`pet`的类型那就会好很多Notice that we had to use type assertion several times. It would be much better if once we performed the check, we could know the type of `pet` within each branch
因为TypeScript有着名为 *类型保护type guard*特性,那么很容易做到了。类型保护一些执行运行时检查的表达式,用以确保类型出于特定范围。要定义一个类型保护,只需定定义一个返回值为 *类型谓词* 的函数即可It just so happens that TypeScript has something called a *type guard*. A type guard is some expression that performs a runtime check that guarantees the type in some scope. To define a type guard, we simply need to define a function whose return type is a *type perdicate*)。
```typescript
function isFish(pet: Fish | Bird): pet is Fish {
return (<Fish>pet).swim !== undefined;
}
```
`pet is Fish`就是该示例中的类型谓词。谓词的形式就是`parameterName is Type`,其中的`parameterName`必须是当前函数签名中某个参数的名称。
现在只要以某个变量对`isFish`进行调用如果初始类型兼容那么TypeScript就会将那个变量 *缩小* 到特定类型Any time `isFish` is called with some variable, TypeScript will *narrow* that variable to that specific type if the original type is compatible
```typescript
// 现在对`swim`与`fly`的调用都没有问题了
if (isFish(pet)) {
pet.swim();
}
else {
pet.fly();
}
```
请注意TypeScript不仅知道`if`分支语句中的`pet`是一个`Fish`;它还知道在`else`分支语句中,在不是`Fish`时,那么就肯定是`Bird`了。
### `typeof`的类型保护(`typeof` type guards
现在来回头写一下使用联合类型版本的`padLeft`。可像下面这样使用类型谓词加以编写:
```typescript
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}'`);
}
```
但是这里不得不去定义一个函数来判断某个原生类型就太痛苦了。幸运的是因为TypeScript本身就可以将`typeof x === "number"`识别为类型保护所以无需将其抽象到其本身的函数中。那就意味着可将这些检查写在行内That means we could just write these checks inline
```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` 类型保护* 是一种使用构造函数来限定类型的方式( *`instanceof` type guards* are a way of narrowing types using their constructor function )。下面借用之前的生产中的字符串追加器实例来做说明:
```typescript
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 | StringPadder`
let padder: Padder = getRandomPadder();
if ( padder instanceof SpaceRepeatingPadder ) {
padder; // 类型被限定为 `SpaceRepeatingPadder`
}
if ( padder instanceof StringPadder ) {
padder; // 类型被限定为`StringPadder`
}
```
`instanceof`的右侧需要是构造函数而TypeScript将把变量限定为The right side of the `instanceof` needs to be a constructor function, and TypeScript will narrow down to:
1. 在该函数的`prototype`属性类型不是`any`时,该函数的`prototype`属性类型the type of the function's `prototype` property if its type is not `any`
2. 该函数`prototype`属性类型的构造签名所返回的类型联合the union of types returned by that type's construct signatures
并按二者的先后顺序进行。
## 可为空值的类型Nullable types
TypeScript有两种特别的类型`null`与`undefined`,相应地有着空值与未定义值。在[基本类型章节](00_basic_data_types.md)对它们进行了简要介绍。默认情况下,类型检查器认为`null`与`undefined`可被赋值给任何变量。对于所有类型,`null`与`undefined`都是有效的值。那就意味着,要 *阻止* 将它们赋值给各种类型,即使有意这样做,都是不可能的。`null`值的发明者Tony Hoare, 把这一特性,称之为他的[“十亿美元错误”](https://en.wikipedia.org/wiki/Null_pointer#History)。
编译器的`--strictNullChecks`开关可修正这一点:在声明某个变量时,它就不自动包含`null`或`undefined`了。要显式地包含它们,可使用联合类型:
```typescript
let s = "foo";
s = null; // 错误,`null` 无法赋值给`string`
let sn: string | null = "bar";
sn = null; // 没有问题
sn = undefined; // 错误,`undefined` 无法赋值给 `string | null`
```
请留意TypeScript是以不同方式来对待`null`与`undefined`的这是为了与JavaScript的语义相匹配。`string | null`与`string | undefined` 及`string | undefined | null`是不同的类型。
### 可选参数与属性Optional parameters and properties
在开启`--strictNullChecks`编译选项时,可选参数将自动加上`| undefined`
```typescript
function f(x: number, y?: number) {
return x + (y || 0);
}
f(1, 2);
f(1);
f(1, undefined);
f(1, null); //错误,`null`不能赋值给`number | undefined`
```
对于可选属性,这也是适用的:
```typescript
class C {
a: number;
b?: number;
}
let c = new C();
c.a = 12;
c.a = undefined; // 错误,`undefined`不能赋值给`number`
c.b = 13;
c.b = undefined;
c.b = null; // 错误,`null` 无法赋值给`number | undefined`
```
### 类型保护与类型断言Type guards and type assertions
因为可为空值类型是以联合a union实现的那么就需要使用类型保护来处理`null`。幸运的是这与在JavaScript中所写的代码一样
```typescript
function f (sn: string | null): string {
if (sn == null) {
return "default";
}
else {
return sn;
}
}
```
这里`null`的排除是相当直观的,但也可以使用更简洁的运算符:
```typescript
function f (sn: string | null): string {
return sn || "default";
}
```
在那些编译器无法消除`null`或`undefined`的地方可使用类型断言运算符the type assertion operator来手动移除它们。该语法就是后缀`!`的使用: `identifier!`将从`identifier`的类型中移除`null`与`undefined`
```typescript
function broken(name: string | null): string {
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // 错误,`name` 可能是`null`
}
name = name || "Bob";
return postfix("great");
}
function fixed(name: string | null): string {
function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // 没有问题
}
name = name || "Bob";
return postfix("great");
}
```
因为编译器无法消除嵌套函数内部的空值(除了那些立即执行函数表达式外),因此该示例使用了一个嵌套函数。而编译器之所以无法消除嵌套函数内的空值,是因为编译器无法对所有的嵌套函数调用进行追踪,尤其是在外层函数内返回嵌套函数时。由于编译器在不知道嵌套函数在何处调用,那么它就无法知道函数体执行时`name`的类型会是什么The example uses a nested function here because the compiler can't eliminate nulls inside a nested function(except immediately-invoked function expressions). That's because it can't track all calls to the nested function, especially if you return it from the outer function. Without knowing where the function is called, it can't know what the type of `name` will be at the time the body executes
## 类型别名Type Aliases
类型别名特性为某个类型创建出一个新的名称。类型别名有时与接口类似但可以对原生类型、联合类型、元组及其它不得不手写的类型进行命名Type aliases create a new name for a type. Type aliases are sometimes similar to interfaces, but can name primitives, unions, tuples, and any other types that you'd otherwise have to write by hand
```typescript
type Name = string;
type NameResolver = () => string;
type NameOrResolver = Name | NameResolver;
function getName (n: NameOrResolver): Name {
if ( typeof n === "string" ) {
return n;
}
else {
return n();
}
}
```
命名操作并不会直接创建出一个新类型 -- 其创建出一个到那个类型引用的 *名称* Aliasing doesn't actually create a new type - it creates a new *name* to refer to that type。对原生类型的重命名并不十分有用不过这可用作一种程序文档的形式。
与接口一样,类型别名也可以是泛型的(通用的) -- 可仅加上类型参数,并在别名声明的右侧使用即可。
```typescript
type Container<T> = { value: T };
```
还可以在属性中引用类型别名本身:
```typescript
type Tree<T> {
value: T;
left: Tree<T>;
right: Tree<T>;
}
```
当与交集类型一起时,就可以做出一些相当令人费解的类型:
```typescript
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;
```
但是,要将类型别名放在声明右侧的任意地方,是不可能的:
```typescript
type Yikes = Array<Yikes>; //错误
```
### 接口与类型别名Interfaces vs. Type Aliases
如前面所提到的,类型别名能表现得有点像接口那样;但类型别名与接口也有着些许不同。
一个差异在于接口创建出在所有地方使用的新名称。而类型别名并不会创建出新名称 -- 举例来说,错误消息就不会使用别名。在下面的代码里,如在代码编辑器中鼠标悬挺在`interfaced`上,就会提示其返回的是一个`Interface`,但对于`aliased`则将提示返回的是对象字面值类型object literal type
```typescript
type Alias = { num: number }
interface Interface {
num: number;
}
declare function aliased (arg: Alias): Alias;
declare function interfaced (arg: Interface): Interface;
```
第二个重要的不同就是类型别名不能被扩展或被实施它们也不能扩展或实施其它类型A second important difference is that type aliases cannot be extended or implemented from(nor can they extend/implement other types))。由于[软件的理想属性,在于对扩展始终开放](https://en.wikipedia.org/wiki/Open/closed_principle),因此应尽可能使用接口,而不是类型别名。
反过来说,在无法使用接口类表达某个外形(建模)及需要使用联合或元组类型时,往往就是类型别名派上用场的时候。
## 字符串字面类型String Literal Type
字符串字面类型特性允许给某个字符串指定其所肯定具有的准确值String literal types allow you to specify the exact value a string must have。实践中的字符串字面类型与联合类型、类型保护及类型别名等有很好的结合。可一并使用这些特性从而获得字符串的类似于枚举的表现。
```typescript
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 {
// 错误!不会传入 `null``undefined`
}
}
}
let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // 错误: `uneasy`是不允许的
```
可传入三个允许字串的任意一个,但任何其它字符串的传入,都将导致错误:
```sh
`"uneasy"`类型的参数不能指派给类型`"ease-in" | "easy-out" | "easy-in-out"`的参数
```
字符串字面值类型还可以同样方式用于区分加载元素String literal types can be used in the same way to distinguish overloads
```typescript
function createElement (tagName: "img"): HTMLImageElement;
function createElement (tagName: "input"): HTMLInputElement;
// ... 更多的加载元素 ...
function createElement (tagName: string): Element {
// ... 这里是代码 ...
}
```
## 数字字面值类型Numeric Literal Types
TypeScript 也具有数字字面值类型。
```typescript
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}
```
很少显式地写数字字面值类型,在使用数字字面值类型来缩小范围,从而可捕获程序错误时,此特性就有用处:
```typescript
function foo (x: number) {
if ( x !== 1 || x !== 2 ) {
// ~~~~~~~
// 运算符 `!==` 不能应用在类型 `1``2`
}
}
```
也就是说,`x` 在与`2`进行比较时,`x`必定为 `1`, 这意味着上面的检查造成了无效的比较。
## 枚举成员类型Enum Member Types
如同在[枚举章节](07_enums.md)所提到的当所有枚举成员都有字面值初始化时枚举成员就具有类型As mentioned in [our section on enums](07_enums.md), enum members have types when every member is literal-initialized
在谈及“单例类型”时,大部分都指的是枚举成员类型与数字/字符串字面值类型虽然很多人都会将“单例类型”与“字面值类型”混用Much of the time when we talk about "singleton types", we're referring to both enum member types as well as numeric/string literal types, though many users will use "singleton types" and "literal types" interchangeably
## 可辨识联合Dicriminated Unions
可将单体类型、联合类型、类型保护及类型别名结合起来,构建出一种名为 *可辨识联合*,也叫作 *标签联合**代数数据类型* 的复杂模式。可辨识联合在函数式编程中是有用的。一些编程语言会对联合进行自动辨识但TypeScript是在JavaScript模式上构建可辨识联合的因为这些JavaScript模式业已存在。可辨识联合有以下三种成分You can combine singleton types, union types, type guards, and type aliases to build an advanced pattern called *dicriminated unions*, also known as *tagged unions* or *algebraic data types*. Discriminated unions are useful in functional programming. Some languages automatically discriminate unions for you; TypeScript instead builds on JavaScript patterns as they exist today. There are three ingredients
1. 具有共同的、单体类型属性的一些类型 -- 辨识依据Types that have a common, singleton type property - the *discrimainant*
2. 一个这些类型联合的类型别名 -- 联合A type alias that takes the union of those types - the *union*
3. 共同属性上的类型保护Type guards on the common property
```typescript
interface Square {
kind: "square";
size: number;
}
interface Rectangle {
kind: "rectangle";
width: number;
height: number;
}
interface Circle {
kind: "circle";
radius: number;
}
```
这里首先声明了一些将在联合中使用到的一些接口。注意每个接口都具备一个有着不同字符串字面值的`kind`属性。该`kind`属性就被成为 *辨识依据discriminant**标签tag*。其它属性则是特定于不同接口的。注意此时这些接口都还未联系起来。那么下面就将它们放入到一个联合中:
```typescript
type Shape = Square | Rectangle | Circle;
```
现在对该可辨识联合加以使用:
```typescript
function area (s: Shape) {
switch ( s.kind ) {
case "square": return s.size * s.size;
case "rectangle": return s.width * s.height;
case "circle": return Math.PI * s.radius ** 2;
}
}
```
### 全面性检查Exhaustiveness checking
在没有涵盖到可辨识联合的全部变种时,如果编译器能予以提示,那就再好不过了。比如,在将`Triangle`加入到`Shape`后,就需要同时更新`area`
```typescript
type Shape = Square | Rectangle | Circle | Triangle;
function area (s: Shape) {
switch ( s.kind ) {
case "square": return s.size * s.size;
case "rectangle": return s.width * s.height;
case "circle": return Math.PI * s.radius ** 2;
}
// 这里因该报错 -- 因为并未处理“triangle”情况
}
```
要达到此目的,有两种方式。第一个就是开启`--strictNullChecks`编译选项,并为该函数指定一个返回值类型:
```typescript
function area (s: Shape): number { // 错误:返回 `number | undefined` (因为三角形时将返回 undefined
switch (s.kind) {
case "square": return s.size * s.size;
case "rectangle": return s.width * s.height;
case "circle": return Math.PI * s.radius ** 2;
}
}
```
因为`switch`已不全面所以TypeScript就注意到某些时候函数`area`将返回`undefined`。在使用了显式的`number`返回类型时,就会得到编译器给出的返回值类型为`number | undefined`的报错。当这种方式有些微妙,同时`--strictNullChecks`对于旧代码也并不总是管用。
第二种方式使用了可被编译器用来对完备性进行检查的`never`类型The second method uses the `never` type that the compiler uses to check for exhaustiveness:
```typescript
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.width * s.height;
case "circle": return Math.PI * s.radius ** 2;
default: return assertNever(s); // 如有漏掉的情形,这里就会出现错误
}
}
```
这里的`assertNever`函数检查`s`为`never`类型 -- 即在排除了所有其它情形后剩下的类型Here, `assertNever` checks that `s` is of type `never` -- the type's left after all other cases have been removed。如果忘掉某个情形那么`s`将具有真实的类型,就将得到一个类型错误。此方式需要定义一个额外函数,不过在忘掉某个情形时,这种方式要直观得多。
## 多态`this`类型Polymorphic `this` types
多态`this`类型,代表的是包含`this`的类或接口的一个 *子类型* 。这被称为F-边界多态。此特性好处在于比如令到更易于表达层次化的i **流式接口** 等。下面是一个在每次操作后都返回`this`的一个简单的计算器代码A polymorphic `this` type represents a type that is the *subtype* of the containing class or interface. This is called F-bounded polymorphism. This makes hierarchical **fluent interfaces** much easier to express, for example. Take a simple calculator that return `this` after each operation
```typescript
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;
}
// ... 这里是其它运算 ...
}
let v = new BasicCalculator (2)
.multiply(5)
.add(1)
.currentValue();
```
因为类用到`this`类型,就可以将其扩展为新类型,且新类型可不加修改地使用要旧有的方法:
```typescript
class ScientificCalculator extends BasicCalculator {
public constructor ( value = 0 ) {
super ( value );
}
public sin () {
this value = Math.sin ( this.value );
return this;
}
// ... 其它的运算在这里 ...
}
let v = new ScientificCalculator (2)
.multiply(5)
.sin()
.add(1)
.currentValue();
```
如果没有`this`类型,`ScientificCalculator`就无法对`BasicCalculator`进行扩展并保留流式接口the fluent interface。`multiply`将返回`BasicCalculator`,而`BasicCalculator`是没有`sin`方法的。不过,在有了`this`类型后,`multiply`将返回`this`,就是这里的`ScientificCalculator`了。
## 索引类型Index types
使用索引类型特性,就可以让编译器对那些用到 **动态属性名称** 的代码进行检查。一种常见的JavaScript范式就是从某个对象中拾取属性的子集With index types, you can get the compiler to check code that uses **dynamic property names**. For example, a common JavaScript pattern is to pick a subset of properties from an object
```typescript
function pluck (o, names) {
return names.map( n => o[n] );
}
```
下面是在TypeScript中上面函数的写法与用法使用了 **索引类型查询****经索引的读写** 运算符Here's how you would write and use this function in TypeScript, using the **index type query** and **indexed access** operators
```typescript
function pluck<T, K extends keyof T>(o: T, names: K[]): T[K][] {
return names.map(n => o[n]);
}
interface Person {
name: string;
age: number;
}
let person: Person = {
name: "Jirad",
age: 35
}
let strings = string[] = pluck ( person, ['name'] ); // 没有问题string[]
```
编译器对`name`是`Person`上的属性进行检查。该示例引入了两个新的类型运算符type operators。首先是`keyof T`,也就是 **索引类型查询运算符**。对于任意类型的`T``keyof T`都是`T`的 **已知的、公开的属性名称的联合**First is `keyof T`, the **index type query operator**. For any type `T`, `keyof T` is **the union of known, public names** of `T`)。比如:
```typescript
let personProps: keyof Person; // 'name' | `age`;
```
`keyof Person`与`'name' | 'age'`是可完全互换的。区别在于如给`Person`加上另一个属性,比如`address: string`,那么`keyof Person`将自动更新为`'name' | 'age' | 'address'`。同时可在如`pluck`函数这样的,提前并不知道有哪些属性名称的通用场合,使用`keyof`运算符。那意味着编译器将就是否传递了正确的属性名称集合给`pluck`进行检查:
```typescript
pluck ( person, ['age', 'unkown'] ); // 错误,'unkown' 不再 `'name' | 'age'` 属性名称联合中
```
引入的第二个运算符,就是`T[K]`**受索引的读写运算符**the **indexed access operator**。这里的类型语法反映的是表达式语法Here, **the type syntax** reflects **the expression syntax**)。那就是说`person['name']`具有类型`Person['name']` -- 在示例中那就仅是`string`。但与索引类型查询一样,可在通用环境中使用`T[K]`,这才是其与生俱来的威力。只需确保类型变量`K extends keyof T`即可。下面是另一个名为`getProperty`函数的示例:
```typescript
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] 的类型就是`T[K]`
}
```
在函数`getProperty`中,`o: T` 与 `name: K`,就意味着`o[name]: T[K]`。一旦返回了`T[K]`结果,编辑器将立即实例化键的实际类型,因此`getProperty`函数的返回值类型,将根据所请求的属性,而有所不同。
```typescript
let name: string = getProperty(person, 'name');
let age: number = getProperty(person, 'age');
let unkown = getProperty(person, 'unknown'); // 错误,'unkown'不在 `'name' | 'age'`
```
## 索引类型与字符串索引签名Index types and string index signatures
`keyof`与`T[K]`都作用于 **字符串索引签名**。如有一个带有字符串索引签名的类型,那么`keyof T`就仅仅是`string`。同时`T[string]`也仅仅是该索引签名的类型(`keyof` and `T[K]` interact with **string index signatures**. If you have a type with a string index signature, `keyof T` will just be `string`. And `T[string]` is just the type of the index signature
```typescript
interface Map<T> {
[key: string]: T;
}
let keys: keyof Map<number>; // 字符串
let values: Map<number>['foo']; // 数字
```
## 映射的类型Mapped types
使用既有类型并令到其各个属性可选是一项很常见的任务A common task is to take an existing type and make each of its properties optional
```typescript
interface PersonPartial {
name?: string;
age?: number;
}
```
或者可能想要一个只读版本:
```typescript
interface PersonPartial {
readonly name: string;
readonly age: number;
}
```
因为在JavaScript或出现很多这种情况因此TypeScript提供了一种基于原有类型来创建新类型的方式 -- **映射的类型**This happens often enough in JavaScript that TypeScript provides a way to create new types based on old types -- **mapped types**)。在映射的类型中,新类型对原有类型中的各个属性,都以同样方式进行转换。比如,可将所有属性,都成为`readonly`的或可选的。下面是两个示例:
```typescript
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]? T[P];
}
```
下面是用法:
```typescript
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;
```
来看看下面这个最简单的映射类型及其构成Let's take a look at the simplest mapped type and its parts
```typescript
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };
```
该语法酷似那种内部带有`for .. in`的索引签名语法The syntax resembles the syntax for index signatures with a `for .. in` inside。其有着三个构成部分
1. 类型变量`K`,其将依次绑定到各个属性。
2. 字符串字面值的联合`Keys`,其包含了那些要进行迭代的属性名称。
3. 属性的结果类型The resulting type of the property
上面的示例中,`Keys`是一个硬编码的属性名称清单,同时属性类型全是`boolean`,那么此映射的类型就等价于下面的写法:
```typescript
type Flags = {
option1: boolean;
option2: boolean;
}
```
但实际应用看起来会像上面的`Readonly`或`Partial`Real applications, however, look like `Readonly` or `Partial` above。它们总是基于一些既有类型以某种方式对属性进行转换。那就是要用到`keyof`与受索引读写类型的地方:
```typescript
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }
```
在这些示例中,属性清单为`keyof T`,结果类型是`T[P]`的某些变种。这可作为映射类型一般用法的良好模板。这是因为此类变换,是[同态的](https://en.wikipedia.org/wiki/Homomorphism),也就是说,只是对于`T`的属性进行映射而不涉及其它部分。编译器明白在加入任何新东西前其只能对所有既有属性修饰器进行拷贝In these examples, the properties list is `keyof T` and the resulting type is come variant of `T[P]`. This is a good template for any general use of mapped types. That's because this kind of transformation is [homemorphic](https://en.wikipedia.org/wiki/Homomorphism), which means that the mapping applies only to properties of `T` and no others. The compiler knows that it can copy all the existing property modifiers before adding any new ones. For example, if `Person.name` was readonly, `Partial<Person>.name` would be readonly and optional。比如如果`Person.name`是只读的,那么`Partial<Person>.name`将是只读且可选的。
下面是另一个示例,其中`T[P]`被封装在`Proxy<T>`类中:
```typescript
type Proxy<T> = {
get(): T;
set(value: T): void;
}
type Proxify<T> = {
[P in keyof T]: Proxy<T[P]>;
}
function proxify<T>(o: T): Proxify<T> {
// ... 这里封装了代理 ...
}
let proxyProps = proxify(props);
```
注意`Readonly<T>`与`Partial<T>`是如此重要,以至于它们与`Pick`及`Record`一道被收录进入了TypeScript的标准库中
```typescript
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
}
type Record<K extends string, T> = {
[P in K]: T;
}
```
`Readonly`、`Partial`与`Pick`是同态的,但`Record`却不是。`Record`不是同态的一个原因就是其并不是取得一个要从其中拷贝属性的输入类型One clue that `Record` is not homomorphic is that it doesn't take an input type to copy properties from
```typescript
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>
```
非同态类型会创建新的属性因此它们无法从任何地方拷贝属性修饰器Non-homomorphic types are essentially creating new properties, so they can't copy property modifiers from anywhere
### 自映射类型的推导Inference from mapped types
现在以及知晓封装类型属性的方法那么接着就要对这些类型属性进行解封装Now that you know how to wrap the properties of a type, the next thing you'll want to do is unwrap them。幸运的是对类型属性的解封装是相当容易的:
```typescript
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for ( const k in t ) {
result[k] = t[k].get();
}
return result;
}
```
注意这种解封装推导仅适用于同态的映射类型。如映射类型不是同态的就必须给解封装函数一个显式类型参数Note that this unwrapping inference only works on homomorphic mapped types. If the mapped type is not homomorphic you'll have to give an explicit type parameter to your unwrapping function

100
11_symbols.md Normal file
View File

@ -0,0 +1,100 @@
# 符号
**Symbols**
## 简介
从ECMAScript 2015开始与`number`和`string`一样,`symbol`就是一种原生数据类型了。
`symbol`值的创建,是通过调用`Symbol`构造器完成的。
```typescript
let sym1 = Symbol();
let sym2 = Symbol("key"); // “key”是可选字符串
```
符号是不可修改的,且具独特性。
```typescript
let sym2 = Symbol("key");
let sym3 = Symbol("key");
sym2 === sym3; // `false`,因为符号是独特的
```
与字符串一样,符号可作为对象属性的键来使用。
```typescript
let sym = Symbol();
let obj = {
[sym]: "value"
};
console.log(obj[sym]); // "value"
```
符号也可与 **计算属性声明** 一起来声明对象属性与类成员Symbols can also be combined with **computed property declarations** to declare object properties and class members
```typescript
const getClassNameSymbol = Symbol();
class C {
[getClassNameSymbol](){
return "C";
}
}
let c = new C();
let className = c[getClassNameSymbol](); // "C"
```
## 一些熟知的符号Well-known Symbols
出来用户定义的符号外还有一些内建熟知的符号。内建的符号用于表示内部的语言行为In addition to user-defined symbols, there are Well-known built-in symbols. Built-in symbols are used to represent internal language behaviors
下面是一个熟知符号的清单。
### `Symbol.hasInstance`
判断构造器对象是否将某个对象识别为该构造器的一个实例的方法。由`instanceof`运算符的语义调用A method that determines if a constructor object recognizes an object as one of the constructor's insstance. Called by the semantics of the `instanceof` operator
### `symbol.isConcatSpreadable`
一个表明某对象应通过`Array.prototype.concat`被扁平化到其数组元素的逻辑值A Boolean value indicating taht an object should be flattened to its array elements by `Array.prototype.concat`)。
### `Symbol.iterator`
返回某对象默认迭代器的方法。由`for .. of`语句的语义调用A method that returns the default iterator for an object. Called by the semantics of the `for .. in` statement
### `Symbol.match`
一个对正则表达式与字符串进行匹配的正则表达式函数。由`String.prototype.match`方法进行调用A regular expression method that matches the regular expression against a string. Called by the `String.prototype.match` method
### `Symbol.replace`
一个对字符串中匹配的子字符串进行匹配的正则表达式函数。由`String.prototype.replace`方法进行调用A regular expression method that replaces matched substring of a string. Called by the `String.prototype.replace` method
### `Symbol.search`
一个返回与正则表达式匹配的某字符串中索引的正则表达式函数。由`String.prototype.search`方法调用A regular expression method that returns the index within a string that matches the regular expression. Called by the `String.prototype.search` method
### `Symbol.species`
一个用于创建派生对象的函数值属性也就是构造函数A function valued property that is the constructor function that is used to create derived objects
### `Symbol.split`
一个在与给出的正则表达式匹配的索引出对字符串进行分割的正则表达式函数。由`String.prototype.split`方法进行调用A regular expression method that splits a string at the indices that match the regular expression. Called by the `String.prototype.split` method
### `Symbol.toPrimitive`
一个将对象转换到相应的原生值的方法。由`ToPrimitive`抽象操作进行调用A method that converts an object to a corresponding primitive value. Called by the `ToPrimitive` abstract operation
### `Symbol.toStringTag`
一个在对象默认字符串说明的创建中用到的字符串值。由内建的`Object.prototype.toString`方法调用A string value that is used in the creation of the default string description of an object. Called by the built-in method `Object.prototype.toString`)。
### `Symbol.unscopables`
一个其自身属性名称是那些排除在相关对象的`with`环境绑定之外的属性名称的对象An `Object` whose own property names are property names excluded from the `with` environment bindings of the associated objects

View File

@ -0,0 +1,82 @@
# 迭代器与生成器
**Iterators and Generators**
## 可迭代对象Iterables
在对象有着`Symbol.iterator`属性时,其就被认为是可迭代的。一些内建类型,比如`Array`、`Map`、`Set`、`String`、`Int32Array`、`Uint32Array`等,都已经有着其已实现的`Symbol.iterator`属性。对象上的`Symbol.iterator`函数,赋值返回要迭代的值的清单(`Symbol.iterator` function on an object is responsible for returning the list of values to iterate on
### `for..of`语句
`for..of`对可迭代对象进行循环,调用对象上的`Symbol.iterator`属性。下面是一个在数组上的简单`for..of`循环:
```typescript
let someArray = [1, "string", false];
for (let entry of someArray){
console.log(entry); // 1, "string", false
}
```
### `for..of`与`for..in`语句
`for..of`与`for..in`语句都是对清单进行迭代;但所迭代的值却是不同的,`for..in`返回的是所迭代对象的 *键* 的清单,而`for..of`则是返回的所迭代对象数值属性的 *值* 的清单Both `for..of` and `for..in` statements iterate over lists; the values iterated on are differenct though, `for..in` returns a list of *keys* on the object being iterated, whereas `for..of` returns a list of *values* of the numeric properties of the object being interatd
```typescript
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`属性的内建对象才允许对存储值的访问Built-in objects like `Map` and `Set` implement `Symbol.iterator` property allowing access to stored values
```typescript
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"
}
```
## 关于代码生成Code generation
### 目标代码为ES5及ES3
在生成目标代码为ES5或ES3时只允许在`Array`类型上使用迭代器。就算非数组值实现了`Symbol.iterator`属性, 在它们上使用`for..of`循环都是错误的。
编译器将为`for..of`循环生成一个简单的`for`循环,例如:
```typescript
let numbers = [1, 2, 3];
for (let num of numbers) {
console.log(num);
}
```
将生成如下代码:
```javascript
var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
var num = numbers[_i];
console.log(_i);
}
```
### 目标代码为ECMAScript2015或更高版本时
在以兼容ECMAScript2015引擎为目标时编译器将生成`for..of`循环,从而以引擎中的内建迭代器实现为目标。

777
13_modules.md Normal file
View File

@ -0,0 +1,777 @@
# 模块
**Modules**
> **关于术语的一点说明**在TypeScript 1.5中需要注意的是其中的命名法发生了改变。为了与ECMAScript 2015的命名法保持一致"内部模块"以及被命名为“命名空间”。“外部模块”已被简化为“模块”,(名以上`module X {`与现在所指的`namespace X{`是等价的。it's important to note that in TypeScript 1.5, the nomenclature has changed. "Internal modules" are now "namespace". "External modules" are now simply "modules", as to align with ECMAScript 2015's terminology, (namely that `module X {` is equivalent to the now-preferred `namespace X{`))。
## 简介
从ECMAScript 2015开始JavaScript就有了模块的概念。TypeScript采纳了此概念。
模块在它们自己的作用域而非全局作用域中执行Modules are executed within their own scope, not in the global scope; 这就意味着在模块中所声明的变量、函数、类等等,除非在使用某种[`export`形式](#export)被显式地对其进行了导出,否则它们在模块外面是不可见的。反过来,要消费从另一个模块中导出的变量、函数、类、接口等,就必须要使用某种[`import`形式](#import)将其导入。
模块是声明式的模块间的关系实在文件级别以导入及导出进行指定的Modules are declarative; the relationships between modules are specified in terms of imports and exports at the file level
使用模块加载器可在模块中导入其它模块。运行时的模块加载器负责在执行某个模块前定位并执行其所有依赖。在JavaScript中熟知的模块加载器有Node.js的[CommonJS](https://en.wikipedia.org/wiki/CommonJS)模块加载器及Web应用的[require.js](http://requirejs.org/)加载器。
在TypeScript中就如同ECMAScript 2015中一样任何包含了顶级`import`与`export`的文件都被看作是一个模块In TypeScript, just as in ECMAScript 2015, any file containing a top-level `import` and `export` is considered a module。相反不带有顶级`import`或`export`声明的文件,则被作为脚本对待,其内容是全局作用域可用的(因此对模块也可用)。
## 导出
### 导出某个声明
任何声明(诸如变量、函数、类型、类型别名或接口等)都可以通过加上`export`关键字,加以导出。
*Validation.ts*
```typescript
export interface StringValidator {
isAcceptable(s: string): boolean;
}
```
*ZipCodeValidator.ts*
```typescript
export const numberRange = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
```
### 导出语句
在需要为消费者而将导出项重命名时,导出语句就很好用,因此上面的示例可写成这样:
```typescript
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator };
```
### 再度导出Re-exports
通常模块会对其它模块进行扩展并部分地暴露出一些它们的特性。那么再度导出就不会在本地导入或是引入一个本地变量A re-export does not import it locally, or introduce a local variable
*ParseIntBasedZipCodeValidator.ts*
```typescript
export class ParseIntBasedZipCodeValidator {
isAcceptable (s: string) {
return s.length === 5 && parseInt(s).toString() === s;
}
}
// 在重命名原始的验证器后导出
export { ZipCodeValidator as RegExpBasedZipCodeValidator } from "./ZipCodeValidator";
```
作为可选项,一个模块可封装一个或多个模块,并通过使用`export * from "module"`,而将所有它们的导出项结合起来。
*AllValidators.ts*
```typescript
export * from "./StringValidator"; // 导出接口 `StringValidator`
export * from "./LettersOnlyValidator"; // 导出类 `LettersOnlyValidator`
export * from "./ZipCodeValidator"; // 导出类`ZipCodeValidator`
```
## 导入
导入就跟从模块导出一样容易。通过下面这些`import`形式,就可完成已导出声明的导入:
### 从某个模块中导入单一的导出项
```typescript
import { ZipCodeValidator } from "./ZipCodeValidator";
let myValidator = new ZipCodeValidator();
```
导入项也可以被重命名
```typescript
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();
```
### 将整个模块导入到单个变量中,并使用该变量来访问该模块的导出项
```typescript
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();
```
### 仅以副作用目的导入模块Import a module for side-effects only
尽管此种方式不是推荐的做法,但某些模块设置了一些可被其它模块使用的全局状态。这些模块可以没有导出项,或者消费者并不对其导出项感兴趣。要导入这些模块,就用下面的形式:
```typescript
import "./my-module.js"
```
## 默认导出项Default exports
每个模块可选择导出一个`default`导出项。默认导出项是以关键字`default`进行标记的;同时每个模块只能有一个`default`导出项。`default`导出项的导入,使用的是一种有别的形式。
`default`导出项用起来很顺手。比如诸如Jquery这样的库就可以有一个`jQuery`或`$`的默认导出项,当然也要以名称`$`或`jQuery`对其进行导入。
*JQuery.d.ts*
```typescript
declare let $: JQuery;
export default $;
```
*App.ts*
```typescript
import $ from "JQuery";
$("button.continue").html( "Next Step..." );
```
类与函数的声明可直接作为默认导出项进行编写。默认导出的类与函数声明名称是可选的Default export class and function declaration names are optional
*ZipCodeValidator.ts*
```typescript
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable (s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}
```
*Test.ts*
```typescript
import validator from "./ZipCodeValidator";
let myValidator = new validator();
```
或者
*StaticZipCodeValidator.ts*
```typescript
const numberRegexp = /^[0-9]+$/;
export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
```
*Test.ts*
```typescript
import validator from "./StaticZipCodeValidator";
let strings = ["Hello", "98052", "101"];
// 使用函数验证
strings.forEach(s => {
console.log("${s}" ${validate(s)}) ? " matches ": " does not match ";
});
```
`default`导出项还可以只是值:
*OneTwoThree.ts*
```typescript
export default "123";
```
*Log.ts*
```typescript
import num from "./OneTwoThree";
console.log(num); // "123"
```
## `export =`与`import = require()`
CommonJS与AMDAsynchronous Module Definition API异步模块定义接口都具有`exports`对象这个概念,该对象包含了来自某个模块的所有导出项。
它们也支持以一个定制单一对象,来替换`exports`。默认导出项就是为了作为进行此做法的一个替代但二者并不兼容。TypeScript支持`export =`特性以对传统的CommonJS与AMD工作流进行模仿They also support replacing the `exports` object with a custom single object. Default exports are meant to act as a replacement for this behavior; however, the two are incompatible. TypeScript supports `export =` to model the traditional CommonJS and AMD workflow
`export =`语法指定出从模块导出的单个对象。其可以是类、接口、命名空间、函数或枚举等The `export =` syntx specifies a single object that is exported from the module. This can be a class, interface, namespace, funciton, or enum
在使用`export =`来导出某个模块时必须使用特定于TypeScript的`import module = require("module")`,来导入该模块。
*ZipCodeValidator.ts*
```typescript
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;
```
*Test.ts*
```typescript
import zip = require("./ZipCodeValidator");
// 一些要尝试的示例
let strings = ["hello", "98052", "101"];
// 要使用的验证器
let validator = new zip();
// 给出各个字符串是否通过各个验证器检查
strings.forEach(s => {
console.log("${s}" - ${ validator.isAcceptable(s) ? " matches" : "does not match"});
});
```
## 模块的代码生成
根据编译期间特定的目标模块编译器将生成针对Node.jsCommonJS、require.jsAMD、[UMD](https://github.com/umdjs/umd)Universal Module Definition API通用模块定义接口、[SystemJS](https://github.com/systemjs/systemjs)启用在浏览器及NodeJS中动态ES模块工作流的可配值模块加载器或[ECMAScript 2015原生模块](http://www.ecma-international.org/ecma-262/6.0/#sec-modules)ES6的模块加载系统。可参考上述各个模块加载器文档来进一步了解有关生成代码中`define`、`require`与`register`调用有什么作用。
下面的简单示例,演示了在导入与导出期间所用到的名称,是如何被翻译到模块加载代码中去的。
*SimpleModule.ts*
```typescript
import m = require("mod");
export let t = m.something + 1;
```
*AMD/RequireJS 的 SimpleModule.js*
```javascript
define(["require", "exports", "./mod"], function(require, exports, mod_1) {
exports.t = mod_1.something + 1;
});
```
*CommonJS/Node 的 SimpleModule.js*
```javascript
var mod_1 = require("./mod");
exports.t = mod_1。something + 1;
```
*UMD de SimpleModule.js*
```javascript
(function (factory) {
if (typeof module === "object" && typeof module.exports === "object") {
var 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) {
var mod_1 = require("./mod");
exports.t = mod_1.something + 1;
});
```
*SystemJS 的 SimpleModule.js*
```javascript
System.register(["./mod"], function(exports_1) {
var mod_1;
var t;
return {
setters: [
function (mod_1_1) {
mod_1 = mod_1_1;
}],
execute: function () {
exports_1("t", t = mod_1.something + 1);
}
}
});
```
*原生ECMAScript 2015模块式的 SimpleModule.js*
```javascript
import { something } from "./mod"
export var t = something + 1;
```
## 简单示例
接下来将对先前示例中用到的验证器实现综合为仅从各个模块导出单个的命名导出项Below, we've consolidated the Validator implementations used in previous examples to only export a single named export from each module
必须要在命令行指定模块编译的目标。对于Node.js使用`--module commonjs`对于require.js使用`--module amd`。比如:
```bash
tsc --module commonjs Test.ts
```
编译完成时,每个模块都将成为一个单独的`.js`文件。对于那些引用标签,编译器将跟随`import`语句对依赖文件进行编译As with reference tags, the compiler will follow `import` statements to compile dependent files
*Validation.ts*
```typescript
export interface StringValidator {
isAcceptable (s: string): boolean;
}
```
*LettersOnlyValidator.ts*
```typescript
import { StringValidator } from "./Validation"
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable (s: string) {
return lettersRegexp.test(s);
}
}
```
*ZipCodeValidator.ts*
```typescript
import { StringValidator } from "./Validation";
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable (s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
```
*Test.ts*
```typescript
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";
// 一些测试样本
let strings = ["Hello", "98052", "101"];
// 要用到的验证器
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// 演示各个字符串是否通过各个验证器验证
strings.forEach(s => {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches": "does not match" } ${ name }`);
}
});
```
## 可选的模块加载与其它复杂加载场景Optional Module Loading and Other Advanced Loading Scenarios
在一些案例中可能打算仅在部分情况下才装入某个模块In some cases, you may want to only load a module under some condition。TypeScript中可使用下面所给出的模式实现这种或其它复杂加载场景以在不损失类型安全的前提下实现模块加载器的直接调用。
编译器对各个模块在生成的JavaScript中是否用到进行探测。如果某个模块识别符仅作为类型注记的部分被用到而没有作为表达式用到那么对那个模块就不会生成`require`调用If a module identifier is only ever used as part of a type annotations and never as an expression, then no `require` call is emitted for that module。这种对未使用引用的省略是一种良好的性能优化同时也允许这些模块的可选加载。
该模式的核心理念, 就是`import id = require("...")`语句给予了对该模块所暴露出的类型的访问The core idea of the pattern is that the `import id = require("...")` statement gives us access to the types exposed by the module。如下面所给出的`if`块一样,该模块加载器是动态触发的(通过`require`。此特性利用了引用省略优化the reference-elision optimization因此仅在需要该模块时才进行加载。此模式要生效就在于通过`import`所定义的符号在类型位置处用到即是绝不能在将会生成到JavaScript中的位置用到
可使用`typeof`关键字,来维护类型安全。在某个类型位置出使用`typeof`关键字时,将产生某个值的类型,在该模式的情况下,就是模块的类型。
*Node.js 中的动态模块加载*
```typescript
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("...")) { /* ... */ }
}
```
*示例: require.js中的动态模块加载*
```typescript
declare function require(moduleName: 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("...")) { /* ... */ }
});
}
```
## 与别的JavaScript库打交道Working with Other JavaScript Libraries
需要对库所暴露出的API进行声明以描述那些不是用TypeScript编写的库的形状To describe the shape of libraries not written in Typescript, we need to declare the API that the library exposes
对于那些并未定义某种实现的声明将其成为“外围”We call delarations that don't define an implementation "ambient")。这些声明通常都是在`.d.ts`文件中定义的。如属性C/C++语言,那么这些文件可被看作是`.h`文件。来看看下面这些示例。
### 外围模块Ambient Modules
Node.js中的大多数任务都是通过加载一个或多个模块完成的。尽管可将各个模块定义在其自己的、带有顶层导出声明的`.d.ts`文件中,不过将这些模块作为一个较大的`.d.ts`文件进行编写,则会更方便。做法就是使用一个类似与外围命名空间的结构,实际上使用`module`关键字与在随后的导入中可以使用的模块引用名称To do so, we use a construct similar to ambient namespaces, but we use the `module` keyword and the quoted name of the module which will be available to a later import。比如
*node.d.ts (简化摘要)*
```typescript
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 nomarlize(p: string): string;
export function join(...paths: any[]): string;
export var sep: string;
}
```
现在就可以 `/// <reference> node.d.ts` 并使用`import url = require("url");` 或 `import * as URL from "url"`来装入模块了。
```typescript
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("http://www.typescriptlang.org");
```
### 速记式外围模块Shorthand ambient modules
在不打算于使用某个新模块之前花时间编写其声明时就可使用速记式声明特性a shorthand declaration以快速开工If you don't want to take the time to write out declarations before using a new module, you can use a shorthand declaration to get started quickly
*declarations.d.ts*
```typescript
declare module "hot-new-module";
```
来自速记模块的所有导入,都将具有`any`类型。
```typescript
import x, {y} from "hot-new-module";
x(y);
```
### 通配符式模块声明Wildcard module declarations
一些诸如`SystemJS`及`AMD`的模块加载器允许导入非JavaScript内容Some module loaders such as SystemJS and AMD allow non-JavaScript content to be imported。这些模块加载器通常使用前缀或后缀a prefix or suffix来表明特殊加载的语义。通配符式模块声明就可用来满足这些情况。
```typescript
declare module "*!text" {
const content: string;
export default content;
}
// 反过来的做法
declare module "json!*" {
const value: any;
export default value;
}
```
现在就可以导入与`"*!text"`或`json!*`匹配的模块了。
```typescript
import fileContent from "./xyz.txt!text";
import data from "json!http://example.com/data.json";
console.log(data, fileContent);
```
### UMD模块UMD Modules
一些库被设计为可在多种模块加载器中使用或是不带有模块加载功能它们采用全局变量。这些就是所说的UMD模块。这类库的访问是通过导入项或全局变量进行的。比如
*math-lib.d.ts*
```typescript
export function isPrime (x: number): boolean;
export as namespace mathLib;
```
随后该库便可作为模块内的一个导入进行使用了:
```typescript
import { isPrime } from "math-lib";
isPrime(2);
mathLib.isPrime(2); // 错误:在模块内部不能使用全局定义
```
其也可以作为一个全局变量使用,但仅限于在脚本内部(脚本是不带有导入与导出的文件)。
```typescript
mathLib.isPrime(2);
```
## 模块如何组织的守则Guidance for structuring modules
***尽可能在顶层进行导入Export as close to top-level as possible***
模块消费者在使用导入的模块时摩擦应尽可能少。加入过多层次的嵌套将导致低效因此在打算如何组织模块上应深思熟虑Consumers of your module should have as little friction as possible when using things that you export. Adding too many levels of nesting tends to be cumbersome, so think carefully about how you want to structure things
从模块导出命名空间,就是加入过多层数嵌套的一个示例。虽然命名空间有时有其用处,但在使用模块时它们也加入了一层额外的非直接因素。这种做法很快会变为用户的痛点,同时通常是不必要的。
导出类上的静态方法,有着类似问题 -- 类本身加入了一层嵌套。除非这么做提升表现力或有某种明确有用形式的意图那么就考虑简单地导出一个辅助函数a helper function
***如仅导出单个的`class` 或 `function`,那么请使用`export default`***
与`靠近顶层导出`一样默认导出项的使用也能减少模块消费者上的摩擦Just as "exporting near the top-level" reduces friction on your module's consumers, so does introducing a default export。在模块的主要目的是存放一个特定的导出时就应考虑将其作为默认导出项进行导出。这样做令到导入与导入项的使用更为容易一些。比如
*MyClass.ts*
```typescript
export default class SomeType {
constructor () { ... }
}
```
*MyFunc.ts*
```typescript
export default function getThing() { return "thing"; }
```
*Consumer.ts*
```typescript
import t from "./MyClass";
import f from "./MyFunc";
let x = new t();
console.log(f());
```
对于模块消费者,这是可选的。它们可将类型命名为它们想要的任何名字(这里的`t`并不需要任何过度过度点缀来找到对象They can name your type whatever they want(`t` in this case) and don't have to do any excessive dotting to find your objects
***如要导出多个对象,那么将它们一起放在顶层***
*MyThings.ts*
```typescript
export class SomeType { /* ... */ }
export function someFunc () { /* ... */ }
```
相反,在导入时应注意以下规则:
***显式地列出所导入的名称Explicitly list imported names***
*Consumer.ts*
```typescript
import { SomeType, someFunc } from "./MyThings";
let x = new SomeType();
let y = someFunc();
```
***使用命名空间导入模式来导入较多的对象Use the namespace import pattern if you're importing a large number of things***
*MyLargeModule.ts*
```typescript
export class Dog { ... }
export class Cat { ... }
export class Tree { ... }
export class Flower { ... }
```
*Consumer.ts*
```typescript
import * as myLargeModule from "./MyLargeModule.ts";
let x = new myLargeModule.Dog();
```
### 再导出以进行扩展Re-export to extend
通常需要在某个模块上进行功能扩展。一种常见的JS模式就是使用 *扩展* 来增加原始对象这与JQuery的扩展原理类似。如同先前提到的模块并不会像全局命名空间对象那样 *融合*。因此推荐的做法是 *不要* 改动原始对象而是导出一个提供新功能的新实体A common JS pattern is to augment the original object with *extensions*, similar to how JQuery extensions work. As we've mentioned before, modules do not *merge* like global namespace objects would. The recommended solution is to *not* mutate the original object, but rather export a new entity that provides the new functionality
考虑下面这个定义在模块`Calculator.ts`中简单的计算器实现。该模块还导出了一个通过传入输入字符串清单,并在最后写出结果的,用于对计算器进行功能测试的辅助函数。
*Calculator.ts*
```typescript
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`函数的一个计算器的简单测试。
*TestCalculator.ts*
```typescript
import { Calculator, test } from "./Calculator";
let c = new Calculator;
test(c, "1+2*33/11=");
```
接着将其扩展到加入对其它进制的支持,来创建`ProgrammerCalculator.ts`吧。
*ProgrammerCalculator.ts*
```typescript
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("基数必须要在0到16的范围");
}
}
protected processDigit(digit: string, currentValue: number) {
if (Programmercalculator.digits.indexOf(digit) >= 0) {
return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
}
}
}
// 将新的已扩展的计算器作为`Calculator`进行导出
export { ProgrammerCalculator as Calculator };
// 还要导出辅助函数
export { test } from "./Calculator";
```
新的`ProgrammerCalculator`模块导出了一个与原始的`Calculator`模块类似的API外形但并没有对原始模块中的任何对象进行修改。下面是对`ProgrammerCalculator`类的测试:
*TestProgrammerCalculator.ts*
```typescript
import { Calculator, test } from "./ProgrammerCalculator";
let c = new Calculator(2);
test(c, "001+010=");
```
### 不要在模块中使用命名空间Do not use namespaces in modules
在初次迁移到基于模块的组织形式时常见的倾向就是将导出项封装到一个额外的命名空间层中When first moving to a module-based organization, a common tendency is to wrap exports in an additional layer of namespaces。模块有着其自己的作用域同时仅有导出的声明是从模块外部可见的。记住了这一点就明白在使用模块时命名空间所提供到的价值就是很有限的。
在组织方式前对于将逻辑上有联系的对象与类型在全局作用域中进行分组命名空间是很好用的。比如在C#中,就能在`System.Collections`找到所有的集合类型。通过将类型组织到层次化的命名空间,就能为这些类型的用户提到到良好的“发现”体验。但是模块本身必然已经文件系统中有所呈现。必须通过路径与文件名来解析这些模块,因此已经有了一套可使用的逻辑组织方案。比如可有着一个包含了清单模块的`/collections/generic/`文件夹On the organization front, namespaces are handy for grouping together logically-related objects and types in the global scope. For example, in C#, you're going to find all the collection types in `System.Collections`. By organizing our types into hierarchical namespaces, we provide a good "discovery" experience for users of those types. Modules, on the other hand, are already present in a file system, necessarily. We have to resolve them by path and filename, so there's a logical organization scheme for us to use. We can have a `/collections/generic` folder with a list module in it
命名空间特性要注意避免全局作用域下的命名冲突。比如可能存在`My.Application.Customer.AddForm`与`My.Application.Order.AddForm`两个有着同样名称而不在同一命名空间的类型。这在模块中却不成问题。在模块中并没有要让两个对象使用相同名称的不明原因。而从消费侧来看任何给定模块的消费者有自主选取它们用于引用该模块的名称的权力因此偶发的命名冲突是不可能出现的Namespaces are important to avoid naming collisions in the global scope. For example, you might have `My.Application.Customer.AddForm` and `My.Application.Order.AddForm` -- two types with the same name, but a different namespace. This, however, is not an issue with modules. Within a module, there's no plausible reason to have two objects with the same name. From the comsumption side, the consumer of any given modules gets to pick the name that they will use to refer to the modules, so accidental naming conflicts are impossible
> 关于模块与命名空间的更多讨论,请参考[命名空间与模块](15_namespaces_and_modules.md)小节。
## 避免事项Red Flags
所有下面列出的都是模块组织中需要避免的。在文件中有下面之一时对要两次检查没有试着将外部模块进行命名空间操作All of the following are red flags for module structuring. Double-check that you're not trying to namespace your external modules if any of these apply to your files
+ 仅有一个顶层声明,且为`export namespace Foo { ... }`的文件(移除`Foo`并将所有内容进行提升A file whose only top-level declaration is `export namespace Foo { ... }` (remove `Foo` and move everything 'up' level)
+ 仅有单个的`export class`或`export function`的文件(请考虑使用`export default`
+ 有着相同位处顶层的`export namespace Foo {`的多个文件(一定不要认为它们会结合到同一个`Foo`中去Multiple files that have the same `export namespace Foo {` at top-level(don't think that these are going to combine into one `Foo`!)

255
14_namespaces.md Normal file
View File

@ -0,0 +1,255 @@
# 命名空间
> **关于术语的一点说明**在TypeScript 1.5中需要注意的是其中的命名法发生了改变。为了与ECMAScript 2015的命名法保持一致"内部模块"以及被命名为“命名空间”。“外部模块”已被简化为“模块”,(名以上`module X {`与现在所指的`namespace X{`是等价的。it's important to note that in TypeScript 1.5, the nomenclature has changed. "Internal modules" are now "namespace". "External modules" are now simply "modules", as to align with ECMAScript 2015's terminology, (namely that `module X {` is equivalent to the now-preferred `namespace X{`))。
## 简介
本文指出了TypeScript中使用命名空间也就是先前的“内部模块”来组织代码的不同方法。正如在有关命名法的注释中所暗示的现已使用“命名空间”来指代“内部模块”了。此外在将`module`关键字用于声明某个内部模块时,都可以且应当使用`namespace`关键字。这样做可避免由相似命名的用词带来的负担,而令到新用户迷惑。
## 第一步First Steps
这里以一个将贯穿本章节的示例开始。因为可能要对用户在web页中表单上的输入或对某个外部提供的数据文件的格式进行检查前面在模块章节曾编写了一个小的简化字符串验证器合集。
**单个文件中的验证器**
```typescript
interface StringValidator {
isAcceptable (s: string): boolean;
}
let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;
class LettersOnlyValidator implements StringValidator {
isAcceptable (s: string) {
return lettersRegexp.test(s);
}
}
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
// 一些尝试样本
let strings = ["Hello", "98052", "101"];
// 要使用的验证器
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
}
}
```
## 命名空间化Namespacing
随着更多验证器的加入就将想有着某种组织方案从而可对类型加以跟踪并不必担心与其它对象的名称冲突。可将这些对象封装到命名空间以取代将很多不同名称放到全局命名空间的落后方式As we add more validators, we're going to want to have some kind of organization scheme so that we can keep track of our types and not worry about name collisions with other objects. Instead of putting lots of different names into the global namespace, let's wrap up our objects into a namespace
在本例中,将把所有验证器相关的实体,移入到一个叫做`Validation`的命名空间中。因为想要这里的接口与类对外部可见,所以要使用`export`来为它们建立索引。反过来,变量`lettersRegexp`与`numberRegexp`则是实现细节,因此它们会保持非导出状态,且对命名空间外部不可见。在该文件底部的测试代码中,在命名空间外部使用这些类型时,就需要对这些类型的名称进行修饰了,比如`Validation.LettersOnlyValidator`。
**已命名空间化的验证器**
```typescript
namespace Validation {
export interface StringValidator {
isAcceptable (s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable (s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
// 一些尝试样本
let strings = ["Hello", "98052", "101"];
// 要使用的验证器
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
}
}
```
## 拆分到多个文件Splitting Across Files
随着应用日益增长,就有将代码拆分到多个文件的想法,目的是令代码易于维护。
### 多文件命名空间Multi-file namespace
下面将把上面的命名空间`Validation`拆分到许多文件Here, we'll split our `Validation` namespace across many files。尽管这些文件是分立的它们却都能贡献到同一命名空间且可以像是定义在一处那样被消费。因为文件之间存在依赖所以将添加 **参考标志**reference tags来告诉编译器文件之间的关系。此外测试代码并没有改动。
*Validation.ts*
```typescript
namespace Validation {
export interface StringValidator {
isAcceptable (s: string): boolean;
}
}
```
*LettersOnlyValidator.ts*
```typescript
/// <reference path="Validation.ts">
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable (s: string) {
return lettersRegexp.test(s);
}
}
}
```
*ZipCodeValidator.ts*
```typescript
/// <reference path="Validation.ts">
namespace Validation {
const numberRegexp = /^[0-9]+$/;
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
```
*Test.ts*
```typescript
/// <reference path="Validation.ts">
/// <reference path="LettersOnlyValidator.ts">
/// <reference path="ZipCodeValidator.ts">
// 一些尝试样本
let strings = ["Hello", "98052", "101"];
// 要使用的验证器
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
}
}
```
在涉及到多个文件时,就需要确保所有已编译的代码得到加载。而确保所有已编译代码得到加载的方式,有两种。
第一种可使用级联输出concatenated output。就是使用`--outFile`编译选项,来将所有输入文件,编译到一个单独的输出文件中。
```bash
tsc --outFile sample.js Test.ts
```
基于这些文件中所出现的参考标志,编译器将自动对输出文件进行排序。还可以对各个文件进行单独指定:
```bash
tsc --outFile sample.js Validation.ts LettersOnlyValidator.ts ZipCodeValidator.ts Test.ts
```
第二种方式就是各个文件的单独编译这是默认的做法从而为每个输入文件都生成一个JavaScript文件。在产生了多个JS文件后就需要在网页上使用`<script>`
*MyTestPage.html片段*
```html
<script src="Validation.js" type="text/javascript" />
<script src="LettersOnlyValidarot.js" type="text/javascript" />
<script src="ZipCodeValidator.js" type="text/javascript" />
<script src="Test.js" type="text/javascript" />
```
## 别名Alias
另一种可简化命名空间使用的方式,就是使用`import q = x.y.z`来为一些常用对象创建较短名称Another way that you can simplify working with of namespaces is to use `import q = x.y.z` to create shorter names for commonly-used objects。请将此种语法不要与用于加载模块的`import x = require("name")`语法搞混此种语法只是简单地创建一个指定符号的别名。对于任何种类的标识符包括模块导入项所建立的对象都可以使用这类的导入通常被称作别名You can use these sorts of imports(commonly referred to as aliases) for any kind of identifier, including objects created from module imports
```typescript
namespace Shapes {
export namespace Polygons {
export class Triangle {}
export class Square {}
}
}
import polygons = Shapes.Polygons;
let sq = new polygons.Square(); // 与 `new Shapes.Polygons.Square()` 效果一样
```
注意这里没有使用`require`关键字;而是直接从所导入的符号的合格名称进行赋值。这就与使用`var`关键字类似,但也在所导入的符号的类型与命名空间涵义上有效。重要的是,对于数值来说,`import`则相对于原始符号,是不同的引用,因此对别名化的`var`的修改将不会反映到原始变量instead we assign directly from the qualified name of the symbol we're importing. This is similar to using `var`, but also works on the type and namespace meanings of the imported symbol. Importantly, for values, `import` is a distinct reference from the original symbol, so changes to an aliased `var` will not be reflected in the original variable
## 与其它JavaScript库的交互Working with Other JavaScript Libraries
为对那些不是以TypeScript编写的库的外形进行描述需要声明该库所暴露出的API。因为大多数的JavaScript库都仅会暴露少数几个的顶级对象所以命名空间是一种对这些库进行表示的好方式To describe the shape of libraries not written in TypeScript, we need to declare the API that the library exposes. Because most JavaScript libraries expose only a few top-level objects, namespaces are a good way to represent them
对于那些没有对实现进行定义的声明,这里将其成为“外围”声明。外围声明通常都是定义在`.d.ts`文件中的。如对C/C++较为熟悉,那么可将这些`.d.ts`文件,看作是`.h`文件。下面是一些示例。
### 外围命名空间Ambient Namespaces
流行库[D3](https://d3js.org/)就是在叫做`d3`的全局对象中定义其功能的。因为该库是通过`<script>`使TypeScript使an ambient namespace declaration
*D3.d.ts (简化摘抄)*
```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 var d3: D3.Base;
```

View File

@ -0,0 +1,100 @@
# 命名空间与模块
> **关于术语的一点说明**在TypeScript 1.5中需要注意的是其中的命名法发生了改变。为了与ECMAScript 2015的命名法保持一致"内部模块"以及被命名为“命名空间”。“外部模块”已被简化为“模块”,(名以上`module X {`与现在所指的`namespace X{`是等价的。it's important to note that in TypeScript 1.5, the nomenclature has changed. "Internal modules" are now "namespace". "External modules" are now simply "modules", as to align with ECMAScript 2015's terminology, (namely that `module X {` is equivalent to the now-preferred `namespace X{`))。
## 简介
本文对TypeScript中使用命名空间与模块进行代码组织的多种方式进行了说明。还将对如何使用命名空间与模块中的一些复杂问题进行分析并就TypeScript中它们的使用上的一些常见陷阱进行排除。
关于模块与命名空间的更多信息,请分别参阅[模块](13_moduels.md)与[命名空间](14_namespaces.md)章节。
## 使用命名空间Using Namespaces
简单来说命名空间就是全局命名空间中的一些命名的JavaScript对象。这就令到命名空间成其为一种可用的、非常简单的解构Namespaces are simply named JavaScript objects in the global namespace. This makes namespaces a very simple construct to use。它们可以跨越多个文件并可通过使用`--outFile`编译选项进行级联。对于Web应用中代码的结构化就是将所有依赖都作为HTML页面中`<script>`
与所有全局命名空间污染一样,命名空间的使用仍有着难于识别组件依赖的问题,尤其实在大型应用中。
## 使用模块Using Modules
与命名空间一样,模块也可包含代码与声明。主要的不同就是模块 *声明* 了它们的依赖。
模块同样有着模块加载器的依赖比如CommonJS/Require.jsModules also have a dependency on a module loader(such as CommonJS/Require.js)。对于小型JS应用这可能不是最优的但对于较大型应用却可受益于长期的模块化与可维护性所带来的开销。模块提供了更佳的代码重用、更强的隔离与更好的捆绑工具支持Modules provide for better code reuse, stronger isolation and better tooling support for bundling
值得指出的是对于Node.js应用模块是组织代码的默认及推荐方法。
从ECMAScript 2015开始模块已经是该语言的原生部分同时应被所有兼容殷勤实现所支持。因此模块应作为新项目的推荐代码组织机制。
## 命名空间与模块的一些陷阱Pitfalls of Namespaces and Modules
本小节将对在使用命名空间与模块中的一些常见陷阱,以及如何避免它们进行描述。
### `/// <reference>-ing a moudle` (使用`///` 语法对模块的引用)
一个常见错误,就是尝试使用`/// <reference ... />`语法,而不是使用`import`语句,来对模块文件进行引用。为对这两种语法进行区分,首先需要搞清楚编译器是如何根据某个`import`的路径(比如`import x from "...";`中的`...`),来定位某个模块的类型信息的。
编译器将尝试在应用路径下找到一个`.ts`、`.tsx`及随后的`.d.ts`文件。如找不到特定文件,那么编译器将查找某个 *外围模块声明*an *ambient module declaration*)。回顾一下就知道这些(外围模块声明)需在某个`.d.ts`文件中进行定义。
+ `myModule.d.ts`
```typescript
// 应在某个`.d.ts`文件,或一个非模块的`.ts`文件中
declare module "SomeModule" {
export function fn(): string;
}
```
+ `myOtherModule.ts`
```typescript
/// <reference path="myModule.d.ts" />
import * as m from "SomeModule";
```
这里用到的引用标签允许对包含了外围模块声明的声明文件进行定位。这就是多个TypeScript示例所使用的`node.d.ts`被消费的方式The reference tag here allows us to locate the declaration file that contains the declaration for the ambient module. This is how the `node.d.ts` file that several of the TypeScript samples use is consumed
### 不必要的命名空间化Needless Namespacing
如正将某个程序从命名空间式转换为模块式,就很容易得到一个像是下面的程序:
+ `shapes.ts`
```typescript
export namespace Shapes {
export class Triangle { /* ... */ }
export class Square { /* ... */ }
}
```
这里的顶层模块`Shapes`毫无理由的封装了`Triange`与`Square`。这样做对于模块消费者来说,是令人困惑与烦人的。
+ `shapeConsumer.ts`
```typescript
import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes, 真的吗?
```
TypeScript中模块的关键特性就是连个不同的模块绝不会将其名称放在同一作用域中。因为是由模块的消费者来决定将什么药的名称指派模块所以不需要主动地将所导出的符号封装到某个命名空间中A key feature of modules in TypeScript is that two different modules will never contribute names to the same scope. Because the consumer of a module decides what name to assign it, there's no need to proactively wrap up the exported symbols in a namespace
为对为何不应对模块内容进行命名空间化操作进行重申,要知道命名空间化的一般构思,就是为了提供结构体的逻辑分组,以及防止名称上的冲突。因为模块文件本身就已经是逻辑分组的了,同时其顶层名称也已经通过其导入代码进行了定义,因此为已导出对象而使用一个额外模块层就是不必要的了。
下面是一个修订后的示例:
+ `shapes.ts`
```typescript
export class Triangle { /* ... */ }
export class Square { /* ... */ }
```
+ `shapeConsumer.ts`
```typescript
import * as shapes from "./shapes";
let t = new shapes.Triangle();
```
## 模块的权衡Trade-offs of Modules
JS文件与模块之间有着一一对应关系与之一样TypeScript在模块源文件和它们所生成的文件之间也有着一一对应关系。这一特征的一个影响就是无法将多个依据目标模块系统的模块源文件级联起来One effect of this is that it's not possible to concatenate multiple source files depending on the module system you target。比如就在目标模块系统为`commonjs`或`umd`时,无法使用`--outFile`编译选项但在TypeScript 1.8之后的版本中,在以`amd`或`system`为目标模块系统时,使用`--outFile`选项是可能的。

468
16_module_resolution.md Normal file
View File

@ -0,0 +1,468 @@
# 模块解析
**Module Resolution**
> 本章节假定已对模块的一些基本知识有所掌握。请参阅[模块](13_modules.md)了解更多信息。
*模块解析* 是编译器用于弄清导入项引用的是什么的过程( *Module resolution* is the process the compiler uses to figure out what an import refers to。请想想`import { a } from "moduleA"`这样的导入语句;为了对`a`的所有使用进行检查,编译器就需要准确地知道其代表的是什么,且将对其定义`moduleA`进行检查。
此刻,编译器就会询问“`moduleA`的外形是什么?”,尽管这听起来很直截,`moduleA`则可能是定义在某个`.ts/.tsx`文件中,或这在代码所依赖的某个`.d.ts`文件中。
首先编译器将尝试找到代表所导入模块的文件。编译器依据两种不同策略之一完成这一步经典方式或节点方式First, the compiler will try to locate a file that represents the imported module. To do so the compiler follows one of two different strategies: Classic or Node。这两种策略告诉编译器去 *哪里* 查找`moduleA`。
如两种策略都不奏效且模块名称是非相对的(在`moduleA`这种情况下模块名称就是相对的那么编译器将尝试定位一个外围模块声明ambient module declaration。接下来会对非相对导入项进行讨论。
最后,在编译器无法对模块进行解析是,就会记录一个错误。在这种情况下,错误将为像是`error TS2307: Cannot find module 'moduleA'.`这样的。
### 相对与非相对模块导入项Relative vs. Non-relative module imports
根据模块引用为相对或非相对的不同模块导入项的解析会有所不同Module imports are resolved differently based on whether the module reference is relative or Non-relative
*相对导入项* 就是以 `/`、`./`或`../`开头的导入项。下面是一些示例:
+ `import Entry from "./components/Entry";`
+ `import { DefaultHeaders } from "../constants/http";`
+ `import "/mod";`
除此之外所有其它导入都被认为是 **非相对** 的。下面是一些示例:
+ `import * as $ from "jquery";`
+ `import { Component } from "@angular/core";`
相对导入项被解析为相对于导入文件,并 *无法* 解析为外围模块声明。对于自己的可在运行时保证其相对位置维护的模块,可使用相对导入项( You should use relative imports for your own modules that are guaranteed to maintain their relative location at runtime
非相对导入则可被解析为相对于`baseUrl`或通过下面将讲到的路径映射A non-relative import can be resolved relative to `baseUrl`, or through path mapping, which we'll cover below。非相对导入项也可解析到外围模块声明。在导入所有外部依赖时都要使用非相对路径Use non-relative paths when importing any of your external dependencies
### 模块解析策略Module Resolution Strategies
模块解析策略有两种:节点策略与经典策略。可使用`--moduleResoluton`选项来指定模块解析策略。在没有指定时,对于`--module AMD | System | ES2015`的默认策略是经典策略,对其它模块,默认策略是节点策略。
### 经典策略Classic
该模块解析策略曾是TypeScript的默认解析策略。如今该策略主要是为向后兼容性而保留。
相对导入项将被解析为相对于导入文件。因此源文件`/root/src/folder/A.ts`中的`import { b } from "./moduleB"`将导致以下查找:
1. `/root/src/folder/moduleB.ts`
2. `/root/src/folder/moduleB.d.ts`
而对于非相对导入项,编译器就唤醒以包含导入文件开始的目录树,尝试定位匹配的定义文件。
比如:
在源文件`/root/src/folder/A.ts`中到`moduleB`的一个非相对导入项,比如`import { b } from "moduleB"`,将导致尝试在下面的位置,对`moduleB`进行定位:
1. `/root/src/folder/moduleB.ts`
2. `/root/src/folder/moduleB.d.ts`
3. `/root/src/moduleB.ts`
4. `/root/src/moduleB.d.ts`
5. `/root/moduleB.ts`
6. `/root/moduleB.d.ts`
7. `/root/moduleB.ts`
8. `/root/moduleB.d.ts`
### 节点策略Node
此解析策略尝试在运行时对`Node.js`的模块解析机制进行模仿This resolution strategy attempts to mimic the Node.js module resolution mechanism at runtime。完整的Node.js解析算法在[Node.js模块文档](https://nodejs.org/api/modules.html#modules_all_together)中有说明。
*Node.js是如何解析模块的*
要理解TypeScript所跟随的脚步就要对Node.js模块有进一步了解。
传统上Node.js中的导入是通过调用一个名为`require`的函数完成的。根据给予`require`函数的是一个相对路径或绝对路径Node.js所采取的做法会有所不同。
相对路径就相当直接。比如,考虑一个包含了`var x = require("./moduleB");`,位于`/root/src/moduleA.js`的文件Node.js就会按照以下顺序对那个导入进行解析
1. 如存在名为`/root/src/moduleB.js`的文件,就询问该文件。
2. 如文件夹`/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`。
3. 询问文件夹`/root/src/moduleB`是否包含一个名为`index.js`的文件。那个文件被显式地认为是那个文件夹的`main`模块。
有关此方面的内容可参考Node.js的文档中[文件模块](https://nodejs.org/api/modules.html#modules_file_modules)与[文件夹模块](https://nodejs.org/api/modules.html#modules_folders_as_modules)的内容。
但对[非相对模块名称](https://www.typescriptlang.org/docs/handbook/module-resolution.html#relative-vs-non-relative-module-imports)的解析则是不同的。Node.js将在名为`node_modules`的特殊文件夹中查找。`node_modules`文件夹可以与当前文件在同一级别或在目录链中的更高级别。Node.js将唤醒该目录链将各个`node_modules`找个遍,直到找到尝试载入的模块为止。
接着上面的示例,试想在`/root/src/moduleA.js`使用了非相对路径并有着`var x = require("moduleB");`。那么Node就会尝试将`moduleB`解析到下面这些位置,知道某个位置工作。
1. `/root/src/node_modules/moduleB.js`
2. `/root/src/node_modules/moduleB/package.json` (在该文件指明了一个`main`属性时)
3. `/root/src/node_modules/moduleB/index.js`
4. `/root/node_modules/moduleB.js`
5. `/root/node_modules/moduleB/package.json` (在该文件指明了一个`main`属性时)
6. `/root/node_modules/moduleB/index.js`
7. `/node_modules/moduleB.js`
8. `/node_modules/moduleB/package.json` (在该文件指明了一个`main`属性时)
9. `/node_modules/moduleB/index.js`
请注意在第4及7步Node.js都往上跳了一个目录。
可从Node.js文档中有关[从`node_modules`加载模块](https://nodejs.org/api/modules.html#modules_loading_from_node_modules_folders)部分,了解更多此过程的信息。
## TypeScript解析模块的方式How TypeScript resolves modules
为了在编译时对模块的定义文件进行定位TypeScript将模仿Node.js的运行时解析策略the Node.js run-time resolution strategy。为达到此目的TypeScript以TypeScript源文件扩展名`.ts`、`.tsx`及`.d.ts`来覆盖Node的解析逻辑。TypeScript也将使用`package.json`中的一个名为`"types"`的字段来反映Node.js中`"main"`的目的 -- 编译器将使用`"types"`字段来找到需要参考的“main”定义文件To accomplish this, TypeScript overlays the TypeScript source file extensions(`.ts`, `.tsx`, and `.d.ts`) over the Node's resolution logic. TypeScript will also use a field in `package.json` names `"types"` to mirror the purpose of `"main"` -- the compiler will use it to find the "main" definition file to consult
比如,一个`/root/src/moduleA.ts`中像`import { b } from "./moduleB";`的导入语句,将导致编译器尝试在一下位置对`"./moduleB"`进行定位:
1. `/root/src/moduleB.ts`
2. `/root/src/moduleB.tsx`
3. `/root/src/moduleB.d.ts`
4. `/root/src/moduleB/package.json` (如`package.json`中指明了`types`属性)
5. `/root/src/moduleB/index.ts`
6. `/root/src/moduleB/index.tsx`
7. `/root/src/moduleB/index.d.ts`
回想上面Node.js就是先查找名为`moduleB.js`的文件,再查找一个应用的`package.json`,随后再查找一个`index.js`的。
与此类似对于非相对导入项也会依循Node.js的解析逻辑首先查找文件再查找应用文件夹an application folder。因此源文件`/root/src/moduleA.ts`中的`import { b } from "moduleB";`将导致下面的查找:
1. `/root/src/node_modules/moduleB.ts`
2. `/root/src/node_modules/moduleB.tsx`
3. `/root/src/node_modules/moduleB.d.ts`
4. `/root/src/node_modules/moduleB/package.json` (在其指明了`types`属性时)
5. `/root/src/node_modules/moduleB/index.ts`
6. `/root/src/node_modules/moduleB/index.tsx`
7. `/root/src/node_modules/moduleB/index.d.ts`
8. `/root/node_modules/moduleB.ts`
9. `/root/node_modules/moduleB.tsx`
10. `/root/node_modules/moduleB.d.ts`
11. `/root/node_modules/moduleB/package.json` (在其指明了`types`属性时)
12. `/root/node_modules/moduleB/index.ts`
13. `/root/node_modules/moduleB/index.tsx`
14. `/root/node_modules/moduleB/index.d.ts`
15. `/node_modules/moduleB.ts`
16. `/node_modules/moduleB.tsx`
17. `/node_modules/moduleB.d.ts`
18. `/node_modules/moduleB/package.json` (在其指明了`types`属性时)
19. `/node_modules/moduleB/index.ts`
20. `/node_modules/moduleB/index.tsx`
21. `/node_modules/moduleB/index.d.ts`
不要被这里的步数吓到 -- TypeScript仍只是在第8和15步处两次网上跳了一个目录而已。这与Node.js所做的也并没有更复杂。
### 额外的模块解析开关Additional module resolution flags
在有的时候项目源代码布局并不与输出所匹配。通常有一套的构建步骤来生成最终结果A project source layout sometimes does not match that of the output. Usually a set of build steps result in generating the final output。这些步骤包括将`.ts`文件编译为`.js`文件,以及将不同源代码位置的依赖,拷贝到单个的输出位置。最终结果就是运行时的模块,可能有着与包含这些模块定义的源文件所不同的名称。或者最后输出中的模块路径,可能与编译时这些模块所对应的源文件路径不一致。
TypeScript编译器有着一套额外选项*告知* 编译器为了生成最终输出而期望对源程序进行的一些调整The TypeScript compiler has a set of additional flags to *inform* the compiler of transformations that expected to happen to the sources to generate the final output
比如`baseUrl`的设置,就可告诉编译器在何处去找到模块。所有非相对名称的模块导入项,都被假定相对于`baseUrl`。
*baseUrl*的值,取决于以下两个因素:
+ `baseUrl`值的命令行参数(如给出的路径为相对路径,那么`baseUrl`的值就根据当前路径计算得出)
+ 'tsconfig.json'中`baseUrl`属性的值(如果该属性值为相对的,那么`baseUrl`的值就根据'tsconfg.json'的位置计算得出)
注意相对模块导入项是不受baseUrl设置的影响的因为因为相对模块导入项总是被解析到相对于它们的导入文件。
有关baseUrl的更多信息请参考[RequireJS](http://requirejs.org/docs/api.html#config-baseUrl)及[SystemJS](https://github.com/systemjs/systemjs/blob/master/docs/config-api.md#baseurl)的文档。
### 路径映射Path mapping
模块有的时候并不是直接位于 *baseUrl* 下。比如,到模块`jquery`的导入项,就会在运行时被翻译到`node_modules/jquery/dist/jquery.slim.min.js`。加载器使用映射配置a mapping configuration以在运行时将模块名称映射到文件请参阅[RequireJS 文档](http://requirejs.org/docs/api.html#config-paths)与[SystemJS 文档](https://github.com/systemjs/systemjs/blob/master/docs/config-api.md#paths)。
TypeScript编译器通过在`tsconfig.json`文件中使用`paths`属性来支持此类映射的声明The TypeScript compiler supports the declaration of such mappings using `paths` in `tsconfig.json` files。下面是一个如何为`jquery`指定`paths`属性的示例:
```json
{
"compilerOptions": {
"baseUrl": ".", // 如指定了"paths", 那么就必须指定"baseUrl"
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"] // 该映射是相对于baseUrl的
}
}
}
```
请注意`"paths"`是被解析到相对于`"baseUrl"`的。在将`"baseUrl"`设置为非`"."`时,比如`tsconfig.json`的目录时,映射也必须进行相应修改。也就是说,在将上面的示例设置为`"baseUrl": "./src"`后jquery就应被映射到`"../node_modules/jquery/dist/jquery"`。
`"paths"`的使用可实现包括设置多个错误回退位置特性等的较复杂映射机制Using `"paths"` also allows for more sophisticated mappings including multiple fall back locations。设想某个项目的配置中在一个位置仅有部分模块是可用的其余则是在另一处的情形。构建步骤就会将这些不同位置的模块放在一个地方。该项目布局看起来像这样
```bash
projectRoot
├── folder1
│ ├── file1.ts (imports 'folder1/file2' and 'folder2/file3')
│ └── file2.ts
├── generated
│ ├── folder1
│ └── folder2
│ └── file3.ts
└── tsconfig.json
```
那么相应的`tsconfig.json`就应像这样了:
```json
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"*": [
"*",
"generated/*"
]
}
}
}
```
这样做就告诉编译器,对于所有与模式`"*"`(也就是所有值)匹配的模块导入项,要在两个地方进行查找:
1. `"*"` 意指未改变的同一名称,因此映射为`<moduleName> => <baseUrl>/<moduleName>`
2. `"generated/*"` 意指带有追加了前缀"generated"的模块名称,因此映射为 `<moduleName> => <baseUrl>/generated/<moduleName>`
那么按照此逻辑,编译器将如下对这两个导入项进行解析:
+ 对于导入项'folder1/file2':
1. 匹配了模式`'*'`,同时通配符捕获到整个名称
2. 尝试清单中的第一个代换substitution`'*'`, 从而得到 `folder1/file2`
3. 代换结果为非相对名称 -- 将其与 *baseUrl* 结合,得到 `projectRoot/folder1/file2.ts`
4. 该文件存在。解析成功。
+ 对于导入项'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`的虚拟目录Virtual Directories with `rootDirs`
有时编译时多个目录的全部项目源码都要被结合在一起从而生成一个单一的输出目录。这种做法可被视为由一个源代码目录集合创建出一个“虚拟”目录This can be viewed as a set of source directories create a "virtual" directory
通过使用`'rootDirs'`选项(在`tsconfig.json`中),就可以告知编译器组成该“虚拟”路径的 “roots”根目录而因此编译器就可以在这些“根目录”中 *像是* 在单个目录中融合在一起那样对这些相对模块导入项进行解析了Using `"rootDirs"`, you can inform the compiler of the *roots* making up this "virtual" directory; and thus the compiler can resolve relative modules imports within these "virtual" directories *as if* were merged together in one directory
试想下面的项目解构作为示例:
```bash
src
└── views
└── view1.ts (imports './template1')
└── view2.ts
generated
└── templates
└── views
└── template1.ts (imports './view2')
```
`src/views`中的文件是一些UI控件的用户代码。`generated/templates`中的文件则是由模板生成器自动生成的、作为构建一部分的UI模板绑定代码。构建的一步将把`/src/views`与`/generated/templates/views`中的文件进行拷贝到输出中的同一目录。而在运行时,某个视图就可以期望它的模板是存在于它旁边的,并因此而可以使用一个如同`"./template"`这样的相对名称对模板进行导入A build step will copy the files in `/src/views` and `/generated/templates/views` to the same directory in the output. At run-time, a view can expect its template to exist next to ti, and thus should import it using a relative name as `"./template"`)。
要将这种关系指明给编译器,就使用`"rootDirs"`选项。该选项指明一个 *根目录roots* 清单,其中的内容希望在运行时进行融合。因此根据这里的示例,其`tsconfig.json`文件就应该像下面这样:
```json
{
"compilerOptions": {
"rootDirs": [
"src/views",
"generated/templates/views"
]
}
}
```
随后编译器一旦见到`rootDirs`清单中任意条目的子目录中的相对模块导入项,其就会尝试在`rootDirs`的各个条目中查找该导入项。
`rootDirs`的灵活性不仅仅在于指明要被逻辑融合的一个物理源码目录清单。所提供的数组可包含任意数目的特定条目、任意的目录名称而不管这些目录是否存在。这就令到编译器可对复杂捆绑与诸如条件包含及特定于项目的加载器插件等运行时特性以类型安全的方式进行捕获The flexibility of `rootDirs` is not limited to specifying a list of physical source directories that are logically merged. The supplied array may include any number of ad hoc, arbitary directory names, regardless of whether they exist or not. This allows the compiler to capture sophisticated bundling and runtime features such as conditional inclusion and project specified loader plugins in a type safe way
试想这样一个国际化场景,其中通过以相对模块路径的一部分,比如`./#{locale}/messages`,而插入一个特殊令牌,比如`#{locale}`构建工具从而自动生成特定语言环境程序包Consider an internationalization scenario where a build tool automatically generates locale specific bundles by interpolating a special path token, say `#{locale}` as part of a relative module path such as `./#{locale}/messages`)。在这种假定设置下,构建工具将对所支持的语言环境进行枚举,而映射到抽象路径`./zh/messages`、`./de/messages`等等。
假设这些语言模块都导出了一个字符串数组。比如`./zh/messages`可能包含:
```typescript
export default [
"您好吗",
"很高兴认识你"
];
```
利用`rootDirs`,就可以告诉编译器这种映射,从而安全地对`./#{locale}/messages`进行解析,尽管该目录根本不会存在。以下面的`tsconfig.json`文件为例:
```json
{
"compilerOptions": {
"rootDirs": [
"src/zh",
"src/de",
"src/#{locale}"
]
}
}
```
现在编译器将会以工具目的,把`import messages from './#{locale}/messages'` 解析到 `import messages from './zh/messages'`从而允许在语言环境不可知下的开发不受设计时间支持的威胁allowing development in a locale agnostic manner without compromising design time support
### 对模块解析进行追踪Tracing module resolution
正如前面所讲到的,在对模块进行解析时,编译器可访问位处当前文件夹外部的文件。这会导致难于诊断某个模块不能解析,或被解析到不正确的定义的原因。而通过使用`--traceResolution`开启编译器模块解析追踪the compiler module resolution tracing特性就能提供到在模块解析过程中发生了什么的信息。
假设有着一个使用了`typescript`模块的示例应用。`app.ts`具有像是`import * as ts from "typescript"`这样的导入项。
```bash
│ tsconfig.json
├───node_modules
│ └───typescript
│ └───lib
│ typescript.d.ts
└───src
app.ts
```
以`--traceResolution`选项来调用编译器
```bash
tsc --traceResolution
```
将得到如下的输出:
```bash
======== 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'. ========
```
***要查找的项目***
+ 导入项的名称与位置
> ======== Resolving module 'typescript' from 'src/app.ts'. ========
+ 编译器依循的策略
> Module resolution kind is not specified, using 'NodeJs'.
+ 来自npm软件包的加载类型Loading of types from npm packages
> 'package.json' has 'types' field './lib/typescript.d.ts' that references 'node_modules/typescript/lib/typescript.d.ts'.
+ 最终结果
> ======== Module name 'typescript' was successfully resolved to 'node_modules/typescript/lib/typescript.d.ts'. ========
### `--noResolve`选项的使用Using `--noResolve`
通常编译器在开始编译过程前,会先尝试对所有模块导入项进行解析。在每次成功地将一个`import`解析到一个文件后,该文件就被加入到于稍后将进行处理的一个文件集合中。
编译器选项`--noResolve`通知编译器不要将那些未在命令行传递的文件,“添加” 到编译过程。编译器仍会尝试将模块解析到文件但如该文件未被指定其就不会被包含进去The `--noResolve` compiler options instructs the compiler not to "add" any files to the compilation that were not passed on the command line. It will still try to resolve the module to files, but if the file is not specified, it will not be included
举例来说:
*app.ts*
```typescript
import * as A from "moduleA" // 没有问题moduleA在命令行上有传入
import * as B from "moduleB" // Error TS2307: Cannot find module 'moduleB'.
```
```bash
tsc app.ts moduleA.ts --noResolve
```
使用`--noResolve`选项来编译`app.ts`将导致:
+ 因为`moduleA`有在命令行上传入,其被正确地找到
+ 而因为`moduleB`未被传入,故因无法找到`moduleB`而报错
### 常见问题Common Questions
***为何一个排除清单中的模块,仍被编译器拾取到了?***
***Why does a module in the exclude list still get picked up by the compiler?***
`tsconfig.json`文件可将文件夹转变为一个“项目”(`tsconfig.json` turns a folder into a "project")。在没有指定任何`exclude`或`include`条目时,包含了`tsconfig.json`的文件夹中的所有文件,及该文件夹的所有子目录,都是包含在编译中的。如打算使用`"exclude"`来排除某些文件,还不如通过使用`files`来指定所需的文件,从而让编译器来查找这些文件。
那就是`tsconfig.json`的自动包含特性。拿不会嵌入上面所讨论的模块解析。在编译器识别到作为某个模块导入项的目标文件时该文件将自动包含到编译中而不管其是否被排除在前面的步骤That was `tsconfig.json` automatic inclusion. That does not embed module resolution as discussed above. If the compiler identified a file as a target of a module import, it will be included in the compilation regardless if it was excluded in the previous steps
所以要将某个文件排除在编译之外,就需要将其与 **所有** 有着到它的`import` 或 `/// <reference path="...">`指令的文件,都要排除。

296
17_declaration_merging.md Normal file
View File

@ -0,0 +1,296 @@
# 声明融合特性
**Declaration Merging**
## 简介
TypeScript中有一些特有概念它们在类型级别对JavaScript对象外形进行描述Some of the unique concepts in TypeScript describe the shape of JavaScript objects at the type level。一个尤其特定于TypeScript的例子就是“声明融合”这一概念。对此概念的掌握对于与现有Javascript操作较有优势。对此概念的掌握也开启了其它复杂抽象概念的大门。
作为本文的目标,“声明融合”特性,就是指编译器把两个以相同名称进行声明的单独声明,融合为一个单一声明。融合后的声明,有着原先两个声明的特性。任意数目的声明都可被融合;而不受限于仅两个声明。
## 基本概念
在TypeScript中一个声明将创建出至少三组之一的实体命名空间、类型或值。命名空间创建式声明创建出包含可通过使用 *点缀符号* 进行访问的名称的命名空间。类型创建式声明则仅完成这些它们创建出一个对所声明的外形可见且绑定到给定名称的类型。最后值创建式声明创建的是在输出的JavaScript中可见的数值In TypeScript, a declaration creates entities in at least one of three groups: namespace, type, or value. Namespace-creating declarations create a namespace, which contains names that are accessed using a dotted notation. Type-creating declarations do just that: they create a type that is visible with the declared shape and bound to the given name. Lastly, value-creating declarations create values that are visible in the output JavaScript
| 声明类型 | 命名空间 | 类型 | 数值 |
| :--- | :--: | :--: | :--: |
| 命名空间 | X | | X |
| 类 | | X | X |
| 枚举 | | X | X |
| 接口 | | X | |
| 类型别名 | | X | |
| 函数 | | | X |
| 变量 | | | X |
对各种声明都创建了什么的掌握,有助于理解哪些在执行声明融合时被融合了。
## 接口的融合Merging Interfaces
最简单也是最常见的声明融合类别,要数接口融合。在最基础阶段,这种融合机械地将两个声明的成员,以那个相同的名称结合起来。
```typescript
interface Box {
height: number;
width: number;
}
interface Box {
scale: number;
}
let box: Box = {height: 5, width: 6, scale: 10};
```
这些接口的非函数成员都应唯一。如出现重复,那么重复的成员应具有相同类型。在接口声明了有相同名称,类型却不一样的非函数成员时,编译器将发出错误。
对于接口中的函数成员名称相同的各个函数是以对同一函数的过载进行对待的For function members, each function member of the same name is treated as describing an overload of the same function。需要说明的是在接口`A`与其后的接口`A`融合的情况下,那么第二个接口比第一个接口有着较高的优先权。
那就是说,在下面的示例中:
```typescript
interface Cloner {
clone(animal: Animal): Animal;
}
interface Cloner {
clone(animal: Sheep): Sheep;
}
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
}
```
这三个接口将融合为创建一个下面的单一的接口:
```typescript
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
```
注意每个分组的元素保持了同样顺序而各分组本身则以较后过载集合靠前的顺序被融合的Notice that the elements of each group maintains the same order, but the groups themselves are merged with later overload sets ordered first
这条规则的一个例外就是特殊签名specialized signatures。在某个签名具有类型为 *单一* 字符串字面值类型就是说不是字符串字面值的联合的参数时那么该函数将被提升到其融合过载清单的顶部If a signature has a parameter whose type is a *single* string literal type(e.g. not a union of string literals), then it will be bubbled toward the top of its merged overload list
举例来说,下面这些接口将融合到一起:
```typescript
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`将是下面这样:
```typescript
interface Document {
createElement(tagName: "canvas"): HTMLCanvasElement;
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}
```
## 命名空间的融合Merging Namespaces
与接口类似,相同名称的命名空间也将对其成员进行融合。因为命名空间同时创建了命名空间与值,所以需要掌握命名空间与值二者是如何进行融合的。
为对命名空间进行融合来自在各个命名空间中定义的导出接口的类型定义自身被融合从而形成一个单一的内部有着这些接口定义的命名空间To merge the namespaces, type definitions from exported interfaces declared in each namespaces are themselves merged, forming a single namespace with merged interface definitions inside
为对命名空间值进行融合那么在各声明处如已存在有着给定名称的命名空间那么其就被通过以取得既有命名空间并将第二个命名空间所导出的成员加入到前一个的方式被进一步扩展To merge the namespace value, at each declaration site, if a namespace already exists with the given name, it is further extended by taking the existing namespace and adding the exported members of the second namespace to the first
看看下面这个示例中`Animals`的声明融合:
```typescript
namespace Animals {
export class Zebra {}
}
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Dog {}
}
```
其等价于:
```typescript
namespace Animals {
export interface Legged { numberOfLegs: number; }
export class Zebra {}
export class Dog {}
}
```
这种命名空间融合模式作为起点是有用的,但也需要掌握对于非导出成员,是怎样进行融合的。 非导出成员仅在原始(未融合的)命名空间中可见。这就意味着在融合之后,来自其它声明的已融合成员,是无法看到那些非融合成员的。
在下面的示例中,可更清楚的看到这一点:
```typescript
namespace Animal {
let haveMuscles = true;
export function animalsHaveMuscles () {
return haveMuscles;
}
}
namespace Animal {
export function doAnimalsHaveMuscles () {
return haveMuscles; // <-- 错误`haveMuscles` 在这里不可见
}
}
```
因为`haveMuscles`未被导出,所以只有共享了同一未融合命名空间的`animalsHaveMuscles`函数才能看到该符号the symbol。而对于`doAnimalsHaveMuscles`函数,尽管其是融合后的`Animal`命名空间的一部分,其仍然不能看到这个未导出成员。
## 命名空间与类、函数及枚举的融合Merging Namespaces with Classes, Functions, and Enums
因为命名空间有足够的灵活性,故其可与其它类型的声明进行融合。而要与其它类型的声明融合,命名空间就 **必须** 位于要与其融合的声明之后。融合得到的声明将有着所融合声明类型的各自属性。TypeScript利用这种功能来模仿JavaScript及其它编程语言的某些模式The resulting declaration has properties of both declaration types. TypeScript uses this capability to model some of the patterns in JavaScript as well as other programming languages
### 命名空间与类的融合Merging Namespaces with Classes
这么做给出了一种描述内层类的方式:
```typescript
class Album {
label: Album.AlbumLabel;
}
namespace Album {
export class AlbumLabel {}
}
```
被融合成员的可见性规则与“命名空间融合”小节中所讲到的相同,因此为让该融合的类`AlbumLabel`可见就必须将其导出。融合结果就是在另一个类中进行管理的一个类。还可以使用命名空间来将更多静态成员加入到既有的类中The end result is a class managed inside of another class. You can also use namespaces to add more static members to an existing class
出了内层类inner classes这种模式之外还有那种建立一个函数并于随后通过往函数上加入属性来进一步扩展函数的JavaScript做法。TypeScript是通过使用声明融合来以类型安全的方式构造类似定义的。
```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"));
```
于此类似,命名空间也可用于对带有静态成员的枚举进行扩展:
```typescript
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;
}
}
}
```
## 不允许的融合Diallowed Merges
TypeScript中并非所有融合都是允许的。目前类就被允许与其它类或变量融合。有关对类融合的模仿请参考[TypeScript中的混入](20_mixins.md)章节。
## 模块增强Module Augmentation
尽管JavaScript模块并不支持融合但可通过导入并随后对其进行更新来对既有对象进行补充Although JavaScript modules do not support merging, you can patch existing objects by importing and then updating them。看看下面这个`Observable`的示例:
```typescript
// observable.js
export class Observable<T> {
// ... 实现由读者作为练习完成 ...
}
// map.js
import { Observable } from "./observable";
Observable.prototype.map = function (f) {
// ... 作为读者的另一个练习
}
```
这种做法在TypeScript中也是可行的不过编译器并不知道`Observable.prototype.map`。这里就可以使用模块增强特性,来将其告诉编译器:
```typescript
// observable.ts 保持不变
// 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) {
// ... 留给读者的另一个练习
}
// consumer.ts
import { Observable } from "./observable";
import "./map";
let o: Observable<number>;
o.map(x => x.toFixed());
```
### 全局增强Global augmentation
亦可从某个模块内部将声明添加到全局作用域You can also add declarations to the global scope from inside a module
```typescript
// observable.ts
export class Observable<T> {
// ... 仍旧没有实现 ...
}
declare global {
interface Array<T> {
toObservable(): Observable<T>;
}
}
Array.prototype.toObservable = function () {
// ...
}
```
全局增强与模块增强有着同样的行为和限制Global augmentations have the same behavior and limits as module augmentations

386
18_jsx.md Normal file
View File

@ -0,0 +1,386 @@
# JSX
## 简介
[JSX](https://facebook.github.io/jsx/)是一种可嵌入式的、类似于XML的语法JSX is an embeddable XML-like syntax。其是为了被转换成有效的JavaScript固然那种转换的语义是特定于具体实现的。JSX的流行是由[React](https://reactjs.org/)框架的流行带来的在其它应用中也有见到JSX。TypeScript支持JSX的嵌入、类型检查及将JSX直接编译到JavaScript。
## 基本用法Basic Usage
为了使用JSX必须完成两件事
1. 以`.tsx`扩展名来命名文件
2. 开启`jsx`选项(编译器的)
TypeScript本身带有三个JSX模式`preserve`、`react`与`react-native`。这些模式仅影响生成阶段 -- 类型检查不受影响。`preserve`模式将保留JSX作为输出的部分以被其它转换步骤比如[Babel](https://babeljs.io/))进一步处理。此外该模式下的输出将有着`.jsx`文件扩展名。`react`模式将生成`React.createElement`在使用钱不需要通过JSX转换同时输出将有着`.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`是硬编码的,因此必须要有`R`开头的React。
## `as`运算符The `as` Operator
回顾一下类型断言a type assertion是怎么写的
```typescript
var foo = <foo>bar;
```
这里对变量`bar`有着类型`foo`进行了断言。因为TypeScript也将尖括号用于类型断言因此JSX的语法就引入了一些解析困难。结果就是TypeScript不允许在`.tsx`文件中出现尖括号类型断言。
为了弥补`.tsx`文件中的这个功能损失,就加入了一个新的类型断言运算符:`as`。使用`as`运算符,上面的示例就可很容易写出来。
```typescript
var foo = bar as foo;
```
在`.ts`与`.tsx`文件中,都可以使用`as`运算符,其作用与其它类型断言样式一致。
## 类型检查Type Checking
为了理解JSX下的类型检查就必须首先掌握固有元素与基于值的元素的区别In order to understand type checking with JSX, you must first understand the difference between intrinsic elements and value-based elements。对于一个JSX表达式`<expr />``expr` 既可以是对环境中固有元素比如DOM环境中的`div`或`span`)的参考,也可以是对某个创建出来的组件的参考。因为以下两个原因,这一点很重要:
1. 对于React, 固有元素生成的是字符串(`React.createElement("div")`),但对于创建出的组件则不是(`React.createElement(MyComponent)`)。
2. 传入给JSX的属性的类型应以不同的方式进行查找。固有元素属性应本质上就知道而组件将期望给它们指定一套属性The types of the attributes being passed in the JSX element should be looked up differently. Intrinsic element attributes should be known *intrinsically* whereas components will likely want to specify their own set of attributes
对于如何区分二者TypeScript使用了[与React相同的约定](http://facebook.github.io/react/docs/jsx-in-depth.html#html-tags-vs.-react-components)。固有元素总是以小写字母开头,而基于值的元素则全部以大写字母开头。
### 固有元素Intrinsic elements
固有元素是在一个特殊接口`JSX.IntrinsicElements`上查找的。默认情况下,如果没有指定该接口,那么什么都可以且固有元素不会被检查类型。但如果指定了该接口,那么固有元素的名称将作为一个属性,在`JSX.IntrinsicElements`上进行查找。比如:
```typescript
declare namespace JSX {
interface IntrinsicElements {
foo: any
}
}
<foo />; // 没有问题
<bar />; // 错误
```
在上面的示例中,`<foo />`将正确工作,但`<bar />`将因为其没有在`JSX.IntrinsicElements`上进行指明,而导致一个错误。
> 注意: 也可以像下面这样,在`JSX.IntrinsicElements`上指定一个全能字符串索引器a catch-all string indexer
```typescript
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}
```
### 基于值的元素Value-based elements
基于值的元素,是简单地通过作用域中的标识符进行查找的。
```typescript
import MyComponent from "./MyComponent";
<MyComponent />; // 没有问题
<SomeOtherComponent />; // 错误
```
基于值元素的定义有两种方式:
1. 无状态函数式组件方式Stateless Functional Component, SFC
2. 类组件方式Class Component
因为在JSX表达式中二者难于区分所以首先会尝试使用 **过载方案** 来将表达式作为无状态函数式组件进行解析Because these two types of value-based elements are indistinguishable from each other in JSX expression, we first try to resolve the expression as Stateless Functional Component using **overload resolution**。加入该过程成功那么就完成了将表达式解析为其声明。而在将其解析为SFC失败时就会尝试将其作为类组件进行解析。加入仍然失败就会报告一个错误。
***关于无状态函数式组件***
***Stateless Functional Components***
如同该名称所体现的那样,这种组件是以首个参数为`props`对象的JavaScript函数进行定义的。这里要强调其定义函数的返回值必须是可赋值给`JSX.Element`的类型
```typescript
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函数因此这里也可以使用函数过载特性function overload
```typescript
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 {
// ...
}
```
***类组件***
对类组件的限定是可行的。但为达到这个目的,就必须引入两个新术语: *元素类类型**元素示例类型* the *element class type* and the *element instance type*)。
在给定了`<Expr />`时,那么 *元素类类型* 就是 `Expr` 的类型。因此在上面的示例中,在`MyComponent`是一个ES6的类时那么类类型就应是那个类。而如果`MyComponent`是一个工厂函数a factory function那么类类型就应是那个函数。
类类型一旦建立实例类型就有该类类型的调用签名与构造签名的返回值类型联合确定下来Once the class type is established, the instance type is determined by the union of the return types of the class type's call signatures and construct signatures。因此又会出现在ES6类的情况下实例类型将会是那个类的实例的类型同时在工厂函数的情况下实例类型将是自函数返回值的类型。
```typescript
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`的使用限制到仅符合适当接口的那些类型By default `JSX.ElementClass` is `{}`, but it can be augmented to limit the use of JSX to only those types that conform to the proper interface
```typescript
declare namespace JSX {
interface ElementClass {
render: any;
}
}
class MyComponent {
render () {}
}
function MyFactoryFunction () {
return { render: () => {} }
}
<MyComponent />; // 没有问题
<MyFactoryFunction />; // 没有问题
class NotAValidComponent {}
function NotAValidFactoryFunction () {
return {};
}
<NotAValidComponent />; //错误
<NotAValidFactoryFunction />; // 错误
```
### 属性类型的检查
对属性的类型检查的第一步,就是确定 *元素属性类型* The first step to type checking attributes is to determine the *element attributes type*)。这一步对于固有元素及基于值的元素有些许的不同。
对于固有元素,元素属性类型就是`JSX.IntrinsicElements`上的属性的类型
```typescript
declare namespace JSX {
interface IntrinsicElements {
foo: { bar?: boolean }
}
}
// `foo`的元素属性类型,就是 `{bar?: boolean}`
<foo bar />;
```
对于基于值的元素,元素属性类型这个问题,就复杂一些。元素属性类型是由早前所确定下来的 *元素实例类型* 上的一个属性的类型确定的。至于要使用哪一个属性,则是由`JSX.ElementAttributesProperty`所决定的。`JSX.ElementAttributesProperty`又应该以一个单一属性进行声明。随后就会使用那个属性For value-based elements, it is a bit more complex. It is determined by the type of a property on the *element intance type* that was previously determined. Which property to use is determined by `JSX.ElementAttributesProperty`. It should be declared with a single property. The name of that property is then used
```typescript
declare namespace JSX {
interface ElementAttributesProperty {
props; // 指定要使用的属性名称
}
}
class MyComponent {
// 指定元素实例类型上的属性
props: {
foo?: string;
}
}
// `MyComponent` 的元素属性类型,就是`{foo?: string}`
<MyComponent foo="bar" />
```
元素属性类型被用于对JSX中的属性进行类型检查。支持可选与必需属性The element attribute type is used to type check the attributes in the JSX. Optional and required properties are supported
```typescript
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标识符a valid JS identifier, 比如`data-*`这样的元素属性在元素属性类型中没有找到这个无效JS标识符这不会被认为是一个错误If an attribute name is not a valid JS identifier(like a `data-*` attribute), it is not considered to be an error if it is not found in the element attributes type
展开运算符也是可用的The spead operator also works
```typescript
var props = { requiredProp: "bar" };
<foo {...props} />; // 没有问题
var badProps = {};
<foo {...badProps} />; // 错误
```
### 子元素类型检查Children Type Checking
在版本2.3中,引入了对 *子元素* 的类型检查。 *子元素* 是经由元素属性类型检查而确定下来的 *元素属性类型* 的一个属性( *children* is a property in an *element attributes type* which we have determined from type checking attributes。与使用`JSX.ElementAttributesProperty`来确定 *props* 的名称类似,也要使用`JSX.ElementChildrenAttributes`来确定 *子元素* 的名称。
应使用单一属性,来对`JSX.ElementChildrenAttributes`进行声明。
```typescript
declare namespace JSX {
interface ElementChildrenAttributes {
children: {}; // 指定要使用的 子元素 名称
}
}
```
在没有显式指定子元素的类型时,就将使用[React typings](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react) 中的默认类型。
```typescript
<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](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react) 的类型。
```typescript
interface PropsType {
children: JSX.Element
name: string
}
class Component extends React.Component<PropsType, {}> {
render () {
return (
<h2>
this.props.children
</h2>
);
}
}
// 没有问题
<Component>
<h1>Hello World</h1>
</Comonent>
// 错误:子元素 是类型 `JSX.Element` 而不是 `JSX.Element` 的数组
<Component>
<h1>Hello World</h1>
<h2>Hello World</h2>
</Component>
// 错误:子元素 是类型 `JSX.Element` 而不是 `JSX.Element` 的数组或字符串
<Component>
<h1>Hello</h1>
World
</Component>
```
## JSX结果类型The JSX result type
默认JSX表达式的结果的类型为`any`By default the result of a JSX expression is typed as `any`)。通过指定`JSX.Element`接口就可以对该类型进行定制。然而从该接口获取有关JSX的元素、元素属性或子元素的类型信息是无法做到的。其就是一个黑盒。
## 关于表达式的嵌入Embedding Expressions
JSX允许通过将表达式以花括符`{}`括起来的方式将表达式在标签之间进行嵌入JSX allows you to embed expressions between tags by surrounding the expressions with curly braces(`{}`))。
```typescript
var a = <div>
{["foo", "bar"].map(i => <span>{i / 2}</span>)}
</div>
```
因为无法将字符串除以数字,所以上面的代码将得到一个错误。而在使用`preserve`选项时,输出将是下面这样:
```typescript
var a = <div>
{["foo", "bar"].map(function (i) { return <span>{i / 2}</span>; })}
</div>;
```
## React的集成React integration
要使用带有React的JSX就应使用 [React typings](https://github.com/DefinitelyTyped/DefinitelyTyped/tree/master/types/react)。这些分型对`JSX`的命名空间以适应React的使用而进行了定义These typings define the `JSX` namespace appropriately for use with React
```typescript
/// <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} />; // 错误
```

View File

@ -1,8 +1,10 @@
# Summary
* [基本数据类型](01_basic_data_types.md)
* [变量声明](02_variables_declaration.md)
* [](03_classes.md)
* [接口](04_interfaces.md)
* [函数](05_functions.md)
* [泛型](06_generics.md)
* 一、[基本数据类型](01_basic_data_types.md)
* 二、[变量声明](02_variables_declaration.md)
* 三、[类](03_classes.md)
* 四、[接口](04_interfaces.md)
* 五、[函数](05_functions.md)
* 六、[泛型](06_generics.md)
* 七、[枚举](07_enums.md)
* 八、[类型推导](08_type_inference.md)

View File

@ -4,6 +4,7 @@
"description": "TypeScript Learning stuffs.",
"main": "/dist/main.js",
"scripts": {
"gulp": "gulp &",
"start": "live-server dist/",
"test": "echo \"Error: no test specified\" && exit 1",
"git-push": "./push.sh"

74
src/Calculator.ts Normal file
View File

@ -0,0 +1,74 @@
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()}'`);
}

View File

@ -0,0 +1,12 @@
'use strict';
/// <reference path="Validation.ts">
namespace Validation {
const lettersRegexp = /^[A-Za-z]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable (s: string) {
return lettersRegexp.test(s);
}
}
}

View File

@ -0,0 +1,25 @@
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("基数必须要在0到16的范围");
}
}
protected processDigit(digit: string, currentValue: number) {
if (ProgrammerCalculator.digits.indexOf(digit) >= 0) {
return currentValue * this.base + ProgrammerCalculator.digits.indexOf(digit);
}
}
}
// 将新的已扩展的计算器作为`Calculator`进行导出
export { ProgrammerCalculator as Calculator };
// 还要导出辅助函数
export { test } from "./Calculator";

21
src/Test.ts Normal file
View File

@ -0,0 +1,21 @@
/// <reference path="Validation.ts">
/// <reference path="LettersOnlyValidator.ts">
/// <reference path="ZipCodeValidator.ts">
// 一些尝试样本
let strings = ["Hello", "98052", "101"];
// 要使用的验证器
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
}
}

4
src/TestCalculator.ts Normal file
View File

@ -0,0 +1,4 @@
import { Calculator, test } from "./Calculator";
let c = new Calculator;
test(c, "1+2*33/11-3=");

View File

@ -0,0 +1,4 @@
import { Calculator, test } from "./ProgrammerCalculator";
let c = new Calculator(16);
test(c, "001+0A0=");

7
src/Validation.ts Normal file
View File

@ -0,0 +1,7 @@
'use strict';
namespace Validation {
export interface StringValidator {
isAcceptable (s: string): boolean;
}
}

12
src/ZipCodeValidator.ts Normal file
View File

@ -0,0 +1,12 @@
'use strict';
/// <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);
}
}
}

View File

@ -0,0 +1,8 @@
function buildLabel(name) {
return buildLabel.prefix + name + buildLabel.suffix;
}
(function (buildLabel) {
buildLabel.suffix = "";
buildLabel.prefix = "Hello, ";
})(buildLabel || (buildLabel = {}));
console.log(buildLabel("Sam Smith"));

View File

@ -0,0 +1,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"));

View File

@ -1,48 +1,21 @@
'use strict';
interface Lengthwise {
length: number;
}
function loggingIdentity<T extends Lengthwise>(arg: T): T {
console.log(arg.length); // 现在知道`arg`有着一个`.length`属性,因此不再报出错误
return arg;
}
import { StringValidator } from "./Validation";
import { ZipCodeValidator } from "./ZipCodeValidator";
import { LettersOnlyValidator } from "./LettersOnlyValidator";
loggingIdentity("test");
// 一些测试样本
let strings = ["Hello", "98052", "101"];
function getProperty<T, K extends keyof T>(obj: T, key: K) {
return obj[key];
}
// 要用到的验证器
let validators: { [s: string]: StringValidator; } = {};
let x = { a: 1, b: 2, c: 3, d: 4 };
console.log(getProperty(x, "a")); // 没有问题
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 createInstance<A extends Animal>(c: new () => A): A {
return new c();
}
createInstance(Lion).keeper.nametag; //
createInstance(Bee).keeper.hasMask;
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// 演示各个字符串是否通过各个验证器验证
strings.forEach(s => {
for (let name in validators) {
console.log(`"${ s }" - ${ validators[name].isAcceptable(s) ? "matches": "does not match" } ${ name }`);
}
});

39
src/namespaced.ts Normal file
View File

@ -0,0 +1,39 @@
namespace Validation {
export interface StringValidator {
isAcceptable (s: string): boolean;
}
const lettersRegexp = /^[A-Za-z]+$/;
const numberRegexp = /^[0-9]+$/;
export class LettersOnlyValidator implements StringValidator {
isAcceptable (s: string) {
return lettersRegexp.test(s);
}
}
export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
}
// 一些尝试样本
let strings = ["Hello", "98052", "101"];
// 要使用的验证器
let validators: { [s: string]: Validation.StringValidator; } = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
}
}

46
src/sample.js Normal file
View File

@ -0,0 +1,46 @@
'use strict';
/// <reference path="Validation.ts">
var Validation;
(function (Validation) {
var lettersRegexp = /^[A-Za-z]+$/;
var LettersOnlyValidator = /** @class */ (function () {
function LettersOnlyValidator() {
}
LettersOnlyValidator.prototype.isAcceptable = function (s) {
return lettersRegexp.test(s);
};
return LettersOnlyValidator;
}());
Validation.LettersOnlyValidator = LettersOnlyValidator;
})(Validation || (Validation = {}));
/// <reference path="Validation.ts">
var Validation;
(function (Validation) {
var numberRegexp = /^[0-9]+$/;
var ZipCodeValidator = /** @class */ (function () {
function ZipCodeValidator() {
}
ZipCodeValidator.prototype.isAcceptable = function (s) {
return s.length === 5 && numberRegexp.test(s);
};
return ZipCodeValidator;
}());
Validation.ZipCodeValidator = ZipCodeValidator;
})(Validation || (Validation = {}));
/// <reference path="Validation.ts">
/// <reference path="LettersOnlyValidator.ts">
/// <reference path="ZipCodeValidator.ts">
// 一些尝试样本
var strings = ["Hello", "98052", "101"];
// 要使用的验证器
var validators = {};
validators["ZIP code"] = new Validation.ZipCodeValidator();
validators["Letters only"] = new Validation.LettersOnlyValidator();
// 展示各个字符串是否通过各个验证器验证
for (var _i = 0, strings_1 = strings; _i < strings_1.length; _i++) {
var s = strings_1[_i];
for (var name_1 in validators) {
var isMatch = validators[name_1].isAcceptable(s);
console.log("\"" + s + "\" " + (isMatch ? "matches" : "does not match") + " \"" + name_1 + "\".");
}
}

37
src/singleFile.ts Normal file
View File

@ -0,0 +1,37 @@
interface StringValidator {
isAcceptable (s: string): boolean;
}
let lettersRegexp = /^[A-Za-z]+$/;
let numberRegexp = /^[0-9]+$/;
class LettersOnlyValidator implements StringValidator {
isAcceptable (s: string) {
return lettersRegexp.test(s);
}
}
class ZipCodeValidator implements StringValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
// 一些尝试样本
let strings = ["Hello", "98052", "101"];
// 要使用的验证器
let validators: { [s: string]: StringValidator; } = {};
validators["ZIP code"] = new ZipCodeValidator();
validators["Letters only"] = new LettersOnlyValidator();
// 展示各个字符串是否通过各个验证器验证
for (let s of strings) {
for (let name in validators) {
let isMatch = validators[name].isAcceptable(s);
console.log(`"${ s }" ${ isMatch ? "matches" : "does not match" } "${ name }".`);
}
}

View File

@ -1,6 +1,6 @@
{
"files": [
"src/main.ts"
"src/*.ts"
],
"compilerOptions": {
"noImplicitAny": true,