深入理解 Typescript-Deep In Typescript
文章目录

下方内容会略去很多基础知识, 因此不适合用于基础学习

Typescript Projects

Compilation Context

compilerOptions

你可以通过 compilerOptions 来定制你的编译选项:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
{
"compilerOptions": {

/* 基本选项 */
"target": "es5", // 指定 ECMAScript 目标版本: 'ES3' (default), 'ES5', 'ES6'/'ES2015', 'ES2016', 'ES2017', or 'ESNEXT'
"module": "commonjs", // 指定使用模块: 'commonjs', 'amd', 'system', 'umd' or 'es2015'
"lib": [], // 指定要包含在编译中的库文件
"allowJs": true, // 允许编译 javascript 文件
"checkJs": true, // 报告 javascript 文件中的错误
"jsx": "preserve", // 指定 jsx 代码的生成: 'preserve', 'react-native', or 'react'
"declaration": true, // 生成相应的 '.d.ts' 文件
"sourceMap": true, // 生成相应的 '.map' 文件
"outFile": "./", // 将输出文件合并为一个文件
"outDir": "./", // 指定输出目录
"rootDir": "./", // 用来控制输出目录结构 --outDir.
"removeComments": true, // 删除编译后的所有的注释
"noEmit": true, // 不生成输出文件
"importHelpers": true, // 从 tslib 导入辅助工具函数
"isolatedModules": true, // 将每个文件作为单独的模块 (与 'ts.transpileModule' 类似) .

/* 严格的类型检查选项 */
"strict": true, // 启用所有严格类型检查选项
"noImplicitAny": true, // 在表达式和声明上有隐含的 any 类型时报错
"strictNullChecks": true, // 启用严格的 null 检查
"noImplicitThis": true, // 当 this 表达式值为 any 类型的时候, 生成一个错误
"alwaysStrict": true, // 以严格模式检查每个模块, 并在每个文件里加入 'use strict'

/* 额外的检查 */
"noUnusedLocals": true, // 有未使用的变量时, 抛出错误
"noUnusedParameters": true, // 有未使用的参数时, 抛出错误
"noImplicitReturns": true, // 并不是所有函数里的代码都有返回值时, 抛出错误
"noFallthroughCasesInSwitch": true, // 报告 switch 语句的 fallthrough 错误. (即, 不允许 switch 的 case 语句贯穿)

/* 模块解析选项 */
"moduleResolution": "node", // 选择模块解析策略: 'node' (Node.js) or 'classic' (TypeScript pre-1.6)
"baseUrl": "./", // 用于解析非相对模块名称的基目录
"paths": {}, // 模块名到基于 baseUrl 的路径映射的列表
"rootDirs": [], // 根文件夹列表, 其组合内容表示项目运行时的结构内容
"typeRoots": [], // 包含类型声明的文件列表
"types": [], // 需要包含的类型声明文件名列表
"allowSyntheticDefaultImports": true, // 允许从没有设置默认导出的模块中默认导入.

/* Source Map Options */
"sourceRoot": "./", // 指定调试器应该找到 TypeScript 文件而不是源文件的位置
"mapRoot": "./", // 指定调试器应该找到映射文件而不是生成文件的位置
"inlineSourceMap": true, // 生成单个 soucemaps 文件, 而不是将 sourcemaps 生成不同的文件
"inlineSources": true, // 将代码与 sourcemaps 生成到一个文件中, 要求同时设置了 --inlineSourceMap 或 --sourceMap 属性

/* 其他选项 */
"experimentalDecorators": true, // 启用装饰器
"emitDecoratorMetadata": true // 为装饰器提供元数据的支持
}
}

tsconfig.json

好的 IDE 支持对 TypeScript 的即时编译.

  • 运行 tsc, 它会在当前目录或者是父级目录寻找  tsconfig.json  文件.
  • 运行  tsc -p ./path-to-project-directory   绝对路径和相对路径都支持.

你甚至可以使用  tsc -w  来启用 TypeScript 编译器的 watch 模式, 在检测到文件改动之后, 它将重新编译.

Files for compile

你也可以显式指定需要编译的文件:

1
2
3
4
5
{
"files": [
"./some/file.ts"
]
}

