Gowhich

Durban's Blog

模块

关于术语的一点说明: 请务必注意一点,TypeScript 1.5里术语名已经发生了变化。 “内部模块”现在称做”命名空间”。 “外部模块”现在则简称为”模块”,这是为了与 ECMAScript 2015里的术语保持一致,(也就是说 module X { 相当于现在推荐的写法 namespace X {)。

介绍

从ECMAScript 2015开始,JavaScript引入了模块的概念。TypeScript也沿用这个概念。

模块在其自身的作用域里执行,而不是在全局作用域里;这意味着定义在一个模块里的变量,函数,类等等在模块外部是不可见的,除非你明确地使用export形式之一导出它们。 相反,如果想使用其它模块导出的变量,函数,类,接口等的时候,你必须要导入它们,可以使用import形式之一。

模块是自声明的;两个模块之间的关系是通过在文件级别上使用imports和exports建立的。

模块使用模块加载器去导入其它的模块。 在运行时,模块加载器的作用是在执行此模块代码前去查找并执行这个模块的所有依赖。 大家最熟知的JavaScript模块加载器是服务于Node.js的 CommonJS和服务于Web应用的Require.js。

TypeScript与ECMAScript 2015一样,任何包含顶级import或者export的文件都被当成一个模块。相反地,如果一个文件不带有顶级的import或者export声明,那么它的内容被视为全局可见的(因此对模块也是可见的)。

导出

导出声明

任何声明(比如变量,函数,类,类型别名或接口)都能够通过添加export关键字来导出。

Validation.ts

1
2
3
export interface StringValidator {
isAcceptable(s: string): boolean;
}

ZipCodeValidator.ts

1
2
3
4
5
6
7
export const numberRegexp = /^[0-9]+$/;

export class ZipCodeValidator implements StringValidator {
isAcceptable(s: string): boolean {
return s.length === 5 && numberRegexp.test(s);
}
}

导出语句

导出语句很便利,因为我们可能需要对导出的部分重命名,所以上面的例子可以这样改写:

1
2
3
4
5
6
7
8
9
10
export const numberRegexp = /^[0-9]+$/;

class ZipCodeValidator implements StringValidator {
isAcceptable(s: string): boolean {
return s.length === 5 && numberRegexp.test(s);
}
}

export { ZipCodeValidator };
export { ZipCodeValidator as mainValidator }

重新导出

我们经常会去扩展其它模块,并且只导出那个模块的部分内容。 重新导出功能并不会在当前模块导入那个模块或定义一个新的局部变量。
ParseIntBasedZipCodeValidator.ts

1
2
3
4
5
6
7
export class ParseIntBasedZipCodeValidator {
isAcceptable(s: string): boolean {
return s.length === 5 && parseInt(s).toString() === s;
}
}

export { ZipCodeValidator as RegExpBaseZipCodeValidator } from './ZipCodeValidator';

或者一个模块可以包裹多个模块,并把他们导出的内容联合在一起通过语法:export * from “module”。

AllValidators.ts

1
2
export * from "./StringValidator"; // exports interface 'StringValidator'
export * from "./ZipCodeValidator"; // exports class 'ZipCodeValidator'

导入

模块的导入操作与导出一样简单。 可以使用以下import形式之一来导入其它模块中的导出内容。

导入一个模块中的某个导出内容

1
2
3
import { ZipCodeValidator } from "./ZipCodeValidator";

let myValidator = new ZipCodeValidator();

可以对导入内容重命名

1
2
import { ZipCodeValidator as ZCV } from "./ZipCodeValidator";
let myValidator = new ZCV();

将整个模块导入到一个变量,并通过它来访问模块的导出部分

1
2
import * as validator from "./ZipCodeValidator";
let myValidator = new validator.ZipCodeValidator();

具有副作用的导入模块

尽管不推荐这么做,一些模块会设置一些全局状态供其它模块使用。 这些模块可能没有任何的导出或用户根本就不关注它的导出。 使用下面的方法来导入这类模块:

1
import "./my-module.js";

默认导出

每个模块都可以有一个default导出。 默认导出使用 default关键字标记;并且一个模块只能够有一个default导出。 需要使用一种特殊的导入形式来导入 default导出。

default导出十分便利。 比如,像JQuery这样的类库可能有一个默认导出 jQuery或$,并且我们基本上也会使用同样的名字jQuery或$导出JQuery。

JQuery.d.ts

1
2
declare let $: JQuery;
export default $;

App.ts

1
2
3
import $ from "JQuery";

$("button.continue").html( "Next Step..." );

类和函数声明可以直接被标记为默认导出。 标记为默认导出的类和函数的名字是可以省略的。

ZipCodeValidator.ts

1
2
3
4
5
6
export default class ZipCodeValidator {
static numberRegexp = /^[0-9]+$/;
isAcceptable(s: string) {
return s.length === 5 && ZipCodeValidator.numberRegexp.test(s);
}
}

Test.ts

1
2
3
import validator from "./ZipCodeValidator";

let myValidator = new validator();

或者

StaticZipCodeValidator.ts

1
2
3
4
5
const numberRegexp = /^[0-9]+$/;

export default function (s: string) {
return s.length === 5 && numberRegexp.test(s);
}

Test.ts

1
2
3
4
5
6
7
8
import validate from "./StaticZipCodeValidator";

let strings = ["Hello", "98052", "101"];

// Use function validate
strings.forEach(s => {
console.log(`"${s}" ${validate(s) ? " matches" : " does not match"}`);
});

default导出也可以是一个值
OneTwoThree.ts

1
export default "123";

Log.ts

1
2
3
import num from "./OneTwoThree";

console.log(num); // "123"

export =import = require()
CommonJS和AMD都有一个exports对象的概念,它包含了一个模块的所有导出内容。

它们也支持把exports替换为一个自定义对象。 默认导出就好比这样一个功能;然而,它们却并不相互兼容。 TypeScript模块支持 export =语法以支持传统的CommonJS和AMD的工作流模型。

export =语法定义一个模块的导出对象。 它可以是类,接口,命名空间,函数或枚举。

若要导入一个使用了export =的模块时,必须使用TypeScript提供的特定语法import module = require(“module”)。

ZipCodeValidator.ts

1
2
3
4
5
6
7
let numberRegexp = /^[0-9]+$/;
class ZipCodeValidator {
isAcceptable(s: string) {
return s.length === 5 && numberRegexp.test(s);
}
}
export = ZipCodeValidator;

Test.ts

1
2
3
4
5
6
7
8
9
10
11
12
import zip = require("./ZipCodeValidator");

// 尝试一些字符
let strings = ["Hello", "98052", "101"];

// 使用validator
let validator = new zip();

// 检测每个字符串,是否通过验证
strings.forEach(s => {
console.log(`"${ s }" - ${ validator.isAcceptable(s) ? "matches" : "does not match" }`);
});

迭代性

如果对象具有Symbol.iterator属性的实现,则该对象被视为可迭代。
一些内置类型,如Array,Map,Set,String,Int32Array,Uint32Array等,已经实现了Symbol.iterator属性。
对象上的Symbol.iterator函数负责返回值列表以进行迭代。

for..of语句

for..of循环遍历可迭代对象,调用对象上的Symbol.iterator属性。下面是一个简单的for..of循环数组:

1
2
3
4
5
let someArray = [1, "string", false];

for (let entry of someArray) {
console.log(entry); // 1, "string", false
}

for..of vs. for..in语句

for..offor..in语句都遍历列表;迭代的值是不同的,for..in返回正在迭代的对象上的键列表,而for..of返回正在迭代的对象的数值属性的值列表。下面展示一个对比的例子:

1
2
3
4
5
6
7
8
9
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属性,允许访问存储的值。如下实例演示

1
2
3
4
5
6
7
8
9
10
let pets = new Set(["Cat", "Dog", "Hamster"]);
pets["species"] = "mammals";

for (let pet in pets) {
console.log(pet); // "species"
}

for (let pet of pets) {
console.log(pet); // "Cat", "Dog", "Hamster"
}

上面这段代码我在运行的时候是报错了的,不知道是不是官方哪里弄错了,也可能是需要做另外一些配置。如果您也遇到了跟我一样的错误,请留言指导

生成器

目标为 ES5 和 ES3

在针对ES5或ES3时,只允许在Array类型的值上使用迭代器。在非数组值上使用for循环是错误的,即使这些非数组值实现了Symbol.iterator属性也是如此。编译器将为for..of循环生成一个简单的for循环,例如:

1
2
3
4
let numbers = [1, 2, 3];
for (let num of numbers) {
console.log(num);
}

编译后生成的代码如下

1
2
3
4
5
var numbers = [1, 2, 3];
for (var _i = 0; _i < numbers.length; _i++) {
var num = numbers[_i];
console.log(num);
}

在针对ECMAScipt 2015兼容引擎时,编译器将生成for..of循环以定位引擎中的内置迭代器实现。

介绍

自ECMAScript 2015起,symbol成为了一种新的原生类型,就像number和string一样。

symbol类型的值是通过Symbol构造函数创建的。

1
2
let sym1 = Symbol();
let sym2 = Symbol("key"); // 可选的字符串key

Symbols是不可改变且唯一的。

1
2
3
let sym2 = Symbol("key");
let sym3 = Symbol("key");
sym2 === sym3; // false, symbols是唯一的

像字符串一样,symbols也可以被用做对象属性的键。

1
2
3
4
5
6
7
let sym = Symbol();

let obj = {
[sym]: "value"
};

console.log(obj[sym]); // "value"

Symbols也可以与计算出的属性名声明相结合来声明对象的属性和类成员。

1
2
3
4
5
6
7
8
9
10
const getClassNameSymbol = Symbol();

class C {
[getClassNameSymbol](){
return "C";
}
}

let c = new C();
let className = c[getClassNameSymbol](); // "C"

众所周知的Symbols

除了用户定义的symbols,还有一些已经众所周知的内置symbols。 内置symbols用来表示语言内部的行为。

以下为这些symbols的列表:

Symbol.hasInstance
方法,会被instanceof运算符调用。构造器对象用来识别一个对象是否是其实例。

Symbol.isConcatSpreadable
布尔值,表示当在一个对象上调用Array.prototype.concat时,这个对象的数组元素是否可展开。

Symbol.iterator
方法,被for-of语句调用。返回对象的默认迭代器。

Symbol.match
方法,被String.prototype.match调用。正则表达式用来匹配字符串。

Symbol.replace
方法,被String.prototype.replace调用。正则表达式用来替换字符串中匹配的子串。

Symbol.search
方法,被String.prototype.search调用。正则表达式返回被匹配部分在字符串中的索引。

Symbol.species
函数值,为一个构造函数。用来创建派生对象。

Symbol.split
方法,被String.prototype.split调用。正则表达式来用分割字符串。

Symbol.toPrimitive
方法,被ToPrimitive抽象操作调用。把对象转换为相应的原始值。

Symbol.toStringTag
方法,被内置方法Object.prototype.toString调用。返回创建对象时默认的字符串描述。

Symbol.unscopables
对象,它自己拥有的属性会被with作用域排除在外。

TypeScript新特性之项目引用(project references)

项目引用是TypeScript 3.0中的一项新功能,允许您将TypeScript程序构建为更小的部分。

通过这样做,您可以大大缩短构建时间,实现组件之间的逻辑分离,并以新的更好的方式组织代码。

我们还为tsc引入了一种新模式,即–build标志,它与项目引用协同工作,以实现更快的TypeScript构建。

示例项目

让我们看一个相当正常的程序,看看项目引用如何帮助我们更好地组织它。
想象一下,你有一个项目有两个模块,转换器和单元,以及每个模块的相应测试文件:

1
2
3
4
5
/src/converter.ts
/src/units.ts
/test/converter-tests.ts
/test/units-tests.ts
/tsconfig.json

测试文件导入实现文件并进行一些测试:

1
2
3
4
// converter-tests.ts
import * as converter from "../converter";

assert.areEqual(converter.celsiusToFahrenheit(0), 32);

以前,如果您使用单个tsconfig文件,则此结构很难处理:

  1. 实现文件可以导入测试文件
  2. 在输出文件夹名称中没有出现src的情况下,无法同时构建test和src,这可能是您不想要的
  3. 仅更改实现文件中的内部结构需要再次检查测试,即使这不会导致新的错误
  4. 仅更改测试需要再次对实现进行检查,即使没有任何改变

您可以使用多个tsconfig文件来解决其中的一些问题,但会出现新的问题:

  1. 没有内置的最新检查,因此您最终总是运行两次tsc
  2. 两次调用tsc会导致更多的启动时间开销
  3. tsc -w无法一次在多个配置文件上运行

项目引用(project references)可以解决所有这些问题等等。

什么是项目引用(project references)?

tsconfig.json文件有一个新的顶级属性”references”。
它是一个对象数组,指定要引用的项目:

1
2
3
4
5
6
7
8
{
"compilerOptions": {
// The usual
},
"references": [
{ "path": "../src" }
]
}

每个引用的path属性可以指向包含tsconfig.json文件的目录,也可以指向配置文件本身(可以具有任何名称)。
当您引用项目时,会发生新的事情:

  1. 从引用的项目导入模块将改为加载其输出声明文件(.d.ts)
  2. 如果引用的项目生成outFile,则输出文件.d.ts文件的声明将在此项目中可见
  3. 如果需要,构建模式(下面会提到)将自动构建引用的项目

通过分成多个项目,您可以大大提高类型检查和编译的速度,减少使用编辑器时的内存使用量,并改进程序逻辑分组的实施。

composite

引用的项目必须启用新的composite设置。
需要此设置以确保TypeScript可以快速确定在何处查找引用项目的输出。
启用composite标志会改变一些事情:

  1. rootDir设置(如果未显式设置)默认为包含tsconfig文件的目录
  2. 所有实现文件必须由include模式匹配或在files数组中列出。如果违反此约束,tsc将通知您未指定哪些文件
  3. declaration必须打开

declarationMaps

我们还增加了对declaration source maps的支持。如果启用–declarationMap,您将能够使用编辑器功能,如”转到定义”和重命名,以在支持的编辑器中跨项目边界透明地导航和编辑代码。

以outFile为前缀

您还可以使用引用中的prepend选项启用前置依赖项的输出:

1
2
3
"references": [
{ "path": "../utils", "prepend": true }
]

预先设置项目将包括项目的输出高于当前项目的输出。
这适用于.js文件和.d.ts文件,源代码映射文件也将正确发出。

tsc只会使用磁盘上的现有文件来执行此过程,因此可以创建一个项目,其中无法生成正确的输出文件,因为某些项目的输出将在结果文件中出现多次。
例如:

1
2
3
4
5
6
7
   A
^ ^
/ \
B C
^ ^
\ /
D

在这种情况下,重要的是不要在每个参考文献中添加前缀,因为在D的输出中最终会得到两个A副本 - 这可能会导致意外结果。

项目引用的注意事项

项目引用有一些您应该注意的权衡。

因为依赖项目使用从其依赖项构建的.d.ts文件,所以您必须在克隆之后签入某些构建输出或构建项目,然后才能在编辑器中导航项目而不会看到虚假错误。
我们正在开发一个能够缓解这种情况的幕后.d.ts生成过程,但是现在我们建议告知开发人员他们应该在克隆之后构建它们。

此外,为了保持与现有构建工作流的兼容性,除非使用–build开关调用,否则tsc不会自动构建依赖项。
让我们了解更多关于–build的信息。

TypeScript的构建模式

期待已久的功能是TypeScript项目的智能增量构建。
在3.0中,您可以将-build标志与tsc一起使用。
这实际上是tsc的新入口点,其行为更像构建协调器而不是简单的编译器。

运行tsc --build(简称tsc -b)将执行以下操作:

  1. 查找所有引用的项目
  2. 检测它们是否是最新的
  3. 按正确的顺序构建过时的项目

您可以为tsc -b提供多个配置文件路径(例如tsc -b src test)。
就像tsc -p一样,如果命名为tsconfig.json,则不需要指定配置文件名本身。

1
2
3
> tsc -b                                # 在当前目录中构建tsconfig.json
> tsc -b src # 构建src/tsconfig.json
> tsc -b foo/release.tsconfig.json bar # 构建foo/release.tsconfig.json和构建bar/tsconfig.json

不要担心您在命令行上传递的排过序的文件 - 如果需要,tsc将重新排序它们,以便始终首先构建依赖项。
还有一些特定于tsc -b的标志:

–verbose: 打印详细日志记录以解释正在发生的事情(可能与任何其他标志组合)
–dry: 显示将要完成的但实际上不构建任何内容
–clean: 删除指定项目的输出(可以与–dry结合使用)
–force: 就好像所有项目都已过时一样
–watch: 监视模式(除了–verbose外,不得与任何标志组合使用)

注意事项

通常,除非出现noEmitOnError,否则tsc将在出现语法或类型错误时生成输出(.js和.d.ts)。
在增量构建系统中执行此操作将非常糟糕 - 如果您的一个过时的依赖项出现新错误,您只能看到它一次,因为后续构建将跳过构建现在最新的项目。
因此,tsc -b实际上就像为所有项目启用noEmitOnError一样。
如果您检查任何构建输出(.js,.d.ts,.d.ts.map等),您可能需要在某些源控制操作之后运行–force构建,具体取决于源控制工具是否保留
本地副本和远程副本之间的时间映射。

MSBuild

如果您有msbuild项目,则可以通过添加如下代码到您的proj文件来启用构建模式

1
<TypeScriptBuildMode>true</TypeScriptBuildMode>

这将启用自动增量构建和清洁。

请注意,与tsconfig.json/-p一样,不会遵循现有的TypeScript项目属性 - 应使用tsconfig文件管理所有设置。

一些团队已经设置了基于msbuild的工作流,其中tsconfig文件与他们配对的托管项目具有相同的隐式图表排序。
如果您的解决方案是这样的,您可以继续使用msbuild和tsc -p以及项目引用;
这些是完全可互操作的。

指导(Guidance)

整体结构

使用更多tsconfig.json文件,您通常需要使用配置文件继承来集中您的常用编译器选项。
这样,您可以在一个文件中更改设置,而不必编辑多个文件。

另一个好的做法是拥有一个”解决方案”tsconfig.json文件,该文件只引用了所有leaf-node项目。
这提供了一个简单的切入点;
例如,在TypeScript repo中,我们只运行tsc -b src来构建所有端点,因为我们列出了src/tsconfig.json中的所有子项目。请注意,从3.0开始,如果在tsconfig.json中至少有一个reference将不会针对空的files数组报错

您可以在TypeScript存储库中看到这些模式 - src/tsconfig_base.jsonsrc/tsconfig.jsonsrc/tsc/tsconfig.json作为关键示例。

构建相关模块

通常,使用相关模块transition a repo并不需要太多。
只需将tsconfig.json文件放在给定父文件夹的每个子目录中,并添加对这些配置文件的引用以匹配程序的预期分层。
您需要将outDir设置为输出文件夹的显式子文件夹,或将rootDir设置为所有项目文件夹的公共根目录。

构建outFiles

使用outFile进行编译的布局更灵活,因为相对路径无关紧要。
要记住的一件事是,您通常希望在”最后”项目之前不使用前置 - 这将改善构建时间并减少任何给定构建中所需的I/O量。
TypeScript repo本身就是一个很好的参考 - 我们有一些”库”项目和一些”端点”项目;
“端点”项目尽可能小,只吸引他们需要的库。

原文地址:
http://www.typescriptlang.org/docs/handbook/project-references.html

高级类型

映射类型

一个常见的任务是将一个已知的类型每个属性都变为可选的:

1
2
3
4
interface PersonPartial {
name?: string;
age?: number;
}

或者我们想要一个只读版本:

1
2
3
4
interface PersonReadonly {
readonly name: string;
readonly age: number;
}

这在JavaScript里经常出现,TypeScript提供了从旧类型中创建新类型的一种方式 — 映射类型。 在映射类型里,新类型以相同的形式去转换旧类型里每个属性。 例如,你可以令每个属性成为 readonly类型或可选的。 下面是一些例子:

1
2
3
4
5
6
type Readonly<T> = {
readonly [P in keyof T]: T[P];
}
type Partial<T> = {
[P in keyof T]?: T[P];
}

像下面这样使用:

1
2
type PersonPartial = Partial<Person>;
type ReadonlyPerson = Readonly<Person>;

下面来看看最简单的映射类型和它的组成部分:

1
2
type Keys = 'option1' | 'option2';
type Flags = { [K in Keys]: boolean };

它的语法与索引签名的语法类型,内部使用了 for .. in。 具有三个部分:

  1. 类型变量 K,它会依次绑定到每个属性。
  2. 字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
  3. 属性的结果类型。
    在这个简单的例子里, Keys是硬编码的的属性名列表并且属性类型永远是boolean,因此这个映射类型等同于:
1
2
3
4
type Flags = {
option1: boolean;
option2: boolean;
}

在真正的应用里,可能不同于上面的 Readonly或 Partial。 它们会基于一些已存在的类型,且按照一定的方式转换字段。 这就是 keyof和索引访问类型要做的事情:

1
2
type NullablePerson = { [P in keyof Person]: Person[P] | null }
type PartialPerson = { [P in keyof Person]?: Person[P] }

但它更有用的地方是可以有一些通用版本。

1
2
type Nullable<T> = { [P in keyof T]: T[P] | null }
type Partial<T> = { [P in keyof T]?: T[P] }

在这些例子里,属性列表是 keyof T且结果类型是 T[P]的变体。 这是使用通用映射类型的一个好模版。 因为这类转换是 同态的,映射只作用于 T的属性而没有其它的。 编译器知道在添加任何新属性之前可以拷贝所有存在的属性修饰符。 例如,假设 Person.name是只读的,那么 Partial.name也将是只读的且为可选的。

下面是另一个例子, T[P]被包装在 Proxy类里:

1
2
3
4
5
6
7
8
9
10
11
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> {
// ... wrap proxies ...
}
let proxyProps = proxify(props);

注意 Readonly和 Partial用处不小,因此它们与 Pick和 Record一同被包含进了TypeScript的标准库里:

1
2
3
4
5
6
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并不需要输入类型来拷贝属性,所以它不属于同态:

1
type ThreeStringProps = Record<'prop1' | 'prop2' | 'prop3', string>

非同态类型本质上会创建新的属性,因此它们不会从它处拷贝属性修饰符。

由映射类型进行推断

现在你了解了如何包装一个类型的属性,那么接下来就是如何拆包。 其实这也非常容易:

1
2
3
4
5
6
7
8
9
function unproxify<T>(t: Proxify<T>): T {
let result = {} as T;
for (const k in t) {
result[k] = t[k].get();
}
return result;
}

let originalProps = unproxify(proxyProps);

注意这个拆包推断只适用于同态的映射类型。 如果映射类型不是同态的,那么需要给拆包函数一个明确的类型参数。

高级类型

索引类型(Index types)

使用索引类型,编译器就能够检查使用了动态属性名的代码。 例如,一个常见的JavaScript模式是从对象中选取属性的子集。

1
2
3
function pluck(o, names) {
return names.map(n => o[n]);
}

下面是如何在TypeScript里使用此函数,通过 索引类型查询和 索引访问操作符:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function pluck<T, K extends keyof T>(o:T, names: K[]): T[K][] {
return names.map(n => o[n])
}

interface Interface1 {
name: string;
age: number;
}

let i: Interface1 = {
name: "A",
age: 1,
}

let pluckStr: string[] = pluck(i, ['name'])
console.log(pluckStr)

运行后输出如下

1
[ 'A' ]

编译器会检查 name是否真的是Interface1的一个属性。 本例还引入了几个新的类型操作符。 首先是 keyof T, 索引类型查询操作符。 对于任何类型 T, keyof T的结果为 T上已知的公共属性名的联合。 例如:

1
let someProps: keyof Interface1; // 'name' | 'age'

keyof Interface1是完全可以与’name’|’age’互相替换的。 不同的是如果你添加了其它的属性到Interface1,例如address: string,那么 keyof Interface1会自动变为’name’|’age’|’address’。 你可以在像pluck函数这类上下文里使用 keyof,因为在使用之前你并不清楚可能出现的属性名。 但编译器会检查你是否传入了正确的属性名给 pluck:

1
pluck(i, ['age', 'unknown']); // error, 'unknown' is not in 'name' | 'age'

第二个操作符是T[K],索引访问操作符。 在这里,类型语法反映了表达式语法。 这意味着 i[‘name’]具有类型Interface1[‘name’] — 在我们的例子里则为string类型。 然而,就像索引类型查询一样,你可以在普通的上下文里使用 T[K],这正是它的强大所在。 你只要确保类型变量 K extends keyof T就可以了。 例如下面 getProperty函数的例子:

1
2
3
function getProperty<T, K extends keyof T>(o: T, name: K): T[K] {
return o[name]; // o[name] is of type T[K]
}

getProperty里的 o: T和 name: K,意味着 o[name]: T[K]。 当你返回 T[K]的结果,编译器会实例化键的真实类型,因此 getProperty的返回值类型会随着你需要的属性改变。

1
2
3
let name: string = getProperty(i, 'name');
let age: number = getProperty(i, 'age');
let unknown = getProperty(i, 'unknown'); // error, 'unknown' is not in 'name' | 'age'

索引类型和字符串索引签名

keyof和T[K]与字符串索引签名进行交互。如果你有一个带有字符串索引签名的类型,那么 keyof T会是 string。并且T[string]为索引签名的类型:

1
2
3
4
5
interface Map<T> {
[key: string]: T;
}
let keys: keyof Map<number>; // string
let value: Map<number>['foo']; // number

高级类型

多态的this类型

多态的this类型表示的是某个包含类或接口的子类型。 这被称做F-bounded多态性。 它能很容易的表现连贯接口间的继承,比如。 在下面的例子里,在每个操作之后都返回this类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
class Query {
public whereCon: Array<string> = [];

public constructor(protected tableName: string = '') { }

public andWhere(key: string, value: string) {
this.whereCon.push(`${key}=${value}`);
return this;
}

public orWhere(key: string, value: string) {
this.whereCon.push(`OR ${key}=${value}`);
return this;
}

public inWhere(key: string, value: string) {
this.whereCon.push(`AND ${key} IN (${value})`);
return this;
}

public getSQL(): string {
return `SELECT * FROM ${this.tableName} WHERE ${this.whereCon.join(' ')}`;
}

// ... 其他的操作
}

let generateSQL = new Query('table_name')
.andWhere('key1', 'value1')
.orWhere('key2','value2')
.inWhere('key3','value3')
.getSQL();

console.log(generateSQL);

运行后输入结果如下

1
2
$ npx ts-node ./src/advanced_types_5.ts
SELECT * FROM table_name WHERE key1=value1 OR key2=value2 AND key3 IN (value3)

这个类当然还是有点缺陷的,但是我们可以看出这个特性的使用方式由于这个类使用了this类型,你可以继承它,新的类可以直接使用之前的方法,不需要做任何的改变。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class TQuery extends Query {
public constructor(tableName: string = '') {
super(tableName);
}

public getUpdateSql(key: string, value: string) {
return `UPDATE ${this.tableName} SET ${key}=${value} WHERE ${this.whereCon.join(' ')}`;
}

// ... 其他的操作
}

let generateSQL = new TQuery('table_name')
.andWhere('key1', 'value1')
.orWhere('key2', 'value2')
.inWhere('key3', 'value3')
.getUpdateSql('key4', 'value4');
console.log(generateSQL);

运行后输入结果如下

1
2
$ npx ts-node ./src/advanced_types_5.ts
UPDATE table_name SET key4=value4 WHERE key1=value1 OR key2=value2 AND key3 IN (value3)

如果没有this类型,TQuery就不能够在继承Query的同时还保持接口的连贯性。 inWhere将会返回Query,它并没有getUpdateSql方法。 然而,使用this类型,inWhere会返回 this,在这里就是 TQuery。

ntpdate报错the NTP socket is in use, exiting

客户端使用ntpdate与NTP服务器进行时钟同步时,报错”the NTP socket is in use, exiting”,如下:

1
2
[root@h3 vdsm]# ntpdate us.pool.ntp.org
21 Feb 03:04:30 ntpdate[19759]: the NTP socket is in use, exiting

原因:ntp服务已运行
解决办法:

1
2
3
4
[root@h3 vdsm]# service ntpd stop
Shutting down ntpd: [ OK ]
[root@h3 vdsm]# ntpdate us.pool.ntp.org
21 Feb 03:06:55 ntpdate[19961]: step time server 192.168.0.253 offset 46.911562 sec

更新完之后记得重新启动ntpd

1
service ntpd start

高级类型

可辨识联合(Discriminated Unions)

你可以合并单例类型,联合类型,类型保护和类型别名来创建一个叫做 可辨识联合的高级模式,它也称做 标签联合或 代数数据类型。 可辨识联合在函数式编程很有用处。 一些语言会自动地为你辨识联合;而TypeScript则基于已有的JavaScript模式。 它具有3个要素:

  1. 具有普通的单例类型属性— 可辨识的特征。
  2. 一个类型别名包含了那些类型的联合— 联合。
  3. 此属性上的类型保护。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
interface Interface1 {
kind: 'interface1',
property1: number,
}

interface Interface2 {
kind: 'interface2',
property2: number,
property3: number,
}

interface Interface3 {
kind: 'interface3',
property4: number,
property5: number,
}

首先我们声明了将要联合的接口。 每个接口都有 kind属性但有不同的字符串字面量类型。 kind属性称做 可辨识的特征或 标签。 其它的属性则特定于各个接口。 注意,目前各个接口间是没有联系的。 下面我们把它们联合到一起:

1
type Type = Interface1 | Interface2 | Interface3;

现在我们使用可辨识联合:

1
2
3
4
5
6
7
8
9
10
function getType(i: Type) {
switch (i.kind) {
case "interface1":
return i.property1 * i .property1;
case "interface2":
return i.property2 * i.property3;
case "interface3":
return i.property4 * i.property5;
}
}

完整性检查

当没有涵盖所有可辨识联合的变化时,我们想让编译器可以通知我们。 比如,如果我们添加了Interface4到Type,我们同时还需要更新getType:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface Interface4 {
kind: 'interface4',
property6: number,
}

type Type = Interface1 | Interface2 | Interface3 | Interface4;

function getType(i: Type) {
switch (i.kind) {
case "interface1":
return i.property1 * i .property1;
case "interface2":
return i.property2 * i.property3;
case "interface3":
return i.property4 * i.property5;
}
}

有两种方式可以实现。 首先是启用 –strictNullChecks并且指定一个返回值类型:

1
2
3
4
5
6
7
8
9
10
function getType(i: Type): number { // error: returns number | undefined
switch (i.kind) {
case "interface1":
return i.property1 * i .property1;
case "interface2":
return i.property2 * i.property3;
case "interface3":
return i.property4 * i.property5;
}
}

因为 switch没有包涵所有情况,所以TypeScript认为这个函数有时候会返回 undefined。 如果你明确地指定了返回值类型为 number,那么你会看到一个错误,因为实际上返回值的类型为 number | undefined。 然而,这种方法存在些微妙之处且 –strictNullChecks对旧代码支持不好。第二种方法使用 never类型,编译器用它来进行完整性检查:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
function assertNever(x: never): never {
throw new Error("Unexpected object: " + x);
}

function getType(i: Type): number { // error: returns number | undefined
switch (i.kind) {
case "interface1":
return i.property1 * i .property1;
case "interface2":
return i.property2 * i.property3;
case "interface3":
return i.property4 * i.property5;
default:
return assertNever(i); // error here if there are missing cases
}
}

这里, assertNever检查 s是否为 never类型—即为除去所有可能情况后剩下的类型。 如果你忘记了某个case,那么 s将具有一个真实的类型并且你会得到一个错误。 这种方式需要你定义一个额外的函数,但是在你忘记某个case的时候也更加明显。**Tips**
上面的代码是根据官网的逻辑写的,奇怪的是最后一步居然在编译的时候报错了。报错信息如下

1
2
3
4
5
$ tsc src/advanced_types_4.ts
src/advanced_types_4.ts:38:26 - error TS2345: Argument of type 'Interface4' is not assignable to parameter of type 'never'.

38 return assertNever(i); // error here if there are missing cases
~

高级类型

字符串字面量类型

字符串字面量类型允许你指定字符串必须的固定值。 在实际应用中,字符串字面量类型可以与联合类型,类型保护和类型别名很好的配合。 通过结合使用这些特性,你可以实现类似枚举类型的字符串。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
type Easing = "ease-in" | "ease-out" | "ease-in-out";
class UIElement {
animate(dx: number, dy: number, easing: Easing) {
if (easing === "ease-in") {
// ...
}
else if (easing === "ease-out") {
}
else if (easing === "ease-in-out") {
}
else {
// error! should not pass null or undefined.
}
}
}

let button = new UIElement();
button.animate(0, 0, "ease-in");
button.animate(0, 0, "uneasy"); // error: "uneasy" is not allowed here

你只能从三种允许的字符中选择其一来做为参数传递,传入其它值则会产生错误。

1
Argument of type '"uneasy"' is not assignable to parameter of type '"ease-in" | "ease-out" | "ease-in-out"'

字符串字面量类型还可以用于区分函数重载:

1
2
3
4
5
6
function createElement(tagName: "img"): HTMLImageElement;
function createElement(tagName: "input"): HTMLInputElement;
// ... more overloads ...
function createElement(tagName: string): Element {
// ... code goes here ...
}

数字字面量类型

TypeScript还具有数字字面量类型。

1
2
3
function rollDie(): 1 | 2 | 3 | 4 | 5 | 6 {
// ...
}

我们很少直接这样使用,但它们可以用在缩小范围调试bug的时候:

1
2
3
4
5
6
function foo(x: number) {
if (x !== 1 || x !== 2) {
// ~~~~~~~
// Operator '!==' cannot be applied to types '1' and '2'.
}
}

换句话说,当x与2进行比较的时候,它的值必须为1,这就意味着上面的比较检查是非法的。

枚举成员类型

如我们在 枚举一节里提到的,当每个枚举成员都是用字面量初始化的时候枚举成员是具有类型的。

在我们谈及“单例类型”的时候,多数是指枚举成员类型和数字/字符串字面量类型,尽管大多数用户会互换使用“单例类型”和“字面量类型”。

0%