你还可以使用 includeexclude 选项来指定需要包含的文件和排除的文件:

1
2
3
4
5
6
7
8
9
{
"include": [
"./folder"
],
"exclude": [
"./folder/**/*.spec.ts",
"./folder/someSubFolder"
]
}

TIP

使用 globs : **/* (一个示例用法: some/folder/**/* ) 意味着匹配所有的文件夹和所有文件 (扩展名为 .ts/.tsx , 当开启了 allowJs: true 选项时, 扩展名可以是 .js/.jsx ) .

Declaration Spaces

  1. TS 有两种 Declaration Spaces:
    1. variable declaration space
    2. type declaration space
  2. 除了 interface 外 的 type 都可以转换成 variable
  3. var 关键字只能用于 variable

Type Declaration Spaces

1
2
3
4
/* 能用做 type 标记的都是 Type Declaration */
class Foo {};
interface Bar {};
type Bas = {};

使用方法:

1
2
3
4
/* 直接用作了 Type */
var foo: Foo;
var bar: Bar;
var bas: Bas;

Variable Declaration Spaces

所有的变量型都是 Variable Declaration

需要注意的是 interface 无法赋值给变量, 即对于 interface 无法将 Type 转为 Variable

1
2
interface Bar {};
var bar = Bar; // ERROR: "cannot find name 'Bar'"

对于其他 Type 却可以这做:

1
2
3
class Foo {};
var someVar = Foo;
var someOtherVar = 123;

Modules

Global Modules

一般情况下 TS 文件里面直接声明变量就是一个 global 的变量

1
var foo = 123;

然后就可以在另一个新文件 new.ts 里面访问这个变量:

1
var bar = foo; // 可以访问

毋庸置疑,使用全局变量空间是危险的,因为它会与文件内的代码命名冲突。

File Module

也被称作 外部模块 external modules.

在 root level 使用 importexport 就可以创建一个本地域

比如如果上面那个变量改为使用 export

1
export var foo = 123;

直接访问就无效了

1
var bar = foo; // ERROR: "cannot find name 'foo'"

显式地 import 才能访问

1
2
import { foo } from "./foo";
var bar = foo; // allowed

使用 import 的同时也让 TS 把当前文件认定为一个模块

TS 生成 JS 的过程实际上用到了一个叫 module 的模块

File Module Details

Clarification: commonjs, amd, es modules, others

你可以根据不同的 module 选项来把 TypeScript 编译成不同的 JavaScript 模块类型, 这有一些你可以忽略的东西:

  • AMD: 不要使用它, 它仅能在浏览器工作;
  • SystemJS: 这是一个好的实验, 已经被 ES 模块替代;
  • ES 模块: 它并没有准备好.

建议使用 module: commonjs 选项来替代这些模式.

TIP

tsconfig.json 使用 module: commonjs 选项以及 使用 ES 模块语法 导入, 导出, 编写模块.

ES Module syntax

很多种 export 的方式:

  • 使用 export 关键字导出一个变量或类型
1
2
3
4
5
// foo.ts
export const someVar = 123;
export type someType = {
foo: string;
};
  • export 的写法除了上面这种, 还有另外一种:
1
2
3
4
5
6
7
// foo.ts
const someVar = 123;
type someType = {
type: string;
};

export { someVar, someType as aDifferentName}; /* 还可以给导出的变量名重命名 */
  • 使用 import 关键字导入一个变量或者是一个类型:
1
2
// bar.ts
import { someVar, someType as aDifferentName} from './foo'; /* 同样的可以给导出的变量名重命名 */
  • 除了指定加载某个输出值, 还可以使用整体加载, 即用星号 (*) 指定一个对象, 所有输出值都加载在这个对象上面:
1
2
3
// bar.ts
import * as foo from './foo';
// 你可以使用 `foo.someVar` 和 `foo.someType` 以及其他任何从 `foo` 导出的变量或者类型
  • 只导入模块:
1
import 'core-js'; // 一个普通的 polyfill 库
  • 从其他模块导入后整体导出:
1
export * from './foo';
  • 从其他模块导入后, 部分导出:
1
export { someVar, anotherVar as aDifferentName } from './foo';

Default exports/imports

  • 使用 export default
    • 在一个变量之前 (不需要使用 let/const/var )
    • 在一个函数之前
    • 在一个类之前.
1
2
3
4
5
6
// some var
export default someVar = 123;
// OR Some function
export default function someFunction() { }
// OR Some class
export default class SomeClass { }

导入的格式:

1
2
/* 没有大括号了 */
import someLocalNameForThisFile from "../foo";

Module paths

TIP

如果你需要使用 moduleResolution: node 选项, 你应该将此选项放入你的配置文件中. 如果使用了 module: commonjs 选项, moduleResolution: node 将会默认开启.

这里存在两种截然不同的模块:

  • 相对模块路径 (路径以 . 开头, 例如: ./someFile 或者 ../../someFolder/someFile 等) ;
  • 其他动态查找模块 (如: core-js , typestyle , react 或者甚至是 react/core 等) .

它们的主要区别在于系统如何解析模块.

Relative path modules

Dynamic lookup

当导入路径不是相对路径时, 模块解析将会模仿 Node 模块解析策略, 下面我将给出一个简单例子:

  • 当你使用 import * as foo from 'foo' , 将会参考如下的 查找目标 顺序查找:
    • ./node_modules/foo
    • ../node_modules/foo
    • ../../node_modules/foo
    • 直到系统的根目录
  • 当你使用 import * as foo from 'something/foo' , 将会参考如下的 查找目标 顺序查找:
    • ./node_modules/something/foo
    • ../node_modules/something/foo
    • ../../node_modules/something/foo
    • 直到系统的根目录

对于上面的各种 查找目标, 并不是直接指对应的目录, 参考如下逻辑:

  • 如果这个 查找目标 表示一个文件,如:foo.ts,欢呼!
  • 否则,如果这个 查找目标 是一个文件夹,并且存在一个文件 foo/index.ts,欢呼!
  • 否则,如果这个 查找目标 是一个文件夹,并且存在一个 foo/package.json 文件,在该文件中指定 types 的文件存在,那么就欢呼!
  • 否则,如果这个 查找目标 是一个文件夹,并且存在一个 package.json 文件,在该文件中指定 main 的文件存在,那么就欢呼!

然后对于文件实际上是指 .ts.d.ts 或者 .js 等文件

Overturning dynamic lookup just for types

项目里可以通过 declare module 'somePath' 声明一个全局模块的方式,来解决查找模块路径的问题。

1
2
3
4
5
// global.d.ts
declare module 'foo' {
// some variable declarations
export var bar: number;
}

接着 :

1
2
3
4
// anyOtherTsFileInYourProject.ts
import * as foo from 'foo';
// TypeScript 将假设(在没有做其他查找的情况下)
// foo 是 { bar: number }

import/require for importing type only

以下导入语法:

1
import foo = require('foo');

它实际上只做了两件事:

  • 导入 foo 模块的所有类型信息;
  • 确定 foo 模块运行时的依赖关系.

TIP

**如果没有把导入的名称当做 Variable Declaration 来用, **在编译成 JavaScript 时, 导入的模块将会被完全移除.

这最好用例子来解释, 下面我们将会给出一些示例.

Example 1

1
import foo = require('foo');

最终 build 就会变成空的

1

Example 2

1
2
import foo = require('foo');
var bar: foo;

将会被编译成:

1
let bar;

这是因为 foo (或者其他任何属性如:foo.bas)没有被当做一个变量使用。

Example 3

1
2
import foo = require('foo');
const bar = foo;

将会被编译成(假设是 commonjs):

1
2
const foo = require('foo');
const bar = foo;

这是因为 foo 被当做变量使用了。

Use case: Lazy loading

有些时候只想在运行时再来加载一些模块

1
2
import foo = require('foo');
var bar: foo.SomeType;

然而, 在某些情景下, 你只想在需要时加载模块 foo , 此时你需要仅在类型注解中使用导入的模块名称, 而是在变量中使用. 在编译成 JavaScript 时, 这些将会被移除. 接着, 你可以手动导入你需要的模块.

作为一个例子, 考虑以下基于 commonjs 的代码, 我们仅在一个函数内导入 foo 模块:

1
2
3
4
5
6
7
8
import foo = require('foo');
export function loadFoo() {
// 这是懒加载 foo, 原始的加载仅仅用来做类型注解
const _foo: typeof foo = require('foo');
// 现在, 你可以使用 `_foo` 替代 `foo` 来作为一个变量使用
}

/* 原理: 第一个 require('foo') 就加载了类型, 第二个 require('foo') 才是真正进行加载 */

一个同样简单的 amd 模块 (使用 requirejs) :

1
2
3
4
5
6
7
8
import foo = require('foo');

export function loadFoo() {
// 这是懒加载 foo, 原始的加载仅仅用来做类型注解
require(['foo'], (_foo: typeof foo) => {
// 现在, 你可以使用 `_foo` 替代 `foo` 来作为一个变量使用
});
}

这些通常在以下情景使用:

  • 在 web app 里, 当你在特定路由上加载 JavaScript 时;
  • 在 node 应用里,当你只想加载特定模块,用来加快启动速度时。

Use case: Breaking Circular dependencies

类似于懒加载的使用用例,某些模块加载器(commonjs/node 和 amd/requirejs)不能很好的处理循环依赖。在这种情况下,一方面我们使用延迟加载代码,并在另一方面预先加载模块是很实用的。

Use case: Ensure Import

当你加载一个模块,只是想引入其附加的作用(如:模块可能会注册一些像 CodeMirror addons)时,然而,如果你仅仅是 import/require (导入)一些并没有与你的模块或者模块加载器有任何依赖的 JavaScript 代码,(如:webpack),经过 TypeScript 编译后,这些将会被完全忽视。在这种情况下,你可以使用一个 ensureImport 变量,来确保编译的 JavaScript 依赖与模块。如:

1
2
3
4
5
import foo = require('./foo');
import bar = require('./bar');
import bas = require('./bas');

const ensureImport: any = foo || bar || bas;

global.d.ts

在上文中,当我们讨论文件模块时,比较了全局变量与文件模块,并且我们推荐使用基于文件的模块,而不是选择污染全局命名空间。

然而,如果你的团队里有 TypeScript 初学者,你可以提供他们一个 global.d.ts 文件,用来将一些接口或者类型放入全局命名空间里,这些定义的接口和类型能在你的所有 TypeScript 代码里使用。

对于任何需要编译成 JavaScript 的代码,我们强烈建议你放入文件模块里。

  • global.d.ts 是一种扩充 lib.d.ts 很好的方式,如果你需要的话。
  • 当你从 JS 迁移到 TS 时,定义 declare module "some-library-you-dont-care-to-get-defs-for" 能让你快速开始。

另外这个文件也可以通过 Webpack 的一些插件来 inject 到编译的过程中。

1
2
3
// can be used for conditional compiling
declare const BUILD_MODE_PRODUCTION: boolean;
declare const BUILD_VERSION: string;

Namespaces

在 JavaScript 使用命名空间时, 这有一个常用的、方便的语法:

1
2
3
(function(something) {
something.foo = 123;
})(something || (something = {}));

something || (something = {}) 允许匿名函数 function (something) {} 向现有对象添加内容,或者创建一个新对象,然后向该对象添加内容。这意味着你可以拥有两个由某些边界拆成的块。

1
2
3
4
5
6
7
8
9
10
11
12
(function(something) {
something.foo = 123;
})(something || (something = {}));

console.log(something);
// { foo: 123 }

(function(something) {
something.bar = 456;
})(something || (something = {}));

console.log(something); // { foo: 123, bar: 456 }

在确保创建的变量不会泄漏至全局命名空间时,这种方式在 JavaScript 中很常见。当基于文件模块使用时,你无须担心这点,但是该模式仍然适用于一组函数的逻辑分组。因此 TypeScript 提供了 namespace 关键字来描述这种分组,如下所示。

1
2
3
4
5
6
7
8
9
10
11
12
namespace Utility {
export function log(msg) {
console.log(msg);
}
export function error(msg) {
console.log(msg);
}
}

// usage
Utility.log('Call me');
Utility.error('maybe');

namespace 关键字编译后的 JavaScript 代码,与我们早些时候看到的 JavaScript 代码一样。

1
2
3
(function (Utility) {
// 添加属性至 Utility
})(Utility || Utility = {});

值得注意的一点是,命名空间是支持嵌套的。因此,你可以做一些类似于在 Utility 命名空间下嵌套一个命名空间 Messaging 的事情。

对于大多数项目,我们建议使用外部模块和命名空间,来快速演示和移植旧的 JavaScript 代码。

Typings

Interfaces

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
interface Point {
x: number;
y: number;
z: number; // New member
}

class MyPoint implements Point {
// ERROR : missing member `z`
x: number;
y: number;
}


/* 需要注意的是, initialize 一个 class 的时候需要用到 new 关键字 */
let foo: Point = new MyPoint();

/* 下面这样使用是不行的 */
foo: Point = MyPoint

lib.d.ts

lib.d.ts 的内容主要是一些变量声明(如:windowdocumentmath)和一些类似的接口声明(如:WindowDocumentMath)。

  • 如果是一个库, 建议将一些 global 的声明放在这 global.d.ts
    • 也可以将所有的 type 和 interface 等定义放到这里面
    • 另外对于 type 也可以单独放在其他 ts 文件里面 (这样可以通过文件名区分不同方面的typing)

另外 tsconfig.json 文件里面包含了 lib 的引用, 比如:

1
2
3
4
"compilerOptions": {
"target": "es5",
"lib": ["es6", "dom"]
}

修改原始类型

通过 lib.d.ts 或者 global.d.ts 都可以修改原始类型:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
interface Window {
helloWorld(): void;
}



// Add it at runtime
window.helloWorld = () => console.log('hello world');

// Call it
window.helloWorld();

// 滥用会导致错误
window.helloWorld('gracius'); // Error: 提供的参数与目标不匹配

然后还可以给 Date, String 等多个原始类型添加成员

Functions

Overrides

1
2
3
4
5
// 重载
// 越来越像 JAVA 了
function padding(all: number);
function padding(topAndBottom: number, leftAndRight: number);
function padding(top: number, right: number, bottom: number, left: number);

Function Declaration

1
2
3
4
5
6
// 其实有两种方法进行 Function 的 type 定义
type LongHand = {
(a: number): number;
};

type ShortHand = (a: number) => number;

但是, 当想使用函数重载时, 只能用第一种方式:

1
2
3
4
type LongHandAllowsOverloadDeclarations = {
(a: number): number;
(a: string): string;
};

Callable

其实使用一个接口也可以进行一系列的重载

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

// 这个地方只定义了一个接口
interface Overloaded {
// 但是可以看到这里进行了一些重载
(foo: string): string;
(foo: number): number;
}

// 实现接口的一个例子
function stringOrNumber(foo: number): number;
function stringOrNumber(foo: string): string;
function stringOrNumber(foo: any): any {
if (typeof foo === 'number') {
return foo * foo;
} else if (typeof foo === 'string') {
return `hello ${foo}`;
}
}

// 在这个地方将 stringOrNumber 赋值给 overloaded 并且类型为 Overloaded
const overloaded: Overloaded = stringOrNumber;

// 然后实际使用的时候就可以根据不同传值来进行调用的.
const str = overloaded(''); // str 被推断为 'string'
const num = overloaded(123); // num 被推断为 'number'

并且这个方法可以用在另一种内联的声明语法中:

1
2
3
4
let overloaded: {
(foo: string): string;
(foo: number): number;
};

另外有一个地方需要注意的是,箭头函数无法重载:

1
const simple: (foo: number) => string = foo => foo.toString();

Type Compatibility

类型重赋值 Type Re-Assignment

一个变量已确定类型后, 可以将其改为继承链上的其他类型, 但是会有一些麻烦:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Animal {
constructor(public name: string) {}
}
class Cat extends Animal {
meow() {
console.log('cat');
}
}

let animal = new Animal('animal');
let cat = new Cat('cat');

// 多态
// Animal <= Cat

/* ------------ POINT 1 ------------ */
/* 最好不要强行赋值, 否则会出很多麻烦 */
animal = cat; // ok
cat = animal; // ERROR: cat 继承于 animal

// 演示每个数组形式
let animalArr: Animal[] = [animal];
let catArr: Cat[] = [cat];

// 明显的坏处, 逆变
// Animal <= Cat
// Animal[] >= Cat[]
catArr = animalArr; // ok, 如有有逆变
catArr[0].meow(); // 允许, 但是会在运行时报错

// 另外一个坏处, 协变
// Animal <= Cat
// Animal[] <= Cat[]
animalArr = catArr; // ok, 协变

animalArr.push(new Animal('another animal')); // 仅仅是 push 一个 animal 至 carArr 里
catArr.forEach(c => c.meow()); // 允许, 但是会在运行时报错.

类比较 Class Comparation

类之间不建议进行各种比较 (Are you crazy?), 因为 constructor 和 static properties 不会被用于比较.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class Animal {
feet: number;
constructor(name: string, numFeet: number) {}
}

class Size {
feet: number;
constructor(meters: number) {}
}

let a: Animal;
let s: Size;

a = s; // OK
s = a; // OK
/* 构造函数和静态成员显然不同, 但是没有进行比较. */

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class Animal {
protected feet: number;
}
class Cat extends Animal {}

let animal: Animal;
let cat: Cat;

animal = cat; // ok
cat = animal; // ok

class Size {
protected feet: number;
}

let size: Size;

animal = size; // ERROR
size = animal; // ERROR
/* 另外, 即使他们有相同的 protect field 也被认作不同, 因为两个相同的 protected field 并没有来自相同的一个类 */

泛型 Generics

也没有什么特别需要注意的, 泛型在确定类型之前是属于 any 类型

1
2
3
4
5
6
7
8
9
let identity = function<T>(x: T): T {
// ...
}

let reverse = function<U>(y: U): U {
// ...
}

identity = reverse; // Okay because (x: any)=>any matches (y: any)=>any

Never vs Void

  • Never 表示永远不存在值的一种类型, 说到底他依然是一种类型。
  • Void 表示没有任何类型.

这个的区别实际上就有点像 null 和 undefined

但是有一个地方需要注意, 函数的不同声明方式将会有不同的返回类型:

1
2
3
4
5
6
7
8
9
10
11
// Inferred return type: void
/* 注意一下, 这个时候是返回 void 类型 */
function failDeclaration(message: string) {
throw new Error(message);
}

// Inferred return type: never
/* 这个时候是返回 never 类型 */
const failExpression = function(message: string) {
throw new Error(message);
};

另外对于上方返回 void 可以强制改成 never:

1
2
3
function failDeclaration(message: string): never {
throw new Error(message);
}

上面这个情况发生的原因是:

1
2
3
4
5
6
7
8
9
10
11
12
class Base {
overrideMe() {
throw new Error("You forgot to override me!");
}
}

class Derived extends Base {
overrideMe() {
/* 我没有手动设定返回值, 那么就会使用父类的函数, 返回一个 never */
// Code that actually returns here
}
}

联合类型示例: Redux - Typescript

这里是一个 Redux 的 Type Check 的例子:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
import { createStore } from 'redux';

type Action =
| {
type: 'INCREMENT';
}
| {
type: 'DECREMENT';
};

/**
* This is a reducer, a pure function with (state, action) => state signature.
* It describes how an action transforms the state into the next state.
*
* The shape of the state is up to you: it can be a primitive, an array, an object,
* or even an Immutable.js data structure. The only important part is that you should
* not mutate the state object, but return a new object if the state changes.
*
* In this example, we use a `switch` statement and strings, but you can use a helper that
* follows a different convention (such as function maps) if it makes sense for your
* project.
*/
function counter(state = 0, action: Action) {
switch (action.type) {
case 'INCREMENT':
return state + 1;
case 'DECREMENT':
return state - 1;
default:
return state;
}
}

// Create a Redux store holding the state of your app.
// Its API is { subscribe, dispatch, getState }.
let store = createStore(counter);

// You can use subscribe() to update the UI in response to state changes.
// Normally you'd use a view binding library (e.g. React Redux) rather than subscribe() directly.
// However it can also be handy to persist the current state in the localStorage.

store.subscribe(() => console.log(store.getState()));

// The only way to mutate the internal state is to dispatch an action.
// The actions can be serialized, logged or stored and later replayed.
store.dispatch({ type: 'INCREMENT' });
// 1
store.dispatch({ type: 'INCREMENT' });
// 2
store.dispatch({ type: 'DECREMENT' });
// 1

indexSignatures

声明一个索引签名

1
2
3
4
5
6
7
8
9
const foo: {
[xxx: string]: { message: string };
/* 这里的 xxx 除了代码可读性参考外没有任何用处, 可以换用任何名称 */
} = {};

const foo2: {
[zzz: number]: { message: string };
/* 可以看见 number 类型也可以接受 */
} = {};

使用一组有限的字符串字面量进行声明:

1
2
3
4
5
6
type Index = 'a' | 'b' | 'c';

/* 这里使用了 in, 其他情况下也可以考虑使用 keyof, typeof 等 */
type FromIndex = { [k in Index]?: number };

const good: FromIndex = { b: 1, c: 2 };

索引签名的嵌套 Nested index signature

例如下方这一段:

1
2
3
4
5
6
7
8
9
10
11
interface NestedCSS {
color?: string;
[selector: string]: string | NestedCSS | undefined;
}

const failsSilently: NestedCSS = {
// No error as `colour` is a valid string selector
// 本身是一个错误的 index, 但是因为被另一个索引签名捕捉因此不会报错.
colour: 'red',
}

这种情况下建议放到一个嵌套的key里面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
interface NestedCSS {
color?: string;
nest?: {
/* 这样子索引签名就被放到了下一层。 */
[selector: string]: NestedCSS;
}
}

const example: NestedCSS = {
color: 'red',
nest: {
'.subclass': {
color: 'blue'
}
}
}

Mixins 混合

采用函数 B 接受一个类 A, 并且返回一个带有新功能的类的方式来替代 A 类扩展 B 来获取 B 上的功能, 前者中的 B 即是混合.

TIP

「混合」是一个函数:

传入一个构造函数;
创建一个带有新功能, 并且扩展构造函数的新类;
返回这个新类.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
// 所有 mixins 都需要
type Constructor<T = {}> = new (...args: any[]) => T;

/////////////
// mixins 例子
////////////

// 添加属性的混合例子
function TimesTamped<TBase extends Constructor>(Base: TBase) {
return class extends Base {
timestamp = Date.now();
};
}

// 添加属性和方法的混合例子
function Activatable<TBase extends Constructor>(Base: TBase) {
return class extends Base {
isActivated = false;

activate() {
this.isActivated = true;
}

deactivate() {
this.isActivated = false;
}
};
}

///////////
// 组合类
///////////

// 简单的类
class User {
name = '';
}

// 添加 TimesTamped 的 User
const TimestampedUser = TimesTamped(User);

// Tina TimesTamped 和 Activatable 的类
const TimestampedActivatableUser = TimesTamped(Activatable(User));

//////////
// 使用组合类
//////////

const timestampedUserExample = new TimestampedUser();
console.log(timestampedUserExample.timestamp);

const timestampedActivatableUserExample = new TimestampedActivatableUser();
console.log(timestampedActivatableUserExample.timestamp);
console.log(timestampedActivatableUserExample.isActivated);

TIPS

科里化

箭头函数连着用就能将函数柯里化

1
2
3
4
5
6
7
8
9
10
11
12
// 一个柯里化函数, 将原本接收两个参数的函数变为单参数函数的两次调用
let add = (x: number) => (y: number) => x + y;

// 简单使用
add(123)(456);

// 部分应用
let add123 = add(123);

// fully apply the function
add123(456);

Singleton Pattern

实际上就是全局的一个统一变量, 使用 import 模块化导入已经隐式的创造了一种单例模式

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
class Singleton {
private static instance: Singleton;
private constructor() {
// ..
}

public static getInstance() {
if (!Singleton.instance) {
Singleton.instance = new Singleton();
}

return Singleton.instance;
}

someMethod() {}
}

let someThing = new Singleton(); // Error: constructor of 'singleton' is private
let instacne = Singleton.getInstance(); // do some thing with the instance

然而,如果你不想延迟初始化,你可以使用 namespace 替代:

1
2
3
4
5
6
7
8
namespace Singleton {
// .. 其他初始化的代码

export function someMethod() {}
}

// 使用
Singleton.someMethod();

对大部分使用者来说,namespace 可以用模块来替代。

1
2
3
4
5
6
// someFile.ts
// ... any one time initialization goes here ...
export function someMethod() {}

// Usage
import { someMethod } from './someFile';