docs: first commit

This commit is contained in:
ruanyf
2023-07-09 12:07:22 +08:00
commit 111aa6ef94
35 changed files with 15133 additions and 0 deletions

1
README.md Normal file
View File

@@ -0,0 +1 @@
TypeScript 开源教程,介绍基本概念和用法,面向初学者。

1
chapters.yml Normal file
View File

@@ -0,0 +1 @@
- intro.md: 简介

218
docs/any.md Normal file
View File

@@ -0,0 +1,218 @@
# any 类型unknown 类型never 类型
本章介绍 TypeScript 的三种特殊类型,它们可以作为学习 TypeScript 类型系统的起点。
## any 类型
### 基本含义
any 类型表示该位置不限制类型,任意类型的值都可以使用。
```typescript
let x:any;
x = 1; // 正确
x = 'foo'; // 正确
x = true; // 正确
```
上面示例中,变量`x`的类型是`any`,就可以被赋值为任意类型的值。
变量类型一旦设为`any`TypeScript 实际上会关闭它的类型检查,即使有明显的类型错误,只要句法正确,都不会报错。
```typescript
let x:any = 'hello';
x(1) // 正确
x.foo = 100; // 正确
```
上面示例中,变量`x`的值是一个字符串但是把它当作函数调用或者当作对象读取任意属性TypeScript 编译时都不报错。原因就是`x`的类型是`any`TypeScript 不对其进行类型检查。
实际项目中,`any`类型往往用于关闭某些变量的类型检查。由于这个原因,应该尽量避免使用`any`类型,否则就失去了使用 TypeScript 的意义。
这个类型的主要设计目的,是为了适配以前老的 JavaScript 项目的迁移。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时你为那些类型复杂的变量加上`any`TypeScript 编译时就不会报错。不过,这大概是`any`唯一的适用场合。
总之TypeScript 认为,只要开发者使用了`any`类型,就表示开发者想要自己来处理这些代码,所以就不对`any`类型进行任何限制,怎么使用都可以。
### 类型推断问题
`any`类型的另一个出现场景是对于那些开发者没有指定类型、TypeScript 必须自己推断类型的变量如果这时无法推断出类型TypeScript 就会认为该变量的类型是`any`
```typescript
function add(x, y) {
return x + y;
}
add(1, [1, 2, 3]) // 正确
```
上面示例中,函数`add()`的参数变量`x``y`都没有足够的信息TypeScript 无法推断出它们的类型,就会认为这些变量的类型是`any`。以至于后面就不再对函数`add()`进行类型检查了,怎么用都可以。
这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要明确声明类型,防止推断为`any`
TypeScript 提供了一个编译选项`--noImplicitAny`,只要打开这个选项,推断不出类型就会报错。
```bash
$ tsc --noImplicitAny app.ts
```
上面命令就使用了`--noImplicitAny`编译选项进行编译,这时上面的函数`add()`就会报错。
### 污染问题
`any`类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。
```typescript
let x:any = 'hello';
let y:number;
y = x; // 正确
y * 123 // 正确
y.toFixed() // 正确
```
上面示例中,变量`x`的类型是`any`,实际的值是一个字符串。数值类型的变量`y`被赋值为`x`,也不会报错。然后,变量`y`继续进行各种数值运算TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。
污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用`any`类型的另一个主要原因。
### 顶端类型
前面说过,`any`类型可以被赋值为任何类型的值。在 TypeScript 语言中,如果类型`A`可以被赋值为类型`B`,那么类型`A`称为父类型,类型`B`称为子类型。TypeScript 的一个规则是,凡是可以使用父类型的地方,都可以使用子类型。
由于任何值都可以赋值给`any`类型,所以`any`类型是 TypeScript 所有其他类型的父类型,或者说,所有其他类型都是`any`的子类型。
所以,`any`类型是 TypeScript 的一个基础类型包含了一切可能的值。所有其他类型都可以看成是它的衍生类型它又被称为顶端类型top type
## unknown 类型
为了解决`any`类型“污染”其他变量的问题TypeScript 3.0 引入了[`unknown`类型](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type)。它与`any`含义相同,表示类型不确定,但是使用上有一些限制,可以视为严格版的`any`
`unknown``any`的相似之处,在于所有类型的值都可以分配给`unknown`类型。
```typescript
let x:unknown;
x = true; // 正确
x = 42; // 正确
x = 'Hello World'; // 正确
```
上面示例中,变量`value`的类型是`unknown`,可以赋值为各种类型的值。这与`any`的行为一致。
`unknown`类型跟`any`类型的不同之处在于,它不能直接使用。主要有以下几个限制。
首先,`unknown`类型的变量,不能直接赋值给其他类型的变量(除了`any`类型和`unknown`类型)。
```typescript
let v:unknown = 123;
let v1:boolean = v; // 报错
let v2:number = v; // 报错
```
上面示例中,变量`v``unknown`类型,赋值给`any``unknown`以外类型的变量都会报错,这就避免了污染问题,从而克服了`any`类型的一大缺点。
另外,也不能直接调用`unknown`类型变量的方法和属性。
```typescript
let v1:unknown = { foo: 123 };
v1.foo // 报错
let v2:unknown = 'hello';
v2.trim() // 报错
let v3:unknown = (n = 0) => n + 1;
v3() // 报错
```
上面示例中,直接调用`unknown`类型变量的属性和方法,或者直接当作函数执行,都会报错。
再次,`unknown`类型变量能够进行的运算是有限的,只能进行比较运算(运算符`==``===``!=``!==``||``&&``?`)、取反运算(运算符`!`)、`typeof`运算符和`instanceof`运算符这几种,其他运算都会报错。
```typescript
let a:unknown = 1;
a + 1 // 报错
a === 1 // 正确
```
上面示例中,`unknown`类型的变量`a`进行加法运算会报错,因为这是不允许的运算。但是,进行比较运算就是可以的。
那么,怎么才能使用`unknown`类型变量呢?
答案是只有经过“类型细化”refine`unknown`类型变量才可以使用。所谓“类型细化”,就是缩小`unknown`变量的类型范围,确保不会出错。
```typescript
let a:unknown = 1;
if (typeof a === 'number') {
let r = a + 10; // 正确
}
```
上面示例中,`unknown`类型的变量`a`经过`typeof`运算以后,能够确定实际类型是`number`,就能用于加法运算了。这就是“类型细化”,即将一个不确定的类型细化为更明确的类型。
下面是另一个例子。
```typescript
let s:unknown = 'hello';
if (typeof s === 'string') {
s.length; // 正确
}
```
上面示例中,确定变量`s`的类型为字符串以后,才能调用它的`length`属性。
这样设计的目的是,只有明确`unknown`变量的实际类型,才允许使用它,防止像`any`那样可以随意乱用,“污染”其他变量。类型细化以后再使用,就不会报错。
总之,`unknown`可以看作是更安全的`any`,凡是需要设为`any`的地方,通常都应该优先考虑设为`unknown`
由于`unknown`类型的变量也可以被赋值为任意其他类型,所以其他类型(除了`any`)都可以视为它的子类型。所以,它和`any`一样都属于 TypeScript 的顶端类型。
## never 类型
类型也可能是空集即不包含任何类型。为了逻辑的完整性TypeScript 把这种情况也当作一种类型,叫做`never`类型。
`never`类型表示不可能的类型,也就是不可能有任何值属于这个类型。
```typescript
let x: never;
```
上面示例中,变量`x`的类型是`never`,就不可能赋给它任何值,都会报错。
`never`类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性,详见后面章节。另外,不可能返回值的函数,返回值的类型就可以写成`never`,详见《函数》一章。
如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于`never`类型。
```typescript
function fn(x:string|number) {
if (typeof x === 'string') {
// ...
} else if (typeof x === 'number') {
// ...
} else {
x; // never 类型
}
}
```
上面示例中,参数变量`x`可能是字符串,也可能是数组,判断了这两种情况后,剩下的`else`分支里面,`x`就是`never`类型了。
任何类型的变量都可以赋值为`never`类型。
```typescript
function f():never {
throw new Error('Error');
}
let v1:number = f(); // 正确
let v2:string = f(); // 正确
let v3:string = f(); // 正确
```
上面示例中,函数`f()`会抛错,所以返回值类型可以写成`never`,即不可能返回任何值。各种其他类型的变量都可以赋值为`f()`的运行结果(`never`类型)。
前面说过,在 TypeScript 中,如果类型`A`可以被赋值为类型`B`,那么类型`B`就称为类型`A`的子类型。所以,`never`类型可以视为所有其他类型的子类型表示不包含任何可能的值这种情况叫做“尾端类型”bottom type`never`是 TypeScript 唯一的尾端类型。

234
docs/array.md Normal file
View File

@@ -0,0 +1,234 @@
# TypeScript 的数组类型
JavaScript 数组在 TypeScript 里面分成两种类型分别是数组array和元组tuple
本章介绍数组,下一章介绍元组。
## 简介
TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。
数组的类型有两种写法。第一种写法是在数组成员的类型后面,加上一对方括号。
```typescript
let arr:number[] = [1, 2, 3];
```
上面示例中,数组`arr`的类型是`number[]`,其中`number`表示成员类型是`number`
如果数组成员的类型比较复杂,可以写在圆括号里面。
```typescript
let arr:(number|string)[];
```
上面示例中,数组`arr`的成员类型是`number|string`
这个例子里面的圆括号是必须的,否则因为竖杠(`|`)的优先级低于`[]`TypeScript 会把`number|string[]`理解成`number``string[]`的联合类型。
如果数组成员可以是任意类型,则写成`any[]`。当然,这种写法是应该避免的。
```typescript
let arr:any[];
```
数组类型的第二种写法是使用 TypeScipt 内置的 Array 接口。
```typescript
let arr:Array<number> = [1, 2, 3];
```
上面示例中,数组`arr`的类型是`Array<number>`,其中`number`表示成员类型是`number`
这种写法对于成员类型比较复杂的数组,代码可读性会稍微好一些。
```typescript
let arr:Array<number|string>;
```
这种写法本质上属于泛型这里只要知道怎么写就可以了详细解释参见《泛型》一章。另外数组类型还有第三种写法因为很少用到本章就省略了详见《interface 接口》一章。
数组类型声明了以后,成员数量是不限制的,任意数量的成员都可以,也可以是空数组。
```typescript
let arr:number[];
arr = [];
arr = [1];
arr = [1, 2];
arr = [1, 2, 3];
```
上面示例中,数组`arr`无论有多少个成员,都是正确的。
这种规定的隐藏含义就是,数组的成员是可以动态变化的。
```typescript
let arr:number[] = [1, 2, 3];
arr[3] = 4;
arr.length = 2;
arr // [1, 2]
```
上面示例中,数组增加成员或减少成员,都是可以的。
正是由于成员数量可以动态变化,所以 TypeScript 不会对数组边界进行检查,如果越界访问数组并不会报错。
```typescript
let arr:number[] = [1, 2, 3];
let foo = arr[3]; // 正确
```
上面示例中,变量`foo`的值是一个不存在的数组成员TypeScript 并不会报错。
TypeScript 允许使用方括号读取数组成员的类型。
```typescript
type Names = string[];
type Name = Names[0]; // string
```
上面示例中,类型`Names`是字符串数组,那么`Names[0]`返回的类型就是`string`
由于数组成员的索引类型都是`number`,所以读取成员类型也可以写成下面这样。
```typescript
type Names = string[];
type Name = Names[number]; // string
```
上面示例中,`Names[number]`表示元组`Names`所有数值索引的成员类型,所以返回`string`
## 数组的类型推断
如果数组变量没有声明类型TypeScript 就会推断数组成员的类型。
这时,推断行为会因为值的不同,而有所不同。
如果变量的初始值是空数组,那么 TypeScript 会推断数组类型是`any[]`
```typescript
// 推断为 any[]
const arr = [];
```
后面为这个数组赋值时TypeScript 会自动更新类型推断。
```typescript
// 推断为 any[]
const arr = [];
// 推断类型为 number[]
arr.push(123);
// 推断类型为 (string | number)[]
arr.push('abc');
```
上面示例中,数组变量`arr`的初始值是空数值然后随着新成员的加入TypeScript 会自动修改推断的数组类型。
但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。
```typescript
// 推断类型为 number[]
const arr = [123];
arr.push('abc'); // 报错
```
上面示例中,数组变量`arr`的初始值是`[123]`TypeScript 就推断成员类型为`number`。新成员如果不是这个类型TypeScript 就会报错,而不会更新类型推断。
## 只读数组const 断言
JavaScript 规定,`const`命令声明的数组变量是可以改变成员的。
```typescript
const arr = [0, 1];
arr[0] = 2;
```
上面示例中,修改`const`命令声明的数组的成员是允许的。
但是,很多时候确实有声明只读数组的需求,即不允许变动数组成员。
TypeScript 允许声明只读数组,方法是在在数组类型前面加上`readonly`关键字。
```typescript
const arr:readonly number[] = [0, 1];
arr[1] = 2; // 报错
arr.push(3); // 报错
delete arr[0]; // 报错
```
上面示例中,`arr`是一个只读数组,删除、修改、新增数组成员都会报错。
TypeScript 将`readonly number[]``number[]`视为两种不一样的类型,后者是前者的子类型。
这是因为只读数组没有`pop()``push()`之类会改变原数组的方法,所以`number[]`的方法数量要多于`readonly number[]`,这意味着`number[]`其实是`readonly number[]`的子类型。
我们知道,子类型继承了父类型的所有特征,并加上了自己的特征,所以子类型`number[]`可以用于所有使用父类型的场合,反过来就不行。
```typescript
let a1:number[] = [0, 1];
let a2:readonly number[] = a1; // 正确
a1 = a2; // 报错
```
上面示例中,子类型`number[]`可以赋值给父类型`readonly number[]`,但是反过来就会报错。
由于只读数组是数组的父类型,所以它不能代替数组。这一点很容易产生令人困惑的报错。
```typescript
function getSum(s:number[]) {
// ...
}
const arr:readonly number[] = [1, 2, 3];
getSum(arr) // 报错
```
上面示例中,函数`getSum()`的参数`s`是一个数组,传入只读数组就会报错。原因就是只读数组是数组的父类型,父类型不能替代子类型。这个问题的解决方法是使用类型断言`getSum(arr as number[])`,详见《类型断言》一章。
注意,`readonly`关键字不能与数组的泛型写法一起使用。
```typescript
// 报错
const arr:readonly Array<number> = [0, 1];
```
上面示例中,`readonly`与数组的泛型写法一起使用,就会报错。
实际上TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。
```typescript
const a1:ReadonlyArray<number> = [0, 1];
const a2:Readonly<number[]> = [0, 1];
```
上面示例中,泛型`ReadonlyArray<T>``Readonly<T[]>`都可以用来生成只读数组类型。两者尖括号里面的写法不一样,`Readonly<T[]>`的尖括号里面是整个数组(`number[]`),而`ReadonlyArray<T>`的尖括号里面是数组成员(`number`)。
只读数组还有一种声明方法就是使用“const 断言”。
```typescript
const arr = [0, 1] as const;
arr[0] = [2]; // 报错
```
上面示例中,`as const`告诉 TypeScript推断类型时要把变量`arr`推断为只读数组,从而使得数组成员无法改变。
## 多维数组
TypeScript 使用`T[][]`的形式,表示二维数组,`T`是最底层数组成员的类型。
```typescript
var multi:number[][] =
[[1,2,3], [23,24,25]];
```
上面示例中,变量`multi`的类型是`number[][]`,表示它是一个二维数组,最底层的数组成员类型是`number`

587
docs/assert.md Normal file
View File

@@ -0,0 +1,587 @@
# TypeScript 的类型断言
## 简介
对于没有类型声明的值TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。
```typescript
type T = 'a'|'b'|'c';
let foo = 'a';
let bar:T = foo; // 报错
```
上面示例中,最后一行报错,原因是 TypeScript 推断变量`foo`的类型是`string`,而变量`bar`的类型是`'a'|'b'|'c'`,前者是后者的父类型。父类型不能赋值给子类型,所以就报错了。
这时TypeScript 提供了“类型断言”这样一种手段允许开发者在代码中“断言”某个值的类型提示编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。
这种做法的实质是,允许开发者在某个位置“绕过”编译器的类型推断,使其能够通过类型检查,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。
回到上面的例子,解决方法就是进行类型断言,在赋值时断言变量`foo`的类型。
```typescript
type T = 'a'|'b'|'c';
let foo = 'a';
let bar:T = foo as T; // 正确
```
上面示例中,最后一行的`foo as T`表示告诉编译器,变量`foo`的类型断言为`T`,所以这一行不再需要类型推断了,编译器直接把`foo`的类型当作`T`,就不会报错了。
总之,类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值。
类型断言有两种语法。
```typescript
// 语法一:<类型>值
<Type>value
// 语法二:值 as 类型
value as Type
```
上面两种语法是等价的,`value`表示值,`Type`表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。
```typescript
// 语法一
let bar:T = <T>foo;
// 语法二
let bar:T = foo as T;
```
上面示例是两种类型断言的语法,其中的语法一因为跟 JSX 语法冲突,使用时必须关闭 TypeScript 的 React 支持,否则会无法识别。由于这个原因,现在一般都使用语法二。
下面看一个例子。《对象》一章提到过,对象类型有严格字面量检查,如果存在额外的属性会报错。
```typescript
// 报错
const p:{ x: number } = { x: 0, y: 0 };
```
上面示例中,等号右侧是一个对象字面量,多出了属性`y`,导致报错。解决方法就是使用类型断言,可以用两种不同的断言。
```typescript
// 正确
const p0:{ x: number } =
{ x: 0, y: 0 } as { x: number };
// 正确
const p1:{ x: number } =
{ x: 0, y: 0 } as { x: number; y: number };
```
上面示例中,两种类型断言都是正确的。第一种断言将类型改成与等号左边一致,第二种断言使得等号右边的类型是左边类型的子类型,子类型可以赋值给父类型,同时因为存在类型断言,就没有严格字面量检查了,所以不报错。
下面是一个网页编程的实际例子。
```typescript
const username = document.getElementById('username');
if (username) {
(username as HTMLInputElement).value; // 正确
}
```
上面示例中,变量`username`的类型是`HTMLElement|null`,排除了`null`的情况以后HTMLElement 类型是没有`value`属性的。如果`username`是一个输入框,那么就可以通过类型断言,将它的类型改成`HTMLInputElement`,就可以读取`value`属性。
注意,上例的类型断言的圆括号是必需的,否则`username`会被断言成`HTMLInputElement.value`,从而报错。
类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。
```typescript
const data:object = {
a: 1,
b: 2,
c: 3
};
data.length; // 报错
(data as Array<string>).length; // 正确
```
上面示例中,变量`data`是一个对象,没有`length`属性。但是通过类型断言,可以将它的类型断言为数组,这样使用`length`属性就能通过类型检查。但是,编译后的代码在运行时依然会报错,所以类型断言可以让错误的代码通过编译。
类型断言的一大用处是,指定 unknown 类型的变量的具体类型。
```typescript
const value:unknown = 'Hello World';
const s1:string = value; // 报错
const s2:string = value as string; // 正确
```
上面示例中unknown 类型的变量`value`不能直接赋值给其他类型的变量,但是可以将它断言为其他类型,这样就可以赋值给别的变量了。
另外,类型断言也适合指定联合类型的值的具体类型。
```typescript
const s1:number|string = 'hello';
const s2:number = s1 as number;
```
上面示例中,变量`s1`是联合类型,可以断言其为联合类型里面的一种具体类型,再将其赋值给变量`s2`
## 类型断言的前提
类型断言并不意味着,可以把某个值断言为任意类型。
```typescript
const n = 1;
const m:string = n as string; // 报错
```
上面示例中,变量`n`是数值无法把它断言成字符串TypeScript 会报错。
类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。
```typescript
expr as T
```
上面代码中,`expr`是实际的值,`T`是类型断言,它们必须满足下面的条件:`expr``T`的子类型,或者`T``expr`的子类型。
也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。
但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为`any`类型和`unknown`类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。
```typescript
// 或者写成 <T><unknown>expr
expr as unknown as T
```
上面代码中,`expr`连续进行了两次类型断言,第一次断言为`unknown`类型,第二次断言为`T`类型。这样的话,`expr`就可以断言成任意类型`T`,而不报错。
下面是本小节开头那个例子的改写。
```typescript
const n = 1;
const m:string = n as unknown as string; // 正确
```
上面示例中,通过两次类型断言,变量`n`的类型就从数值,变成了完全无关的字符串,从而赋值时不会报错。
## as const 断言
如果没有声明变量类型let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一const 命令声明的变量,则被推断为值类型常量。
```typescript
// 类型推断为 string
let s1 = 'JavaScript';
// 类型推断为 JavaScript
const s2 = 'JavaScript';
```
上面示例中,变量`s1`的类型被推断为`string`,变量`s2`的类型推断为值类型`JavaScript`。后者是前者的子类型,相当于 const 命令有更强的限定作用,可以缩小变量的类型范围。
有些时候let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。
```typescript
let s = 'JavaScript';
type Lang =
|'JavaScript'
|'TypeScript'
|'Python';
function setLang(language:Lang) {
/* ... */
}
setLang(s); // 报错
```
上面示例中,最后一行报错,原因是函数`setLang()`的参数`language`类型是`Lang`,这是一个联合类型。但是,传入的字符串`s`的类型被推断为`string`,属于`Lang`的父类型。父类型不能替代子类型,导致报错。
一种解决方法就是把 let 命令改成 const 命令。
```typescript
const s = 'JavaScript';
```
这样的话,变量`s`的类型就是值类型`JavaScript`,它是联合类型`Lang`的子类型,传入函数`setLang()`就不会报错。
另一种解决方法是使用类型断言。TypeScript 提供了一种特殊的类型断言`as const`,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。
```typescript
let s = 'JavaScript' as const;
setLang(s); // 正确
```
上面示例中,变量`s`虽然是用 let 命令声明的,但是使用了`as const`断言以后,就等同于是用 const 命令声明的,变量`s`的类型会被推断为值类型`JavaScript`
使用了`as const`断言以后let 变量就不能再改变值了。
```typescript
let s = 'JavaScript' as const;
s = 'Python'; // 报错
```
上面示例中let 命令声明的变量`s`,使用`as const`断言以后,就不能改变值了,否则报错。
注意,`as const`断言只能用于字面量,不能用于变量。
```typescript
let s = 'JavaScript';
setLang(s as const); // 报错
```
上面示例中,`as const`断言用于变量`s`,就报错了。
另外,`as const`也不能用于表达式。
```typescript
let s = ('Java' + 'Script') as const; // 报错
```
上面示例中,`as const`用于表达式,导致报错。
`as const`也可以写成前置的形式。
```typescript
// 后置形式
expr as const
// 前置形式
<const>expr
```
`as const`断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。
```typescript
const v1 = {
x: 1,
y: 2,
}; // 类型是 { x: number; y: number; }
const v2 = {
x: 1 as const,
y: 2,
}; // 类型是 { x: 1; y: number; }
const v3 = {
x: 1,
y: 2,
} as const; // 类型是 { readonly x: 1; readonly y: 2; }
```
上面示例中,第二种写法是对属性`x`缩小类型,第三种写法是对整个对象缩小类型。
总之,`as const`会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。
下面是数组的例子。
```typescript
// a1 的类型推断为 number[]
const a1 = [1, 2, 3];
// a2 的类型推断为 readonly [1, 2, 3]
const a2 = [1, 2, 3] as const;
```
上面示例中,数组字面量使用`as const`断言后,类型推断就变成了只读元组。
由于`as const`会将数组变成只读元组,所以很适合用于函数的 rest 参数。
```typescript
function add(x: number, y: number) {
return x + y;
}
const nums = [1, 2];
const total = add(...nums); // 报错
```
上面示例中,变量`nums`的类型推断为`number[]`,导致使用扩展运算符`...`传入函数`add()`会报错,因为`add()`只能接受两个参数,而`...nums`并不能保证参数的个数。
事实上,对于固定参数个数的函数,如果传入的参数包含扩展运算符,那么扩展运算符只能用于元组。只有当函数定义使用了 rest 参数,扩展运算符才能用于数组。
解决方法就是使用`as const`断言,将数组变成元组。
```typescript
const nums = [1, 2] as const;
const total = add(...nums); // 正确
```
上面示例中,使用`as const`断言后,变量`nums`的类型会被推断为`readonly [1, 2]`,使用扩展运算符展开后,正好符合函数`add()`的参数类型。
Enum 成员也可以使用`as const`断言。
```typescript
enum Foo {
X,
Y,
}
let e1 = Foo.X; // Foo
let e2 = Foo.X as const; // Foo.X
```
上面示例中,如果不使用`as const`断言,变量`e1`的类型被推断为整个 Enum 类型;使用了`as const`断言以后,变量`e2`的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。
## 非空断言
对于那些可能为空的变量(即可能等于`undefined``null`TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号`!`
```typescript
function f(x?:number|null) {
validateNumber(x);
console.log(x!.toFixed());
}
function validateNumber(e:number|null) {
// 如果 e 不是数值,就抛出错误
}
```
上面示例中,变量`x`的类型是`number|null`,即可能为空。如果为空,就不存在`.toFixed()`方法,编译时会报错。但是,开发者有时可以确认,变量`x`不会为空,这时就可以使用非空断言,为函数体内部的变量`x`加上后缀`!`,编译就不会报错了。
非空断言在实际编程中很有用,有时可以省去一些额外的判断。
```typescript
const root = document.getElementById('root');
// 报错
root.addEventListener('click', e => {
/* ... */
});
```
上面示例中,`getElementById()`有可能返回空值`null`,即变量`root`可能为空,这时对它调用`addEventListener()`方法就会报错,通不过编译。但是一般来说,开发者可以确认`root`元素肯定会在网页中存在,这时就可以使用非空断言。
```typescript
const root = document.getElementById('root')!;
```
上面示例中,`getElementById()`方法加上后缀`!`,表示这个方法肯定返回非空结果。
非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。
```typescript
const root = document.getElementById('root');
if (root === null) {
throw Error('Unable to find DOM element #root');
}
root.addEventListener('click', e => {
/* ... */
});
```
上面示例中,如果`root`为空会抛错,比非空断言更保险一点。
非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。
```typescript
class Point {
x:number; // 报错
y:number; // 报错
constructor(x:number, y:number) {
// ...
}
}
```
上面示例中,属性`x``y`会报错,因为 TypeScript 认为它们没有初始化。
这时就可以使用非空断言,表示这两个属性肯定有值,这样就不会报错了。
```typescript
class Point {
x!:number; // 正确
y!:number; // 正确
constructor(x:number, y:number) {
// ...
}
}
```
另外,非空断言只有在打开编译选项`strictNullChecks`时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为`undefined``null`
## 断言函数
断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。
```typescript
function isString(value) {
if (typeof value !== 'string')
throw new Error('Not a string');
}
```
上面示例中,函数`isString()`就是一个断言函数,用来保证参数`value`是一个字符串。
下面是它的用法。
```typescript
const aValue:string|number = 'Hello';
isString(aValue);
```
上面示例中,变量`aValue`可能是字符串,也可能是数组。但是,通过调用`isString()`,后面的代码就可以确定,变量`aValue`一定是字符串。
断言函数的类型可以写成下面这样。
```typescript
function isString(value:unknown):void {
if (typeof value !== 'string')
throw new Error('Not a string');
}
```
上面代码中,函数参数`value`的类型是`unknown`,返回值类型是`void`,即没有返回值。可以看到,单单从这样的类型声明,很难看出`isString()`是一个断言函数。
为了更清晰地表达断言函数TypeScript 3.7 引入了新的类型写法。
```typescript
function isString(value:unknown):asserts value is string {
if (typeof value !== 'string')
throw new Error('Not a string');
}
```
上面示例中,函数`isString()`的返回值类型写成`asserts value is string`,其中`asserts``is`都是关键词,`value`是函数的参数名,`string`是函数参数的预期类型。它的意思是,该函数用来断言参数`value`的类型是`string`,如果达不到要求,程序就会在这里中断。
使用了断言函数的新写法以后TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。
注意函数返回值的断言写法只是用来更清晰地表达函数意图真正的检查是需要开发者自己部署的。而且如果内部的检查与断言不一致TypeScript 也不会报错。
```typescript
function isString(value:unknown):asserts value is string {
if (typeof value !== 'number')
throw new Error('Not a number');
}
```
上面示例中,函数的断言是参数`value`类型为字符串,但是实际上,内部检查的却是它是否为数值,如果不是就抛错。这段代码能够正常通过编译,表示 TypeScript 并不会检查断言与实际的类型检查是否一致。
另外,断言函数的`asserts`语句等同于`void`类型,所以如果返回除了`undefined``null`以外的值,都会报错。
```typescript
function isString(value:unknown):asserts value is string {
if (typeof value !== 'string')
throw new Error('Not a string');
return true; // 报错
}
```
上面示例中,断言函数返回了`true`,导致报错。
下面是另一个例子。
```typescript
type AccessLevel = 'r' | 'w' | 'rw';
function allowsReadAccess(
level:AccessLevel
):asserts level is 'r' | 'rw' {
if (!level.includes('r'))
throw new Error('Read not allowed');
}
```
上面示例中,函数`allowsReadAccess()`用来断言参数`level`一定等于`r``rw`
如果要断言参数非空,可以使用工具类型`NonNullable<T>`
```typescript
function assertIsDefined<T>(
value:T
):asserts value is NonNullable<T> {
if (value === undefined || value === null) {
throw new Error(`${value} is not defined`)
}
}
```
上面示例中,工具类型`NonNullable<T>`对应类型`T`去除空类型后的剩余类型。
如果要将断言函数用于函数表达式,可以采用下面的写法。
```typescript
// 写法一
const assertIsNumber = (
value:unknown
):asserts value is number => {
if (typeof value !== 'number')
throw Error('Not a number');
};
// 写法二
type AssertIsNumber =
(value:unknown) => asserts value is number;
const assertIsNumber:AssertIsNumber = (value) => {
if (typeof value !== 'number')
throw Error('Not a number');
};
```
注意断言函数与类型保护函数type guard是两种不同的函数。它们的区别是断言函数不返回值而类型保护函数总是返回一个布尔值。
```typescript
function isString(
value:unknown
):value is string {
return typeof value === 'string';
}
```
上面示例就是一个类型保护函数`isString()`,作用是检查参数`value`是否为字符串。如果是的,返回`true`,否则返回`false`。该函数的返回值类型是`value is string`,其中的`is`是一个类型运算符,如果左侧的值符合右侧的类型,则返回`true`,否则返回`false`
如果要断言某个参数保证为真(即不等于`false``undefined``null`TypeScript 提供了断言函数的一种简写形式。
```typescript
function assert(x:unknown):asserts x {
// ...
}
```
上面示例中,函数`assert()`的断言部分,`asserts x`省略了谓语和宾语,表示参数`x`保证为真(`true`)。
同样的,参数为真的实际检查需要开发者自己实现。
```typescript
function assert(x:unknown):asserts x {
if (!x) {
throw new Error(`${x} should be a truthy value.`);
}
}
```
这种断言函数的简写形式,通常用来检查某个操作是否成功。
```typescript
type Person = {
name: string;
email?: string;
};
function loadPerson(): Person | null {
return null;
}
let person = loadPerson();
function assert(
condition:unknown, message:string
):asserts condition {
if (!condition) throw new Error(message);
}
// Error: Person is not defined
assert(person, 'Person is not defined');
console.log(person.name);
```
上面示例中,只有`loadPerson()`返回结果为真(即操作成功),`assert()`才不会报错。
## 参考链接
- [Const Assertions in Literal Expressions in TypeScript](https://mariusschulz.com/blog/const-assertions-in-literal-expressions-in-typescript), Marius Schulz
- [Assertion Functions in TypeScript](https://mariusschulz.com/blog/assertion-functions-in-typescript), Marius Schulz
- [Assertion functions in TypeScript](https://blog.logrocket.com/assertion-functions-typescript/), Matteo Di Pirro

335
docs/basic.md Normal file
View File

@@ -0,0 +1,335 @@
# TypeScript 基本用法
本章介绍 TypeScript 的一些最基本的语法和用法。
## 类型声明
TypeScript 代码最明显的特征,就是为 JavaScript 变量加上了类型声明。
```typescript
let foo:string;
```
上面示例中,变量`foo`的后面使用冒号,声明了它的类型为`string`
类型声明的写法,一律为在标识符后面添加“冒号 + 类型”。函数参数和返回值,也是这样来声明类型。
```typescript
function toString(num:number):string {
return String(num);
}
```
上面示例中,函数`toString()`的参数`num`的类型是`number`。参数列表的圆括号后面,声明了返回值的类型是`string`。更详细的介绍,参见《函数》一章。
注意变量的值应该与声明的类型一致如果不一致TypeScript 就会报错。
```typescript
// 报错
let foo:string = 123;
```
上面示例中,变量`foo`的类型是字符串,但是赋值为数值`123`TypeScript 就报错了。
另外TypeScript 规定,变量只有赋值后才能使用,否则就会报错。
```typescript
let x:number;
console.log(x) // 报错
```
上面示例中,变量`x`没有赋值就被读取,导致报错。而 JavaScript 允许这种行为,不会报错,没有赋值的变量会返回`undefined`
## 类型推断
类型声明并不是必需的如果没有TypeScript 会自己推断类型。
```typescript
let foo = 123;
```
上面示例中,变量`foo`并没有类型声明TypeScript 就会推断它的类型。由于它被赋值为一个数值,因此 TypeScript 推断它的类型为`number`
后面,如果变量`foo`更改为其他类型的值跟推断的类型不一致TypeScript 就会报错。
```typescript
let foo = 123;
foo = 'hello'; // 报错
```
上面示例中,变量`foo`的类型推断为`number`后面赋值为字符串TypeScript 就报错了。
TypeScript 也可以推断函数的返回值。
```typescript
function toString(num:number) {
return String(num);
}
```
上面示例中,函数`toString()`没有声明返回值的类型,但是 TypeScript 推断返回的是字符串。正是因为 TypeScript 的类型推断,所以函数返回值的类型通常是省略不写的。
从这里可以看到TypeScript 的设计思想是,类型声明是可选的,你可以加,也可以不加。即使不加类型声明,依然是有效的 TypeScript 代码,只是这时不能保证 TypeScript 会正确推断出类型。由于这个原因。所有 JavaScript 代码都是合法的 TypeScript 代码。
这样设计还有一个好处,将以前的 JavaScript 项目改为 TypeScript 项目时,你可以逐步地为老代码添加类型,即使有些代码没有添加,也不会无法运行。
## TypeScript 的编译
JavaScript 的运行环境(浏览器和 Node.js不认识 TypeScript 代码。所以TypeScript 项目要想运行,必须先转为 JavaScript 代码这个代码转换的过程就叫做“编译”compile
TypeScript 官方没有做运行环境,只提供编译器。编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。
因此TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript运行时就不再检查类型了。
## 值与类型
学习 TypeScript 需要分清楚“值”value和“类型”type
“类型”是针对“值”的,可以视为是后者的一个元属性。每一个值在 TypeScript 里面都是有类型的。比如,`3`是一个值,它的类型是`number`
TypeScript 代码只涉及类型,不涉及值。所有跟“值”相关的处理,都由 JavaScript 完成。
这一点务必牢记。TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”,另一种是上层的“类型代码”。前者使用 JavaScript 语法,后者使用 TypeScript 的类型语法。
它们是可以分离的TypeScript 的编译过程,实际上就是把“类型代码”全部拿掉,只保留“值代码”。
编写 TypeScript 项目时,不要混淆哪些是值代码,哪些是类型代码。
## TypeScript Playground
最简单的 TypeScript 使用方法,就是使用官网的在线编译页面,叫做 [TypeScript Playground](http://www.typescriptlang.org/play/)。
只要打开这个网页,把 TypeScript 代码贴进文本框,它就会在当前页面自动编译出 JavaScript 代码,还可以在浏览器执行编译产物。如果编译报错,它也会给出详细的报错信息。
这个页面还具有支持完整的 IDE 支持,可以自动语法提示。此外,它支持把代码片段和编译器设置保存成 URL分享给他人。
本书的示例都建议放到这个页面,进行查看和编译。
## tsc 编译器
TypeScript 官方提供的编译器叫做 tsc可以将 TypeScript 脚本编译成 JavaScript 脚本。本机想要编译 TypeScript 代码,必须安装 tsc。
根据约定TypeScript 脚本文件使用`.ts`后缀名JavaScript 脚本文件使用`.js`后缀名。tsc 的作用就是把`.ts`脚本转变成`.js`脚本。
### 安装
tsc 是一个 npm 模块,使用下面的命令安装(必须先安装 npm
```bash
$ npm install -g typescript
```
上面命令是全局安装 tsc也可以在项目中将 tsc 安装为一个依赖模块。
安装完成后,检查一下是否安装成功。
```bash
# 或者 tsc --version
$ tsc -v
Version 5.1.6
```
上面命令中,`-v``--version`参数可以输出当前安装的 tsc 版本。
### 帮助信息
`-h``--help`参数输出帮助信息。
```bash
$ tsc -h
```
默认情况下,“--help”参数仅显示基本的可用选项。我们可以使用“--all”参数查看完整的帮助信息。
```bash
$ tsc --all
```
### 编译脚本
安装 tsc 之后,就可以编译 TypeScript 脚本了。
`tsc`命令后面,加上 TypeScript 脚本文件,就可以将其编译成 JavaScript 脚本。
```bash
$ tsc app.ts
```
上面命令会在当前目录下,生成一个`app.js`脚本文件,这个脚本就完全是编译后生成的 JavaScript 代码。
`tsc`命令也可以一次编译多个 TypeScript 脚本。
```bash
$ tsc file1.ts file2.ts file3.ts
```
上面命令会在当前目录生成三个 JavaScript 脚本文件`file1.js``file2.js``file3.js`
tsc 有很多参数,可以调整编译行为。
**1--outFile**
如果想将多个 TypeScript 脚本编译成一个 JavaScript 文件,使用`--outFile`参数。
```bash
$ tsc file1.ts file2.ts --outFile app.js
```
上面命令将`file1.ts``file2.ts`两个脚本编译成一个 JavaScript 文件`app.ts`
**2--outDir**
编译结果默认都保存在当前目录,`--outDir`参数可以指定保存到其他目录。
```bash
$ tsc app.ts --outDir dist
```
上面命令会在`dist`子目录下生成`app.js`
注意,`--outDir``--outFile`不能同时使用。
**3--target**
为了保证编译结果能在各种 JavaScript 引擎运行tsc 默认会将 TypeScript 代码编译成很低版本的 JavaScript即3.0版本(以`es3`表示)。这通常不是我们想要的结果。
这时可以使用`--target`参数,指定编译后的 JavaScript 版本。建议使用`es2015`,或者更新版本。
```bash
$ tsc --target es2015 app.ts
```
### 编译错误的处理
编译过程中,如果没有报错,`tsc`命令不会有任何显示。所以,如果你没有看到任何提示,就表示编译成功了。
如果编译报错,`tsc`命令就会显示报错信息,但是这种情况下,依然会编译生成 JavaScript 脚本。
举例来说,下面是一个错误的 TypeScript 脚本`app.ts`
```typescript
// app.ts
let foo:number = 123;
foo = 'abc'; // 报错
```
上面示例中,变量`foo`是数值类型,赋值为字符串,`tsc`命令编译这个脚本就会报错。
```bash
$ tsc app.ts
app.ts:2:1 - error TS2322: Type 'string' is not assignable to type 'number'.
2 foo = 'abc';
~~~
Found 1 error in app.ts:2
```
上面示例中,`tsc`命令输出报错信息,表示变量`foo`被错误地赋值为字符串。
这种情况下,编译产物`app.js`还是会照样生成,下面就是编译后的结果。
```javascript
// app.js
var foo = 123;
foo = 'abc';
```
可以看到尽管有错tsc 依然原样将 TypeScript 编译成 JavaScript 脚本。
这是因为 TypeScript 团队认为,编译器的作用只是给出编译错误,至于怎么处理这些错误,那就是开发者自己的判断了。开发者更了解自己的代码,所以不管怎样,编译产物都会生成,让开发者决定下一步怎么处理。
如果希望一旦报错就停止编译,不生成编译产物,可以使用`--noEmitOnError`参数。
```bash
$ tsc --noEmitOnError app.ts
```
上面命令在报错后,就不会生成`app.js`
tsc 还有一个`--noEmit`参数,只检查类型是否正确,不生成 JavaScript 文件。
```bash
$ tsc --noEmit app.ts
```
上面命令只检查是否有编译错误,不会生成`app.js`
tsc 命令的更多参数详见《tsc 编译器》一章。
### tsconfig.json
TypeScript 允许将`tsc`的编译参数,写在配置文件`tsconfig.json`。只要当前目录有这个文件,`tsc`就会自动读取,所以运行时可以不写参数。
```bash
$ tsc file1.js file2.js --outFile dist/app.js
```
上面这个命令写成`tsconfig.json`,就是下面这样。
```json
{
"files": ["file1.ts", "file2.ts"],
"compilerOptions": {
"outFile": "dist/app.js"
}
}
```
有了这个配置文件,编译时直接调用`tsc`命令就可以了。
```bash
$ tsc
```
`tsconfig.json`的详细介绍参见《tsconfig.json 配置文件》一章。
## ts-node 模块
[ts-node](https://github.com/TypeStrong/ts-node) 是一个非官方的 npm 模块,可以直接运行 TypeScript 代码。
使用时,可以先全局安装它。
```bash
$ npm install -g ts-node
```
安装后,就可以直接运行 TypeScript 脚本。
```bash
$ ts-node script.ts
```
上面命令运行了 TypeScript 脚本`script.ts`,给出运行结果。
如果不安装 ts-node也可以通过 npx 调用它来运行 TypeScript 脚本。
```bash
$ npx ts-node script.ts
```
上面命令中,`npx`会在线调用 ts-node从而在不安装的情况下运行`script.ts`
如果执行 ts-node 命令不带有任何参数,它会提供一个 TypeScript 的命令行 REPL 运行环境,你可以在这个环境中输入 TypeScript 代码,逐行执行。
```bash
$ ts-node
>
```
上面示例中,单独运行`ts-node`命令,会给出一个大于号,这就是 TypeScript 的 REPL 运行环境,可以逐行输入代码运行。
```bash
$ ts-node
> const twice = (x:string) => x + x;
> twice('abc')
'abcabc'
>
```
上面示例中,在 TypeScript 命令行 REPL 环境中,先输入一个函数`twice`,然后调用该函数,就会得到结果。
要退出这个 REPL 环境,可以按下 Ctrl + d或者输入`.exit`
如果只是想简单运行 TypeScript 代码看看结果ts-node 不失为一个简单的方法。

1555
docs/class.md Normal file

File diff suppressed because it is too large Load Diff

212
docs/comment.md Normal file
View File

@@ -0,0 +1,212 @@
# TypeScript 的注释指令
TypeScript 接受一些注释指令。
所谓“注释指令”,指的是采用 JS 双斜杠注释的形式,向编译器发出的命令。
## `// @ts-nocheck`
`// @ts-nocheck`告诉编译器不对当前脚本进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。
```javascript
// @ts-nocheck
const element = document.getElementById(123);
```
上面示例中,`document.getElementById(123)`存在类型错误,但是编译器不对该脚本进行类型检查,所以不会报错。
## `// @ts-check`
如果一个 JavaScript 脚本顶部添加了`// @ts-check`,那么编译器将对该脚本进行类型检查,不论是否启用了`checkJs`编译选项。
```javascript
// @ts-check
let isChecked = true;
console.log(isChceked); // 报错
```
上面示例是一个 JavaScript 脚本,`// @ts-check`告诉 TypeScript 编译器对其进行类型检查,所以最后一行会报错。
## `// @ts-ignore`
`// @ts-ignore``// @ts-expect-error`,告诉编译器不对下一行代码进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。
```typescript
let x:number;
x = 0;
// @ts-expect-error
x = false; // 不报错
```
上面示例中,最后一行是类型错误,变量`x`的类型是`number`,不能等于布尔值。但是因为前面加上了`// @ts-expect-error`,编译器会跳过这一行的类型检查,所以不会报错。
## JSDoc
TypeScript 直接处理 JS 文件时,如果无法推断出类型,会使用 JS 脚本里面的 JSDoc 注释。
使用 JSDoc 时,有两个基本要求。
1JSDoc 注释必须以`/**`开始,其中星号(`*`)的数量必须为两个。若使用其他形式的多行注释,则 JSDoc 会忽略该条注释。
2JSDoc 注释必须与它描述的代码处于相邻的位置,并且注释在上,代码在下。
下面是 JSDoc 的一个简单例子。
```javascript
/**
* @param {string} somebody
*/
function sayHello(somebody) {
console.log('Hello ' + somebody);
}
```
上面示例中,注释里面的`@param`是一个 JSDoc 命令,表示下面的函数`sayHello()`的参数`somebody`类型为`string`
TypeScript 编译器支持的大部分的 JSDoc 命令,下面介绍其中的一些。
### @typedef
`@typedef`命令创建自定义类型,等同于 TypeScript 里面的类型别名。
```javascript
/**
* @typedef {(number | string)} NumberLike
*/
```
上面示例中,定义了一个名为`NumberLike`的新类型,它是由`number``string`构成的联合类型,等同于 TypeScript 的如下语句。
```typescript
type NumberLike = string | number;
```
### @type
`@type`命令定义变量的类型。
```javascript
/**
* @type {string}
*/
let a;
```
上面示例中,`@type`定义了变量`a`的类型为`string`
`@type`命令中可以使用由`@typedef`命令创建的类型。
```javascript
/**
* @typedef {(number | string)} NumberLike
*/
/**
* @type {NumberLike}
*/
let a = 0;
```
`@type`命令中允许使用 TypeScript 类型及其语法。
```javascript
/**@type {true | false} */
let a;
/** @type {number[]} */
let b;
/** @type {Array<number>} */
let c;
/** @type {{ readonly x: number, y?: string }} */
let d;
/** @type {(s: string, b: boolean) => number} */
let e;
```
### @param
`@param`命令用于定义函数参数的类型。
```javascript
/**
* @param {string} x
*/
function foo(x) {}
```
如果是可选参数,需要将参数名放在方括号`[]`里面。
```javascript
/**
* @param {string} [x]
*/
function foo(x) {}
```
方括号里面,还可以指定参数默认值。
```javascript
/**
* @param {string} [x="bar"]
*/
function foo(x) {}
```
上面示例中,参数`x`的默认值是字符串`bar`
### @return@returns
`@return``@returns`命令的作用相同,指定函数返回值的类型。
```javascript
/**
* @return {boolean}
*/
function foo() {
return true;
}
/**
* @returns {number}
*/
function bar() {
return 0;
}
```
### @extends 和类型修饰符
`@extends`命令用于定义继承的基类。
/**
* @extends {Base}
*/
class Derived extends Base {
}
```
`@public`、`@protected`、`@private`分别指定类的公开成员、保护成员和私有成员。
`@readonly`指定只读成员。
```javascript
class Base {
/**
* @public
* @readonly
*/
x = 0;
/**
* @protected
*/
y = 0;
}
```

148
docs/conditional-types.md Normal file
View File

@@ -0,0 +1,148 @@
# 条件类型
```typescript
T extends U ? X : Y
```
上面定义中T、U、X、Y 代表任意类型。`T extends U`表示类型的测试条件,如果满足此条件,返回类型`X`,否则返回类型`Y`
下面是一个例子。
```typescript
type NonNullable<T> = T extends null | undefined ? never : T;
```
上面式子定义了一个范型`NonNullable`,用来检测某个类型是否非空。
```typescript
type EmailAddress = string | string[] | null | undefined;
// 等同于 type NonNullableEmailAddress = string | string[];
type NonNullableEmailAddress = NonNullable<EmailAddress>;
```
TypeScript 提供了一些预定义的条件类型,下面逐一介绍。
## `NonNullable<T>`
`NonNullable<T>`从类型`T`里面过滤掉`null``undefined`
```typescript
type NonNullable<T> = T extends null | undefined ? never : T;
```
```typescript
type A = NonNullable<boolean>; // boolean
type B = NonNullable<number | null>; // number
type C = NonNullable<string | undefined>; // string
type D = NonNullable<null | undefined>; // never
```
## `Extract<T, U>`
```typescript
type Extract<T, U> = T extends U ? T : never;
```
`Extract<T, U>`类型表达式相当于提取功能,只要`T`符合`U`就返回`T`,否则就过滤掉。
```typescript
type A = Extract<string | string[], any[]>; // string[]
type B = Extract<(() => void) | null, Function>; // () => void
type C = Extract<200 | 400, 200 | 201>; // 200
type D = Extract<number, boolean>; // never
```
## `Exclude<T, U>`
`Exclude<T, U>`相当于排除功能,只要`T`符合`U`就过滤掉,否则返回`T`
```typescript
type Exclude<T, U> = T extends U ? never : T;
```
```typescript
type A = Exclude<string | string[], any[]>; // string
type B = Exclude<(() => void) | null, Function>; // null
type C = Exclude<200 | 400, 200 | 201>; // 400
type D = Exclude<number, boolean>; // number
```
## `ReturnType<T>`
`ReturnType<T>`提取函数的返回类型。
```typescript
type ReturnType<T extends (...args: any[]) => any> = T extends (
...args: any[]
) => infer R
? R
: any;
```
```typescript
type A = ReturnType<() => string>; // string
type B = ReturnType<() => () => any[]>; // () => any[]
type C = ReturnType<typeof Math.random>; // number
type D = ReturnType<typeof Array.isArray>; // boolean
```
## `Parameters<T>`
`Parameters<T>`提供函数`T`的所有参数类型,它的返回值是一个`tuple`类型,或者`never`(如果 T 不是函数)。
```typescript
type Parameters<T extends (...args: any[]) => any> = T extends (
...args: infer P
) => any
? P
: never;
```
```typescript
type A = Parameters<() => void>; // []
type B = Parameters<typeof Array.isArray>; // [any]
type C = Parameters<typeof parseInt>; // [string, (number | undefined)?]
type D = Parameters<typeof Math.max>; // number[]
```
`Array.isArray()`只有一个参数,所以返回的类型是`[any]`,而不是`any[]``Math.max()`的参数是任意多个数值,而不是一个数值数组,所以返回的类型是`number[]`,而不是`[number[]]`
## `ConstructorParameters<T>`
`ConstructorParameters<T>`提取一个构造函数的所有参数类型。它的返回值是一个 tuple 类型,成员是所有参数的类型,如果 T 不是函数,则返回 never。
```typescript
type ConstructorParameters<
T extends new (...args: any[]) => any
> = T extends new (...args: infer P) => any ? P : never;
```
```typescript
type A = ConstructorParameters<ErrorConstructor>;
// [(string | undefined)?]
type B = ConstructorParameters<FunctionConstructor>;
// string[]
type C = ConstructorParameters<RegExpConstructor>;
// [string, (string | undefined)?]
```
## `InstanceType<T>`
`InstanceType<T>`提取构造函数的返回值的类型,等同于构造函数的`ReturnType<T>`
```typescript
type InstanceType<T extends new (...args: any[]) => any> = T extends new (
...args: any[]
) => infer R
? R
: any;
```
```typescript
type A = InstanceType<ErrorConstructor>; // Error
type B = InstanceType<FunctionConstructor>; // Function
type C = InstanceType<RegExpConstructor>; // RegExp
```

609
docs/d.ts.md Normal file
View File

@@ -0,0 +1,609 @@
# d.ts 类型声明文件
## 简介
模块需要提供一个类型声明文件declaration file让模块使用者了解它的接口类型。
类型声明文件就是接口的类型描述,写在一个单独的文件里面。它里面只有类型代码,没有具体的代码实现。
它的文件名一般为`[模块名].d.ts`的形式,其中的`d`表示 declaration声明
举例来说,有一个模块的代码如下。
```typescript
const maxInterval = 12;
function getArrayLength(arr) {
return arr.length;
}
module.exports = {
getArrayLength,
maxInterval,
};
```
它的类型声明文件可以写成下面这样。
```typescript
export function getArrayLength(arr: any[]): number;
export const maxInterval: 12;
```
如果输出的是一个值,那么类型声明文件需要使用`export default``export=`
```typescript
// 模块输出
module.exports = 3.142;
// 类型输出文件
// 写法一
declare const pi: number;
export default pi;
// 写法二
declare const pi: number;
export= pi;
```
下面是一个简单例子,有一个类型声明文件`types.d.ts`
```typescript
// types.d.ts
export interface Character {
catchphrase?: string;
name: string;
}
```
然后,就可以在 TypeScript 脚本里面导入该文件声明的类型。
```typescript
// index.ts
import { Character } from "./types.d.ts";
export const character:Character = {
catchphrase: "Yee-haw!",
name: "Sandy Cheeks",
};
```
定义了类型声明文件以后,可以将其包括在项目的 tsconfig.json 文件里面方便打包和其他脚本加载。比如moment 模块的类型声明文件是`moment.d.ts`,将其加入 tsconfig.json。
```typescript
{
"compilerOptions": {},
"files": [
"src/index.ts",
"typings/moment.d.ts"
]
}
```
有些模块是 CommonJS 格式,采用`module.exports`输出接口。它的类型描述文件可以写成下面的形式。
```typescript
declare module 'moment' {
function moment(): any;
export = moment;
}
```
上面示例中,模块`moment`是 CommonJS 格式,它的内部有一个函数`moment()`,而`export =`表示`module.exports`输出的就是这个函数。
类型声明文件主要有以下三种来源。
- TypeScript 语言内置的类型声明文件。
- 第三方类型声明文件,需要自己安装。
- 自己编写的类型声明文件。
### 内置声明文件
安装 TypeScript 语言时,会同时安装一些内置声明文件,主要是 JavaScript 语言接口和运行环境 API 的类型声明。
这些内置声明文件位于 TypeScript 语言安装目录的`lib`文件夹内,数量大概有几十个,下面是其中一些主要文件。
- lib.d.ts
- lib.dom.d.ts
- lib.es2015.d.ts
- lib.es2016.d.ts
- lib.es2017.d.ts
- lib.es2018.d.ts
- lib.es2019.d.ts
- lib.es2020.d.ts
- lib.es5.d.ts
- lib.es6.d.ts
这些内置声明文件的文件名统一为“lib.[description].d.ts”的形式其中`description`部分描述了文件内容。比如,`lib.dom.d.ts`这个文件就描述了 DOM 结构的类型。
TypeScript 编译器会自动加载这些内置声明文件,所以不需要特别的配置。
### 第三方声明文件
如果项目中使用了外部的某个第三方代码库,那么就需要这个库的类型声明文件。
这时又分成三种情况。
1这个库自带了类型声明文件。
2这个库没有自带但是可以找到社区制作的类型声明文件。
3找不到类型声明文件需要自己写。
一般来说,如果这个库的源码包含了`[vendor].d.ts`文件,那么就自带了类型声明文件。其中的`vendor`表示这个库的名字,比如`moment`这个库就自带`moment.d.ts`
### DefinitelyTyped 社区
第三方库如果没有提供类型声明文件社区往往会提供。TypeScript 社区主要使用 [DefinitelyTyped 仓库](https://github.com/DefinitelyTyped/DefinitelyTyped),各种类型声明文件都会提交到那里,已经包含了几千个第三方库。
TypeScript 官网有 DefinitelyTyped 的搜索入口,需要第三方声明文件的时候,就可以去 [www.typescriptlang.org/dt/search](https://www.typescriptlang.org/dt/search) 搜搜看。
这些声明文件都会发布到 npm 的`@types`名称空间之下。比如jQuery 的类型声明文件就放在`@types/jquery`这个库,安装这个库就可以了。
```bash
$ npm install @types/jquery --save-dev
```
执行上面的命令,`@types/jquery`这个库就安装到项目的`node_modules/@types/jquery`目录,里面的`index.d.ts`文件就是 jQuery 的类型声明文件。
然后,在`tsconfig.json`文件里面加上类型声明文件的位置。
```javascript
{
"compilerOptions": {
"types" : ["jquery"]
}
}
```
上面设置中,`types`属性是一个数组,成员是所要加载的类型声明文件,要加载几个文件,就有几个成员,每个成员在子目录`node_modules/@types`下面都有一个自己的目录。
这样的话,你的项目加载 jQuery 时,编译器就会正确加载它的类型声明文件。
## declare 关键字
类型声明文件只包含类型描述,不包含具体实现,所以非常适合使用 declare 语句来描述类型。
declare 字的具体用法详见《declare 关键字》一章,这里讲解如何在类型声明文件里面使用它。
类型声明文件里面,变量的类型描述必须使用`declare`命令,否则会报错。
```typescript
declare let foo:string;
```
interface 类型有没有`declare`都可以。
```typescript
interface Foo {} // 正确
declare interface Foo {} // 正确
```
类型声明文件里面,可以使用`export`命令。
```typescript
export interface Data {
version: string;
}
```
下面是 moment 模块的类型描述文件`moment.d.ts`的例子。
```typescript
declare module 'moment' {
export interface Moment {
format(format:string):string;
add(
amount:number,
unit:'days' | 'months' | 'years'
):Moment;
subtract(
amount:number,
unit:'days' | 'months' | 'years'
): Moment;
}
function moment(
input?:string | Date
):Moment;
export default moment;
}
```
下面是 D3 库的`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;
```
## 模块发布
类型声明文件写好后,如果要在 npm 上面发布,可以在 package.json 文件添加一个`types`字段,指明类型声明文件的位置。
```typescript
{
"name": "awesome",
"author": "Vandelay Industries",
"version": "1.0.0",
"main": "./lib/main.js",
"types": "./lib/main.d.ts"
}
```
上面示例中,`types`字段给出了类型声明文件的位置。
注意,`types`字段也可以写成`typings`
另外,如果类型声明文件为`index.d.ts`,且在项目的根目录(与`index.js`在一起),那么不需要注明`types`字段。
有时,类型声明文件会单独发布成一个 npm 模块,这时用户就必须同时加载该模块。
```typescript
{
"name": "browserify-typescript-extension",
"author": "Vandelay Industries",
"version": "1.0.0",
"main": "./lib/main.js",
"types": "./lib/main.d.ts",
"dependencies": {
"browserify": "latest",
"@types/browserify": "latest",
"typescript": "next"
}
}
```
上面示例是一个模块的 package.json 文件,该文件需要 browserify 模块。由于后者的类型声明文件放在另一个模块`@types/browserify`,所以还必需加载那个模块。
## 三斜杠命令
其他脚本可以使用三斜杠命令,加载类型声明文件。
三斜杠命令(`///`)是一个编译器命令,用来指定编译器行为。它只能用在文件的头部,如果用在其他地方,会被当作普通的注释。
若一个文件中使用了三斜线命令,那么在三斜线命令之前只允许使用单行注释、多行注释和其他三斜线命令,否则三斜杠命令会被当作普通的注释。
三斜杠命令主要包含三个参数,代表三种不同的命令。
- path
- types
- lib
下面依次进行讲解。
### `/// <reference path="" />`
`/// <reference path="" />`是最常见的三斜杠命令,告诉编译器在编译时需要包括的文件,常用来声明当前脚本依赖的类型文件。
```typescript
/// <reference path="lib.ts" />
let count = add(1, 2);
```
上面示例中,编译当前脚本时,还会同时编译`lib.ts`。编译产物会有两个 JS 文件,一个当前脚本,另一个就是`lib.js`
编译器会在预处理阶段,找出所有三斜杠引用的文件,将其添加到编译列表中,然后一起编译。
`path`参数指定了所引入文件的路径。如果该路径是一个相对路径,则基于当前脚本的路径进行计算。
使用该命令时,有以下两个注意事项。
- `path`参数必须指向一个存在的文件,若文件不存在会报错。
- `path`参数不允许指向当前文件。
默认情况下,每个三斜杠命令引入的脚本,都会编译成单独的 JS 文件。如果希望编译后只产出一个合并文件,可以使用编译参数`outFile`。但是,`outFile`编译参数不支持合并 CommonJS 模块和 ES 模块,只有当编译参数`module`的值设为 None、System 或 AMD 时,才能编译成一个文件。
如果打开了编译参数`noResolve`,则忽略三斜杠指令。将其当作一般的注释,原样保留在编译产物中。
### `/// <reference types="" />`
types 参数用来告诉编译器当前脚本依赖某个 DefinitelyTyped 类型库,通常安装在`node_modules/@types`目录。
types 参数的值是类型库的名称,也就是安装到`node_modules/@types`目录中的子目录的名字。
```typescript
/// <reference types="node" />
```
上面示例中,这个三斜杠命令表示编译时添加 Node.js 的类型库,实际添加的脚本是`node_modules`目录里面的`@types/node/index.d.ts`
可以看到,这个命令的作用类似于`import`命令。
注意,这个命令只在你自己手写`.d.ts`文件时,才有必要用到,也就是说,只应该用在`.d.ts`文件中,普通的`.ts`脚本文件不需要写这个命令。
我们应该只在类型声明文件(.d.ts中使用“/// <reference types="" />”三斜线命令,而不应该在普通的`.ts`脚本文件中使用该命令。如果是普通的`.ts`脚本,可以使用`tsconfig.json`文件的`types`属性指定依赖的类型库。
### `/// <reference lib="" />`
`/// <reference lib="..." />`命令允许脚本文件显式包含内置 lib 库,等同于在`tsconfig.json`文件里面使用`lib`属性指定 lib 库。
前文说过,安装 TypeScript 软件包时,会同时安装一些内置的类型声明文件,即内置的 lib 库。这些库文件位于 TypeScript 安装目录的`lib`文件夹中,它们描述了 JavaScript 语言的标准 API。
库文件并不是固定的,会随着 TypeScript 版本的升级而更新。库文件统一使用“lib.[description].d.ts”的命名方式`/// <reference lib="" />`里面的`lib`属性的值就是库文件名的`description`部分,比如`lib="es2015"`就表示加载库文件`lib.es2015.d.ts`
```typescript
/// <reference lib="es2017.string" />
```
上面示例中,`es2017.string`对应的库文件就是`lib.es2017.string.d.ts`
## 自定义类型声明文件
有时实在没有第三方库的类型声明文件,你可以告诉 TypeScript 相关对象的类型是`any`。比如,使用 jQuery 的脚本可以写成下面这样。
```typescript
declare var $: any
```
上面代码表示jQuery 的`$`对象是外部引入的,类型是`any`,也就是 TypeScript 不用对它进行类型检查。
为了描述不是用 TypeScript 编写的库的形状,我们需要声明库公开的 API。通常这些是在.d.ts文件中定义的。如果您熟悉 C/C++,您可以将这些视为.h文件。
在 Node.js 中,大多数任务都是通过加载一个或多个模块来完成的。我们可以为每个模块,定义一个自己的 .d.ts 文件,但是把所有模块的类型定义放在一个大的 .d.ts 文件更方便。
开源库 [DefinitelyTyped](https://github.com/DefinitelyTyped/DefinitelyTyped) 提供大部分常用第三方库的类型,在写自己的`[vendor.d.ts]`之前,可以先到这个库查看,它有没有提供。下面是 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 normalize(p: string): string;
export function join(...paths: any[]): string;
export var sep: string;
}
```
后面就可以在脚本里面,使用`/// <reference> node.d.ts`给出类型定义。
```typescript
/// <reference path="lib\jquery-1.8.d.ts" />
```
```typescript
/// <reference path="node.d.ts"/>
import * as URL from "url";
let myUrl = URL.parse("https://www.typescriptlang.org");
```
有时候,很难为别人的模块写出完整的 .d.ts 类型定义文件。TypeScript 这时允许在 .d.ts 里面只写模块名,不写具体的类型定义。
```typescript
declare module "hot-new-module";
```
脚本加载这个模块以后,所有引入的接口都是 any 类型。
```typescript
import x, { y } from "hot-new-module";
x(y);
```
d.ts 文件里面需要声明引入变量的类型。比如jQuery 可以这样声明。
```typescript
declare var $: any;
```
上面示例表示变量`$`可以是任意类型。
也可以像下面这样,自定义一个 JQuery 类型。
```typescript
declare type JQuery = any;
declare var $: JQuery;
```
另一个方法是声明一个模块 jquery。
```typescript
declare module "jquery";
```
然后在脚本里面加载这个模块。
```typescript
import * as $ from "jquery";
```
下面是 Node.js 的 Process 模块的例子。
```typescript
interface Process {
exit(code?: number): void;
}
declare var process: Process;
```
如果你要自己为 Process 对象添加一个方法`exitWithLogging()`,就需要自己补上该方法的类型注释。
```typescript
interface Process {
exitWithLogging(code?: number): void;
}
process.exitWithLogging = function() {
console.log("exiting");
process.exit.apply(process, arguments);
};
```
## package.json
TypeScript扩展了“package.json”文件增加了typings属性和types属性。虽然两者的名字不同但是作用相同它们都用于指定当前npm包提供的声明文件。
```typescript
{
"name": "my-package",
"version": "1.0.0",
"main": "index.js",
"typings": "index.d.ts"
}
```
此例中使用typings属性定义了“my-package”包的声明文件为“index.d.ts”文件。当TypeScript编译器进行模块解析时将会读取该属性的值并使用指定的“index.d.ts”文件作为声明文件。这里我们也可以将typings属性替换为types属性两者是等效的。
如果一个npm包的声明文件为“index.d.ts”且位于npm包的根目录下那么在“package.json”文件中也可以省略typings属性和types属性因为编译器在进行模块解析时若在“package.json”文件中没有找到typings属性和types属性则将默认使用名为“index.d.ts”的文件作为声明文件。
在TypeScript 3.1版本中编译器能够根据当前安装的TypeScript版本来决定使用的声明文件该功能是通过“package.json”文件中的typesVersions属性来实现的。
```javascript
{
"name": "my-package",
"version": "1.0.0",
"main": "index.js",
"typings": "index.d.ts",
"typesVersions": {
">=3.7": {
"*": ["ts3.7/*"]
},
">=3.1": {
"*": ["ts3.1/*"]
}
}
}
```
此例中,我们定义了两个声明文件匹配规则:
▪第7行当安装了TypeScript 3.7及以上版本时将使用“ts3.7”目录下的声明文件。
▪第10行当安装了TypeScript 3.1及以上版本时将使用“ts3.1”目录下的声明文件。
需要注意的是typesVersions中的声明顺序很关键编译器将从第一个声明此例中为">=3.7")开始尝试匹配,若匹配成功,则应用匹配到的值并退出。因此,若将此例中的两个声明调换位置,则会产生不同的结果。
此外如果typesVersions中不存在匹配的版本如当前安装的是TypeScript 2.0版本那么编译器将使用typings属性和types属性中定义的声明文件。
## @types
TypeScript 官方提供了加载许多常用模块的类型注释,都放在 NPM 仓库的 @types 名称空间下面。
你可以像安装 npm 模块一样,安装外部库的类型注释。
```bash
$ npm install @types/jquery --save-dev
```
`@types/jquery`里面包括了模块类型和全局变量的类型。
你可以直接使用下面的语句。
```typescript
import * as $ from "jquery";
```
## 自定义声明文件
如果使用的第三方代码库没有提供内置的声明文件而且在DefinitelyTyped仓库中也没有对应的声明文件那么就需要开发者自己编写一个声明文件。
如果我们不想编写一个详尽的声明文件,而只是想要跳过对某个第三方代码库的类型检查,则可以使用下面介绍的方法。
如果为 jQuery 创建一个“.d.ts”声明文件例如“jquery.d.ts”。“jquery.d.ts”声明文件的内容如下
```typescript
declare module 'jquery';
```
此例中的代码是外部模块声明该声明会将jquery模块的类型设置为any类型。jquery模块中所有成员的类型都成了any类型这等同于不对jQuery进行类型检查。
“typings.d.ts”文件的内容如下
```typescript
declare module 'mod' {
export function add(x: number, y: number): number;
}
```
在“a.ts”文件中可以使用非相对模块导入语句来导入外部模块“mod”。示例如下
```typescript
import * as Mod from 'mod';
Mod.add(1, 2);
```
## es6-shim.d.ts
如果代码编译成 ES5`tsconfig.json`设成`"target": ES5`),但是代码会用到 ES6 的 API并且希望 IDE 能够正确识别,可以引入`es6-shim.d.ts`。
```bash
$ npm install @types/es6-shim -D
```
`tsconfig.json`加入下面的设置。
```javascript
"types" : ["jquery", "es6-shim"]
```
另外一个新的垫片库是`core-js`。
## reference 命令
自己以前的项目可以自定义一个类型声明文件,比如`typings.d.ts`。
比如,你以前写过一个函数。
```typescript
function greeting(name) {
console.log("hello " + name);
}
```
新项目要用到这个函数,你可以为这个函数单独写一个类型文件`src/typings.d.ts`。
```typescript
export function greeting(name: string): void;
```
然后,需要在用到这个库的脚本头部加上一行,用三斜杠语法告诉 TypeScript 类型声明文件的位置。
```typescript
/// <reference path="src/typings.d.ts" />
```
如果类型声明文件是随 NPM 安装的,那么`reference`语句的属性需要从`path`改成`type`。
```typescript
/// <reference types="some-library" />
```
## JavaScript 项目加入 TypeScript
如果现有的 JavaScript 项目需要加入 TypeScript可以在`tsconfig.json`文件加入`"allowJs": true`设置,表示将 JS 文件一起复制到编译产物目录。
这时TypeScript 不会对 JavaScript 脚本进行类型检查。如果你希望也进行类型检查,可以设置`"checkJs": true`。
另一种方法是在 JavaScript 脚本的第一行,加上注释`//@ts-check`,这时 TypeScript 也会对这个脚本进行检查。
打开`"checkJs": true`以后,如果不希望对有的 JavaScript 脚本进行类型检查,可以在该脚本头部加上`//@ts-ignore`。
You can also help tsc with type inference by adding the JSDoc annotations (such as
@param and @return) to your JavaScript code.

382
docs/declare.md Normal file
View File

@@ -0,0 +1,382 @@
# declare 关键字
## 简介
declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。
它的直接作用,就是让当前文件可以使用其他文件声明的类型。因此,编译单个脚本时,不会因为使用了外部类型而报错。
declare 关键字可以描述以下类型。
- 变量const、let、var 命令声明)
- type 或者 interface 命令声明的类型
- class
- enum
- 函数function
- 模块module
- 命名空间namespace
declare 关键字的重要特点是,它只是通知编译器某个类型是存在的,不用给出具体实现。
declare 只能用来描述已经存在的变量和数据结构,不能用来声明新的变量和数据结构。另外,所有 declare 语句都不会出现在编译后的文件里面。
## declare variable
declare 关键字可以给出外部变量的类型描述。
举例来说,当前脚本使用了其他脚本定义的全局变量`x`
```typescript
x = 123; // 报错
```
上面示例中,变量`x`是其他脚本定义的,当前脚本不知道它的类型,编译器就会报错。
这时使用 declare 命令给出它的类型,就不会报错了。
```typescript
declare let x:number;
x = 1;
```
如果 declare 关键字没有给出变量的具体类型,那么变量类型就是`any`
```typescript
declare let x;
x = 1;
```
上面示例中,变量`x`的类型为`any`
下面的例子是脚本使用浏览器全局对象`document`
```typescript
declare var document;
document.title = "Hello";
```
上面示例中declare 告诉编译器,变量`document`的类型是外部定义的(具体定义在 TypeScript 内置文件`lib.d.ts`)。
如果 TypeScript 没有找到`document`的外部定义,这里就会假定它的类型是`any`
注意declare 关键字只用来给出类型描述,是纯的类型代码,不允许设置变量的初始值,即不涉及值。
```typescript
// 报错
declare let x:number = 1;
```
上面示例中declare 设置了变量的初始值,结果就报错了。
## declare function
declare 关键字可以给出外部函数的类型描述。
下面是一个例子。
```typescript
declare function sayHello(
name:string
):void;
sayHello('张三');
```
上面示例中declare 命令给出了`sayHello()`的类型描述,因此可以直接使用它。
注意,这种单独的函数类型声明语句,只能用于`declare`命令后面。一方面TypeScript 不支持单独的函数类型声明语句;另一方面, declare 关键字后面也不能带有函数的具体实现。
```typescript
// 报错
function sayHello(
name:string
):void;
function sayHello(name) {
return '你好,' + name;
}
```
上面示例中,单独写函数的类型声明就会报错。
## declare class
declare 给出 class 的描述描述写法如下。
```typescript
declare class Animal {
constructor(name:string);
eat():void;
sleep():void;
}
```
下面是一个复杂一点的例子。
```typescript
declare class C {
// 静态成员
public static s0():string;
private static s1:string;
// 属性
public a:number;
private b:number;
// 构造函数
constructor(arg:number);
// 方法
m(x:number, y:number):number;
// 存取器
get c():number;
set c(value:number);
// 索引签名
[index:string]:any;
}
```
同样的declare 后面不能给出 Class 的具体实现或初始值。
## declare moduledeclare namespace
如果想把变量、函数、类组织在一起,可以将 declare 与 module 或 namespace 一起使用。
```typescript
declare namespace AnimalLib {
class Animal {
constructor(name:string);
eat():void;
sleep():void;
}
type Animals = 'Fish' | 'Dog';
}
// 或者
declare module AnimalLib {
class Animal {
constructor(name:string);
eat(): void;
sleep(): void;
}
type Animals = 'Fish' | 'Dog';
}
```
上面示例中declare 关键字给出了 module 或 namespace 的类型描述。
declare module 和 declare namespace 里面,加不加 export 关键字都可以。
```typescript
declare namespace Foo {
export var a: boolean;
}
declare module 'io' {
export function readFile(filename:string):string;
}
```
上面示例中namespace 和 module 里面使用了 export 关键字。
下面的例子是当前脚本使用了`myLib`这个外部库,它有方法`makeGreeting()`和属性`numberOfGreetings`
```typescript
let result = myLib.makeGreeting('你好');
console.log('欢迎词:' + result);
let count = myLib.numberOfGreetings;
```
`myLib`的类型描述就可以这样写。
```typescript
declare namespace myLib {
function makeGreeting(s:string):string;
let numberOfGreetings:number;
}
```
declare 关键字的另一个用途,是为外部模块添加属性和方法时,给出新增部分的类型描述。
```typescript
import { Foo as Bar } from 'moduleA';
declare module 'moduleA' {
interface Bar extends Foo {
custom: {
prop1:string;
}
}
}
```
上面示例中,从模块`moduleA`导入了一个`Foo`,将其重命名为`Bar`,并用 declare 关键字增加一个属性`custom`
declare module 后面的模块名可以使用通配符。
```typescript
declare module 'my-plugin-*' {
interface PluginOptions {
enabled: boolean;
priority: number;
}
function initialize(options: PluginOptions): void;
export = initialize;
}
```
上面示例中,模块名`my-plugin-*`表示适配所有以`my-plugin-`开头的模块名(比如`my-plugin-logger`)。
## declare global
如果要为 JavaScript 引擎的原生对象添加属性和方法,可以使用`declare global {}`语法。
```typescript
export {};
declare global {
interface String {
toSmallString(): string;
}
}
String.prototype.toSmallString = ():string => {
// 具体实现
return '';
};
```
上面示例中,为 JavaScript 原生的`String`对象添加了`toSmallString()`方法。declare global 给出这个新增方法的类型描述。
这个示例第一行的空导出语句`export {}`,表示当前脚本是一个模块。这是因为 declare global必须用在模块里面。
下面的示例是为 window 对象添加一个属性`myAppConfig`
```typescript
export {};
declare global {
interface window {
myAppConfig:object;
}
}
const config = window.myAppConfig;
```
declare global 只能扩充现有对象的类型描述,不能增加新的顶层类型。
## declare enum
declare 关键字给出 enum 类型描述的例子如下,后面的写法都是允许的。
```typescript
declare enum E1 {
A,
B,
}
declare enum E2 {
A = 0,
B = 1,
}
declare const enum E3 {
A,
B,
}
declare const enum E3 {
A = 0,
B = 1,
}
```
## declare module 命令
我们可以为每个模块文件,定义一个`.d.ts`文件。但是,更方便的做法是为整个项目,定义一个大的`.d.ts`文件,在这个文件里面使用`declare module`定义每个模块文件的类型。
下面的示例是`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 normalize(p: string): string;
export function join(...paths: any[]): string;
export var sep: string;
}
```
使用时,脚本使用三斜杠命令,加载这个类型声明文件。
```typescript
/// <reference path="node.d.ts"/>
```
如果不加载上面的`reference`标签,脚本文件使用外部模块时,就需要在脚本里面使用 declare 命令单独给出外部模块的类型。
但是,对于某些第三方模块,原始作者没有提供接口类型,这时可以在脚本顶部加上下面一行命令。
```typescript
declare module "模块名";
// 例子
declare module "hot-new-module";
```
加上上面的命令以后,外部模块即使没有类型,也可以通过编译。但是,从该模块输入的所有接口都将为`any`类型。
## 扩展模块类型
declare 可以用来对一个现有模块进行类型扩充,主要适合需要扩充外部模块的情况。
如果一个项目有多个模块文件,可以对一个文件中声明的类型,在另一个文件中进行扩展。
```typescript
// a.ts
export interface A {
x: number;
}
// b.ts
import { A } from './a';
declare module './a' {
interface A {
y: number;
}
}
const a:A = { x: 0, y: 0 };
```
上面示例中,脚本`a.ts`定义了一个接口`A`,脚本`b.ts`为这个接口添加了属性`y``declare module './a' {}`表示对`a.ts`里面的模块,进行类型声明,而同名 interface 会自动合并,所以等同于扩展类型。
使用这种语法进行模块的类型扩展时,有两点需要注意:
1`declare module NAME`语法里面的模块名`NAME`,跟 import 和 export 的模块名规则是一样的,且必须跟当前文件加载该模块的语句写法(上例`import { A } from './a'`)保持一致。
2不能创建新的顶层类型。也就是说只能对`a.ts`模块中已经存在的类型进行扩展,不允许增加新的顶层类型,比如新定义一个接口`B`
3不能对默认的`default`接口进行扩展,只能对 export 命令输出的命名接口进行扩充。这是因为在进行类型扩展时,需要依赖输出的接口名。
## 参考链接
- [How Does The Declare Keyword Work In TypeScript?](https://timmousk.com/blog/typescript-declare/), Tim Mouskhelichvili

826
docs/decorator-legacy.md Normal file
View File

@@ -0,0 +1,826 @@
# 装饰器的传统语法
本章介绍装饰器的传统语法。
## --experimentalDecorators 编译参数
使用装饰器的传统语法,需要打开`--experimentalDecorators`编译参数。
```bash
$ tsc --target ES5 --experimentalDecorators
```
除了`--experimentalDecorators`这个配置项目用来打开装饰器支持,还有另外一个配置项`--emitDecoratorMetadata`,用来产生一些元数据,供其他工具(比如 reflect-metadata )使用。
这两个配置项可以在命令行设置,也可以在`tsconfig.json`文件里面进行设置。
```javascript
{
"compilerOptions": {
"target": "ES6",
"experimentalDecorators": true,
"emitDecoratorMetadata": true
}
}
```
## 装饰器的种类
按照所装饰的不同对象,装饰器可以分成五类。
> - 类装饰器Class Decorators用于类。
> - 属性装饰器Property Decorators用于属性。
> - 方法装饰器Method Decorators用于方法。
> - 存取器装饰器Accessor Decorators用于类的 set 或 get 方法。
> - 参数装饰器Parameter Decorators用于方法的参数。
下面是这五种装饰器一起使用的一个示例。
```typescript
@ClassDecorator() // A
class A {
@PropertyDecorator() // B
name: string;
@MethodDecorator() //C
fly(
@ParameterDecorator() // D
meters: number
) {
// code
}
@AccessorDecorator() // E
get egg() {
// code
}
set egg(e) {
// code
}
}
```
上面示例中A 是类装饰器B 是属性装饰器C 是方法装饰器D 是参数装饰器E 是存取器装饰器。
注意,构造方法没有方法装饰器,只有参数装饰器。类装饰器其实就是用来装饰构造方法。
另外,装饰器只能用于类,要么应用于一个类,要么应用于一个类的内部成员,不能用于独立的函数。
```typescript
function Decorator() {
console.log('In Decorator');
}
@Decorator // 报错
function decorated() {
console.log('in decorated');
}
```
上面示例中,装饰器用于一个普通函数,这是无效的,结果报错。
## 类装饰器
类装饰器应用于类class但实际上是应用于类的构造方法。
类装饰器有唯一参数,就是构造方法,可以在装饰器内部,对构造方法进行各种改造。如果类装饰器有返回值,就会替换掉原来的构造方法。
类装饰器的类型定义如下。
```typescript
type ClassDecorator = <TFunction extends Function>
(target: TFunction) => TFunction | void;
```
上面定义中,类型参数`TFunction`必须是函数,实际上就是构造方法。类装饰器的返回值,要么是返回处理后的原始构造方法,要么返回一个新的构造方法。
下面就是一个示例。
```typescript
function f(target:any) {
console.log('apply decorator')
return target;
}
@f
class A {}
// 输出apply decorator
```
上面示例中,使用了装饰器`@f`,因此类`A`的构造方法会自动传入`f`
`A`不需要新建实例,装饰器也会执行。装饰器会在代码加载阶段执行,而不是在运行时执行,而且只会执行一次。
由于 TypeScript 存在编译阶段所以装饰器对类的行为的改变实际上发生在编译阶段。这意味着TypeScript 装饰器能在编译阶段运行代码,也就是说,它本质就是编译时执行的函数。
下面再看一个示例。
```typescript
@sealed
class BugReport {
type = "report";
title: string;
constructor(t:string) {
this.title = t;
}
}
function sealed(constructor: Function) {
Object.seal(constructor);
Object.seal(constructor.prototype);
}
```
上面示例中,装饰器`@sealed()`会锁定`BugReport`这个类,使得它无法新增或删除静态成员和实例成员。
如果除了构造方法,类装饰器还需要其他参数,可以采取“工厂模式”,即把装饰器写在一个函数里面,该函数可以接受其他参数,执行后返回装饰器。但是,这样就需要调用装饰器的时候,先执行一次工厂函数。
```typescript
function factory(info:string) {
console.log('received: ', info);
return function (target:any) {
console.log('apply decorator');
return target;
}
}
@factory('log something')
class A {}
```
上面示例中,函数`factory()`的返回值才是装饰器,所以加载装饰器的时候,要先执行一次`@factory('log something')`,才能得到装饰器。这样做的好处是,可以加入额外的参数,本例是参数`info`
总之,`@`后面要么是一个函数名,要么是函数表达式,甚至可以写出下面这样的代码。
```typescript
@((constructor: Function) => {
console.log('log something');
})
class InlineDecoratorExample {
// ...
}
```
上面示例中,`@`后面是一个箭头函数,这也是合法的。
类装饰器可以没有返回值,如果有返回值,就会替代所装饰的类的构造函数。由于 JavaScript 的类等同于构造函数的语法糖,所以装饰器通常返回一个新的类,对原有的类进行修改或扩展。
```typescript
function decorator(target:any) {
return class extends target {
value = 123;
};
}
@decorator
class Foo {
value = 456;
}
const foo = new Foo();
console.log(foo.value); // 123
```
上面示例中,装饰器`decorator`返回一个新的类,替代了原来的类。
上例的装饰器参数`target`类型是`any`,可以改成构造方法,这样就更准确了。
```typescript
type Constructor = {
new(...args: any[]): {}
};
function decorator<T extends Constructor> (
target: T
) {
return class extends target {
value = 123;
};
}
```
这时,装饰器的行为就是下面这样。
```javascript
@decorator
class A {}
// 等同于
class A {}
A = decorator(A) || A;
```
上面代码中,装饰器要么返回一个新的类`A`,要么不返回任何值,`A`保持装饰器处理后的状态。
## 方法装饰器
方法装饰器用来装饰类的方法,它的类型定义如下。
```typescript
type MethodDecorator = <T>(
target: Object,
propertyKey: string|symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
```
方法装饰器一共可以接受三个参数。
- target对于类的静态方法类的构造函数或者对于类的实例方法类的原型。
- propertyKey所装饰方法的方法名类型为`string|symbol`
- descriptor所装饰方法的描述对象。
方法装饰器的返回值(如果有的话),就是修改后的该方法的描述对象,可以覆盖原始方法的描述对象。
下面是一个示例。
```typescript
function enumerable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.enumerable = value;
};
}
class Greeter {
greeting: string;
constructor(message:string) {
this.greeting = message;
}
@enumerable(false)
greet() {
return 'Hello, ' + this.greeting;
}
}
```
上面示例中,方法装饰器`@enumerable()`装饰 Greeter 类的`greet()`方法,作用是修改该方法的描述对象的可遍历性属性`enumerable``@enumerable(false)`表示将该方法修改成不可遍历。
下面再看一个例子。
```typescript
function logger(
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
const original = descriptor.value;
descriptor.value = function (...args) {
console.log('params: ', ...args);
const result = original.call(this, ...args);
console.log('result: ', result);
return result;
}
}
class C {
@logger
add(x: number, y:number ) {
return x + y;
}
}
```
上面示例中,方法装饰器`@logger`用来装饰`add()`方法,它的作用是将该方法的结果输出一条日志。每当`add()`调用一次控制台就会打印出一条日志“result: ...”。
## 属性装饰器
属性装饰器用来装饰属性,类型定义如下。
```typescript
type PropertyDecorator =
(
target: Object,
propertyKey: string|symbol
) => void;
```
属性装饰器函数接受两个参数。
- target对于实例属性类的原型对象prototype或者对于静态属性类的构造函数。
- propertyKey所装饰属性的属性名注意类型有可能是字符串也有可能是 Symbol 值。
属性装饰器不需要返回值,如果有的话,也会被忽略。
下面是一个示例。
```typescript
function ValidRange(min:number, max:number) {
return (target:Object, key:string) => {
Object.defineProperty(target, key, {
set: function(v:number) {
if (v < min || v > max) {
throw new Error(`Not allowed value ${v}`);
}
}
});
}
}
// 输出 Installing ValidRange on year
class Student {
@ValidRange(1920, 2020)
year!: number;
}
const stud = new Student();
// 报错 Not allowed value 2022
stud.year = 2022;
```
上面示例中,装饰器`ValidRange`对属性`year`设立了一个上下限检查器,只要该属性赋值时,超过了上下限,就会报错。
注意,属性装饰器的第一个参数,对于实例属性是类的原型对象,而不是实例对象(即不是`this`对象)。这是因为装饰器执行时,类还没有新建实例,所以实例对象不存在。
由于拿不到`this`,所以属性装饰器无法获得实例属性的值。这也是它没有在参数里面提供属性描述对象的原因。
```typescript
function logProperty(target: Object, member: string) {
const prop = Object.getOwnPropertyDescriptor(target, member);
console.log(`Property ${member} ${prop}`);
}
class PropertyExample {
@logProperty
name:string = 'Foo';
}
// 输出 Property name undefined
```
上面示例中,属性装饰器`@logProperty`内部想要获取实例属性`name`的属性描述对象,结果拿到的是`undefined`
因为上例的`target`是类的原型对象,不是实例对象,所以拿不到`name`属性,也就是说`target.name`是不存在的,所以拿到的是`undefined`。只有通过`this.name`才能拿到`name`属性,但是这时`this`还不存在。
属性装饰器不仅无法获得实例属性的值,也不能初始化或修改实例属性,而且它的返回值也会被忽略。因此,它的作用很有限。
不过如果属性装饰器设置了当前属性的取值器setter然后在构造函数里面为实例属性赋值这时可以拿到属性的值。
```typescript
function Min(limit:number) {
return function(
target: Object,
propertyKey: string
) {
let value: string;
const getter = function() {
return value;
};
const setter = function(newVal:string) {
if(newVal.length < limit) {
throw new Error(`Your password should be bigger than ${limit}`);
}
else {
value = newVal;
}
};
Object.defineProperty(target, propertyKey, {
get: getter,
set: setter
});
}
}
class User {
username: string;
@Min(8)
password: string;
constructor(username: string, password: string){
this.username = username;
this.password = password;
}
}
const u = new User('Foo', 'pass');
// 报错 Your password should be bigger than 8
```
上面示例中,属性装饰器`@Min`通过设置存取器,拿到了实例属性的值。
## 存取器装饰器
存取器装饰器用来装饰类的存取器accessor。所谓“存取器”指的是某个属性的取值器getter和存值器setter
存取器装饰器的类型定义,与方法装饰器一致。
```typescript
type AccessorDecorator = <T>(
target: Object,
propertyKey: string|symbol,
descriptor: TypedPropertyDescriptor<T>
) => TypedPropertyDescriptor<T> | void;
```
存取器装饰器有三个参数。
- target对于静态属性的存取器类的构造函数或者对于实例属性的存取器类的原型。
- propertyKey存取器的属性名。
- descriptor存取器的属性描述对象。
存取器装饰器的返回值(如果有的话),会作为该属性新的描述对象。
下面是一个示例。
```typescript
function configurable(value: boolean) {
return function (
target: any,
propertyKey: string,
descriptor: PropertyDescriptor
) {
descriptor.configurable = value;
};
}
class Point {
private _x: number;
private _y: number;
constructor(x:number, y:number) {
this._x = x;
this._y = y;
}
@configurable(false)
get x() {
return this._x;
}
@configurable(false)
get y() {
return this._y;
}
}
```
上面示例中,装饰器`@configurable(false)`关闭了所装饰属性(`x``y`)的属性描述对象的`configurable`键(即关闭了属性的可配置性)。
下面的示例是将装饰器用来验证属性,如果赋值不满足条件就报错。
```typescript
function validator(
target: Object,
propertyKey: string,
descriptor: PropertyDescriptor
){
const originalGet = descriptor.get;
const originalSet = descriptor.set;
if (originalSet) {
descriptor.set = function (val) {
if (val > 100) {
throw new Error(`Invalid value for ${propertyKey}`);
}
originalSet.call(this, val);
};
}
}
class C {
#foo!: number;
@validator
set foo(v) {
this.#foo = v;
}
get foo() {
return this.#foo;
}
}
const c = new C();
c.foo = 150;
// 报错
```
上面示例中,装饰器用自己定义的存值器,取代了原来的存值器,加入了验证条件。
TypeScript 不允许对同一个属性的存取器getter 和 setter使用同一个装饰器也就是说只能装饰两个存取器里面的一个且必须排在前面的那一个否则报错。
```typescript
// 报错
class Person {
#name:string;
@Decorator
set name(n:string) {
this.#name = n;
}
@Decorator // 报错
get name() {
return this.#name;
}
}
```
上面示例中,`@Decorator`同时装饰`name`属性的存值器和取值器,所以报错。
但是,下面的写法不会报错。
```typescript
class Person {
#name:string;
@Decorator
set name(n:string) {
this.#name = n;
}
get name() {
return this.#name;
}
}
```
上面示例中,`@Decorator`只装饰它后面第一个出现的存值器(`set name()`),并不装饰取值器(`get name()`),所以不报错。
装饰器之所以不能同时用于同一个属性的存值器和取值器,原因是装饰器可以从属性描述对象上面,同时拿到取值器和存值器,因此只调用一次就够了。
## 参数装饰器
参数装饰器用来装饰构造方法或者其他方法的参数。它的类型定义如下。
```typescript
type ParameterDecorator = (
target: Object,
propertyKey: string|symbol,
parameterIndex: number
) => void;
```
参数装饰器接受三个参数。
- target对于静态方法类的构造函数或者对于类的实例方法类的原型对象。
- propertyKey所装饰的方法的名字类型为`string|symbol`
- parameterIndex当前参数在方法的参数序列的位置从0开始
该装饰器不需要返回值,如果有的话会被忽略。
下面是一个示例。
```typescript
function log(
target: Object,
propertyKey: string|symbol,
parameterIndex: number
) {
console.log(`${String(propertyKey)} NO.${parameterIndex} Parameter`);
}
class C {
member(
@log x:number,
@log y:number
) {
console.log(`member Paremeters: ${x} ${y}`);
}
}
const c = new C();
c.member(5, 5);
// member NO.1 Parameter
// member NO.0 Parameter
// member Paremeters: 5 5
```
上面示例中,参数装饰器会输出参数的位置序号。注意,后面的参数会先输出。
跟其他装饰器不同,参数装饰器主要用于输出信息,没有办法修改类的行为。
## 装饰器的执行顺序
前面说过,装饰器只会执行一次,就是在代码解析时执行,哪怕根本没有调用类新建实例,也会执行,而且从此就不再执行了。
执行装饰器时,按照如下顺序执行。
1. 实例相关的装饰器。
1. 静态相关的装饰器。
1. 构造方法的参数装饰器。
1. 类装饰器。
请看下面的示例。
```typescript
function f(key:string):any {
return function () {
console.log('执行:', key);
};
}
@f('类装饰器')
class C {
@f('静态方法')
static method() {}
@f('实例方法')
method() {}
constructor(@f('构造方法参数') foo:any) {}
}
```
加载上面的示例,输出如下。
```typescript
```
同一级装饰器的执行顺序,是按照它们的代码顺序。但是,参数装饰器的执行总是早于方法装饰器。
```typescript
function f(key:string):any {
return function () {
console.log('执行:', key);
};
}
class C {
@f('方法1')
m1(@f('参数1') foo:any) {}
@f('属性1')
p1: number;
@f('方法2')
m2(@f('参数2') foo:any) {}
@f('属性2')
p2: number;
}
```
加载上面的示例,输出如下。
```typescript
1
1
1
2
2
2
```
上面示例中,实例装饰器的执行顺序,完全是按照代码顺序的。但是,同一个方法的参数装饰器,总是早于该方法的方法装饰器执行。
如果同一个方法或属性有多个装饰器,那么装饰器将顺序加载、逆序执行。
```typescript
function f(key:string):any {
console.log('加载:', key);
return function () {
console.log('执行:', key);
};
}
class C {
@f('A')
@f('B')
@f('C')
m1() {}
}
// 加载: A
// 加载: B
// 加载: C
// 执行: C
// 执行: B
// 执行: A
```
如果同一个方法有多个参数,那么参数也是顺序加载、逆序执行。
```typescript
function f(key:string):any {
console.log('加载:', key);
return function () {
console.log('执行:', key);
};
}
class C {
method(
@f('A') a:any,
@f('B') b:any,
@f('C') c:any,
) {}
}
// 加载: A
// 加载: B
// 加载: C
// 执行: C
// 执行: B
// 执行: A
```
## 为什么装饰器不能用于函数?
装饰器只能用于类和类的方法,不能用于函数,主要原因是存在函数提升。
JavaScript 的函数不管在代码的什么位置,都会提升到代码顶部。
```typescript
addOne(1);
function addOne(n:number) {
return n + 1;
}
```
上面示例中,函数`addOne()`不会因为在定义之前执行而报错,原因就是函数存在提升,会自动提升到代码顶部。
如果允许装饰器可以用于普通函数,那么就有可能导致意想不到的情况。
```typescript
let counter = 0;
let add = function (target:any) {
counter++;
};
@add
function foo() {
//...
}
```
上面示例中,本来的意图是装饰器`@add`每使用一次,变量`counter`就加`1`,但是实际上会报错,因为函数提升的存在,使得实际执行的代码是下面这样。
```javascript
@add // 报错
function foo() {
//...
}
let counter = 0;
let add = function (target:any) {
counter++;
};
```
上面示例中,`@add`还没有定义就调用了,从而报错。
总之,由于存在函数提升,使得装饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。
另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行,没必要写成装饰器。
```javascript
function doSomething(name) {
console.log('Hello, ' + name);
}
function loggingDecorator(wrapped) {
return function() {
console.log('Starting');
const result = wrapped.apply(this, arguments);
console.log('Finished');
return result;
}
}
const wrapped = loggingDecorator(doSomething);
```
上面示例中,`loggingDecorator()`是一个装饰器,只要把原始函数传入它执行,就能起到装饰器的效果。
## 多个装饰器的合成
多个装饰器可以应用于同一个目标对象,可以写在一行。
```typescript
@f @g x
```
上面示例中,装饰器`@f``@g`同时装饰目标对象`x`
多个装饰器也可以写成多行。
```typescript
@f
@g
x
```
多个装饰器的效果,类似于函数的合成,按照从里到外的顺序执行。对于上例来说,就是执行`f(g(x))`
前面也说过,如果`f``g`是表达式,那么需要先从外到里求值。
## 参考链接
- [A Complete Guide to TypeScript Decorators](https://saul-mirone.github.io/a-complete-guide-to-typescript-decorator/), by Saul Mirone
- [Deep introduction to using and implementing TypeScript decorators](https://techsparx.com/nodejs/typescript/decorators/introduction.html), by David Herron
- [Deep introduction to property decorators in TypeScript](https://techsparx.com/nodejs/typescript/decorators/properties.html), by David Herron
- [Deep introduction to accessor decorators in TypeScript](https://techsparx.com/nodejs/typescript/decorators/accessors.html), by David Herron
- [Using Property Decorators in Typescript with a real example](https://dev.to/danywalls/using-property-decorators-in-typescript-with-a-real-example-44e), by Dany Paredes

672
docs/decorator.md Normal file
View File

@@ -0,0 +1,672 @@
# TypeScript 装饰器
## 简介
装饰器Decorator是一种语法结构用来修改类class的行为。
在语法上,装饰器有如下几个特征。
1第一个字符或者说前缀`@`,后面是一个表达式。
2`@`后面的表达式,必须是一个函数(或者执行后可以得到一个函数)。
3这个函数接受所修饰对象的一些相关值作为参数。
4这个函数要么不返回值要么返回一个新对象取代所修饰的目标对象。
举例来说,有一个函数`Injectable()`当作装饰器使用,那么需要写成`@Injectable`,然后放在某个类的前面。
```typescript
@Injectable class A {
// ...
}
```
上面示例中,由于有了装饰器`@Injectable`,类`A`的行为在运行时就会发生改变。
下面就是一个最简单的装饰器。
```typescript
function simpleDecorator() {
console.log('hi');
}
@simpleDecorator
class A {} // "hi"
```
上面示例中,函数`simpleDecorator()`用作装饰器,附加在类`A`之上,后者在代码解析时就会打印一行日志。
编译上面的代码会报错,提示没有用到装饰器的参数。现在就为装饰器加上参数,让它更像正式运行的代码。
```typescript
function simpleDecorator(
target:any,
context:any
) {
console.log('hi, this is ' + target);
return target;
}
@simpleDecorator
class A {} // "hi, this is class A {}"
```
上面的代码就可以顺利通过编译了,代码含义这里先不解释。大家只要理解,类`A`在执行前会先执行装饰器`simpleDecorator()`,并且会向装饰器自动传入参数就可以了。
装饰器有多种形式,基本上只要在`@`符号后面添加表达式都是可以。下面都是合法的装饰器。
```typescript
@myFunc
@myFuncFactory(arg1, arg2)
@libraryModule.prop
@someObj.method(123)
@(wrap(dict['prop']))
```
注意,`@`后面的表达式,最终执行后得到的应该是一个函数。
相比使用子类改变父类,装饰器更加简洁优雅,缺点是不那么直观,功能也受到一些限制。所以,装饰器一般只用来为类添加某种特定行为。
```javascript
@frozen class Foo {
@configurable(false)
@enumerable(true)
method() {}
@throttle(500)
expensiveMethod() {}
}
```
上面示例中,一共有四个装饰器,一个用在类本身(`@frozen`),另外三个用在类的方法(`@configurable``@enumerable``@throttle`)。它们不仅增加了代码的可读性,清晰地表达了意图,而且提供一种方便的手段,增加或修改类的功能。
## 装饰器的版本
TypeScript 从早期开始就支持装饰器。但是装饰器的语法后来发生了变化。ECMAScript 标准委员会最终通过的语法标准,与 TypeScript 早期使用的语法有很大差异。
目前TypeScript 5.0 同时支持两种装饰器语法。标准语法可以直接使用,传统语法需要打开`--experimentalDecorators`编译参数。
```bash
$ tsc --target ES5 --experimentalDecorators
```
本章介绍装饰器的标准语法,下一章介绍传统语法。
## 装饰器的结构
装饰器函数的类型定义如下。
```typescript
type Decorator = (
value: DecoratedValue,
context: {
kind: string;
name: string | symbol;
addInitializer(initializer: () => void): void;
// Dont always exist:
static: boolean;
private: boolean;
access: {get: () => unknown, set: (value: unknown) => void};
}
) => void | ReplacementValue; // only fields differ
```
上面代码中,`Decorator`是装饰器的类型定义。它是一个函数,接受`value``context`两个参数。
其中,`value`参数是所装饰的对象,`context`是装饰器的上下文对象TypeScript 提供一个原生接口`ClassMethodDecoratorContext`,描述这个对象。
```typescript
function decorator(
value:any,
context:ClassMethodDecoratorContext
) {
// ...
}
```
`context`对象有以下属性。
1`kind`:字符串,表示装饰器类型,可能取以下的值。
- 'class'
- 'method'
- 'getter'
- 'setter'
- 'accessor'
- 'field'
这表示一共有六种类型的装饰器。
2`name`:字符串或者 Symbol 值,所装饰对象的名字,比如类名、属性名等。
3`addInitializer()`:函数,用来在类的初始化阶段,对方法进行一些处理。以前,这些处理通常放在构造函数里面,早于方法本身执行,现在改为放在装饰器的`context`对象里面,具体例子请参阅《方法装饰器》一节。
注意,`addInitializer()`函数没有返回值。
4`private`:布尔值,表示所装饰的方法或属性,是否为私有。
## 类装饰器
类装饰器的类型描述如下。
```typescript
type ClassDecorator = (
value: Function,
context: {
kind: 'class';
name: string | undefined;
addInitializer(initializer: () => void): void;
}
) => Function | void;
```
请看下面的例子。
```typescript
class InstanceCollector {
instances = new Set();
install = (value:any, {kind}:any) => {
if (kind === 'class') {
const _this = this;
return function (...args:any[]) {
const inst = new value(...args);
_this.instances.add(inst);
return value;
} as unknown as typeof MyClass;
}
return;
};
}
const collector = new InstanceCollector();
@collector.install
class MyClass {}
const inst1 = new MyClass();
const inst2 = new MyClass();
const inst3 = new MyClass();
collector.instances // new Set([inst1, inst2, inst3])
```
上面示例中,类装饰器`@collector.install`将所有实例加入一个集合变量`collector.instances`
类装饰器返回的函数,会作为新的构造函数。
```typescript
function countInstances(value:any, context:any) {
let instanceCount = 0;
const wrapper = function (...args:any[]) {
instanceCount++;
const instance = new value(...args);
instance.count = instanceCount;
return instance;
} as unknown as typeof MyClass;
wrapper.prototype = value.prototype; // A
return wrapper;
}
@countInstances
class MyClass {}
const inst1 = new MyClass();
inst1 instanceof MyClass // true
inst1.count // 1
```
上面示例实现了实例的计数。为了确保`wrapper()`的返回值是`MyClass`的示例,特别加入`A`行,确保两者的原型对象是一致的。否则,新的构造函数`wrapper`的原型对象,与`MyClass`不同,通不过`instanceof`运算符。
类装饰器也可以直接返回一个新的类。
```typescript
function countInstances(value:any, context:any) {
let instanceCount = 0;
return class extends value {
constructor(...args:any[]) {
super(...args);
instanceCount++;
this.count = instanceCount;
}
};
}
@countInstances
class MyClass {}
const inst1 = new MyClass();
inst1 instanceof MyClass // true
inst1.count // 1
```
上面示例中,`@countInstances`返回一个`MyClass`的子类。
下面的例子是通过类装饰器,禁止使用`new`命令调用类。
```typescript
function functionCallable(
value as any, {kind} as any
) {
if (kind === 'class') {
return function (...args) {
if (new.target !== undefined) {
throw new TypeError('This function cant be new-invoked');
}
return new value(...args);
}
}
}
@functionCallable
class Person {
constructor(name) {
this.name = name;
}
}
const robin = Person('Robin');
robin.name // 'Robin'
```
上面示例中,类装饰器`@functionCallable`返回一个新的构造方法,里面判断`new.target`是否不为空,如果是的,就表示通过`new`命令调用,从而报错。
## 方法装饰器
方法装饰器用来装饰类的方法method。它的类型描述如下。
```typescript
type ClassMethodDecorator = (
value: Function,
context: {
kind: 'method';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;
```
它的上下文对象`context`有以下属性。
- static布尔值表示是否为静态方法。
- private布尔值表示是否为私有方法。
- access函数表示方法的存取器但是只能用来取值只有`get()`方法),不能用来赋值(不能定义`set()`方法)。
```typescript
class C {
@trace
toString() {
return 'C';
}
}
function trace(decoratedMethod) {
// 此处略
}
```
方法装饰器的实质是执行下面的操作。
```typescript
class C {
toString() {
return 'C';
}
}
C.prototype.toString = trace(C.prototype.toString);
```
如果装饰器返回一个新的函数,就会替代所装饰的对象。
```typescript
function replaceMethod() {
return function () {
return `How are you, ${this.name}?`;
}
}
class Person {
constructor(name) {
this.name = name;
}
@replaceMethod
hello() {
return `Hi ${this.name}!`;
}
}
const robin = new Person('Robin');
robin.hello() // 'How are you, Robin?'
```
上面示例中,装饰器`@replaceMethod`返回的函数,就成为了新的`hello()`方法。
下面是另一个例子。
```typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@log
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
function log(originalMethod:any, context:ClassMethodDecoratorContext) {
const methodName = String(context.name);
function replacementMethod(this: any, ...args: any[]) {
console.log(`LOG: Entering method '${methodName}'.`)
const result = originalMethod.call(this, ...args);
console.log(`LOG: Exiting method '${methodName}'.`)
return result;
}
return replacementMethod;
}
const person = new Person('张三');
person.greet()
// "LOG: Entering method 'greet'."
// "Hello, my name is 张三."
// "LOG: Exiting method 'greet'."
```
下面是装饰器上下文对象的`addInitializer()`方法的例子。类的方法往往会在构造方法里面,进行`this`的绑定。
```typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
// greet() 绑定 this
this.greet = this.greet.bind(this);
}
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
```
上面例子中,构造方法将`greet()`方法绑定了`this`,这行代码必须放在构造方法里面。现在,它可以移到`addInitializer()`
```typescript
function bound(
originalMethod:any, context:ClassMethodDecoratorContext
) {
const methodName = context.name;
if (context.private) {
throw new Error(`不能绑定私有方法 ${methodName as string}`);
}
context.addInitializer(function () {
this[methodName] = this[methodName].bind(this);
});
}
```
上面示例中,绑定`this`转移到了`addInitializer()`方法里面。
```typescript
function collect(
value,
{name, addInitializer}
) {
addInitializer(function () {
if (!this.collectedMethodKeys) {
this.collectedMethodKeys = new Set();
}
this.collectedMethodKeys.add(name);
});
}
class C {
@collect
toString() {}
@collect
[Symbol.iterator]() {}
}
const inst = new C();
inst.@collect // new Set(['toString', Symbol.iterator])
```
上面示例中,装饰器`@collect`会将所装饰的成员名字,加入一个 Set 集合`collectedMethodKeys`
## 属性装饰器
属性装饰器用来装饰定义在类顶部的属性field。它的类型描述如下。
```typescript
type ClassFieldDecorator = (
value: undefined,
context: {
kind: 'field';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown, set: (value: unknown) => void };
addInitializer(initializer: () => void): void;
}
) => (initialValue: unknown) => unknown | void;
```
注意,装饰器的第一个参数`value`的类型是`undefined`,这意味着这个参数实际上是不存在的,即不能从`value`获取目标属性的值。
如果要获取属性的值,必须使用存取器,请看下面的例子。
```typescript
let acc;
function exposeAccess(
value, {access}
) {
acc = access;
}
class Color {
@exposeAccess
name = 'green'
}
const green = new Color();
green.name // 'green'
acc.get.call(green) // 'green'
acc.set.call(green, 'red');
green.name // 'red'
```
上面示例中,`@exposeAccess``name`属性的装饰器,它的第二个参数就是`name`的上下文对象,其中`access`属性包含了取值器(`get`)和存值器(`set`),可以对`name`属性进行取值和赋值。
下面的例子是更改属性的初始值。
```typescript
function twice() {
return initialValue => initialValue * 2;
}
class C {
@twice
field = 3;
}
const inst = new C();
inst.field // 6
```
## getter 装饰器setter 装饰器
getter 装饰器和 setter 装饰器的类型描述如下。
```typescript
type ClassGetterDecorator = (
value: Function,
context: {
kind: 'getter';
name: string | symbol;
static: boolean;
private: boolean;
access: { get: () => unknown };
addInitializer(initializer: () => void): void;
}
) => Function | void;
type ClassSetterDecorator = (
value: Function,
context: {
kind: 'setter';
name: string | symbol;
static: boolean;
private: boolean;
access: { set: (value: unknown) => void };
addInitializer(initializer: () => void): void;
}
) => Function | void;
```
下面的例子是将取值器的结果,保存为一个属性,加快后面的读取。
```typescript
class C {
@lazy
get value() {
console.log('正在计算……');
return '开销大的计算结果';
}
}
function lazy(
value:any,
{kind, name}:any
) {
if (kind === 'getter') {
return function (this:any) {
const result = value.call(this);
Object.defineProperty(
this, name,
{
value: result,
writable: false,
}
);
return result;
};
}
return;
}
const inst = new C();
inst.value
// 正在计算……
// '开销大的计算结果'
inst.value
// '开销大的计算结果'
```
上面示例中,第一次读取`inst.value`,会进行计算,然后装饰器`@lazy`将结果存入只读属性`value`,后面再读取这个属性,就不会进行计算了。
## 装饰器的执行顺序
装饰器的执行分为两三个阶段。
1评估evaluation计算`@`符号后面的表达式的值,得到的应该是函数。
2应用application将调用装饰器后得到的结果应用于类的定义。其中类装饰器在所有方法装饰器和属性装饰器之后应用。
请看下面的例子。
```typescript
function d(str:string) {
console.log(`评估 @d(): ${str}`);
return (
value:any, context:any
) => console.log(`应用 @d(): ${str}`);
}
function log(str:string) {
console.log(str);
return str;
}
@d('类装饰器')
class T {
@d('静态属性装饰器')
static staticField = log('静态属性值');
@d('原型方法')
[log('计算方法名')]() {}
@d('实例属性')
instanceField = log('实例属性值');
}
```
上面示例中,类`T`有四种装饰器:类装饰器、静态属性装饰器、方法装饰器、属性装饰器。
它的运行结果如下。
```typescript
// "评估 @d(): 类装饰器"
// "评估 @d(): 静态属性装饰器"
// "评估 @d(): 原型方法"
// "计算方法名"
// "评估 @d(): 实例属性"
// "应用 @d(): 原型方法"
// "应用 @d(): 静态属性装饰器"
// "应用 @d(): 实例属性"
// "应用 @d(): 类装饰器"
// "静态属性值"
```
可以看到,类载入的时候,代码按照以下顺序执行。
1装饰器评估这一步计算装饰器的值首先是类装饰器然后是类内部的装饰器按照它们出现的顺序。
注意,如果属性名或方法名是计算值(本例是“计算方法名”),则它们在对应的装饰器评估之后,也会进行自身的评估。
2装饰器应用实际执行装饰器函数将它们与对应的方法和属性进行结合。
原型方法的装饰器首先应用,然后是静态属性和静态方法装饰器,接下来是实例属性装饰器,最后是类装饰器。
注意,“实例属性值”在类初始化的阶段并不执行,直到类实例化时才会执行。
如果一个方法或属性有多个装饰器,则内层的装饰器先执行,外层的装饰器后执行。
```typescript
class Person {
name: string;
constructor(name: string) {
this.name = name;
}
@bound
@log
greet() {
console.log(`Hello, my name is ${this.name}.`);
}
}
```
上面示例中,`greet()`有两个装饰器,内层的`@log`先执行,外层的`@bound`针对得到的结果再执行。
## 参考链接
- [JavaScript metaprogramming with the 2022-03 decorators API](https://2ality.com/2022/10/javascript-decorators.html)

605
docs/enum.md Normal file
View File

@@ -0,0 +1,605 @@
# TypeScript 的 Enum 类型
Enum 是 TypeScript 新增的一种数据结构和类型,中文译为“枚举”。
## 简介
实际开发中,经常需要定义一组相关的常量。
```typescript
const RED = 1;
const GREEN = 2;
const BLUE = 3;
let color = userInput();
if (color === RED) {/* */}
if (color === GREEN) {/* */}
if (color === BLUE) {/* */}
throw new Error('wrong color');
```
上面示例中,常量`RED``GREEN``BLUE`是相关的,而且它们具体等于什么值并不重要,只要不相等就可以了。
TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。
```typescript
enum Color {
Red, // 0
Green, // 1
Blue // 2
}
```
上面示例声明了一个 Enum 结构`Color`,里面包含三个成员`Red``Green``Blue`。第一个成员的值默认为整数`0`,第二个为`1`,第二个为`2`,以此类推。
使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。
```typescript
let c = Color.Green; // 1
// 等同于
let c = Color['Green']; // 1
```
Enum 结构本身也是一种类型。比如,上例的变量`c`等于`1`,它的类型可以是 Color也可以是`number`
```typescript
let c:Color = Color.Green; // 正确
let c:number = Color.Green; // 正确
```
上面示例中,变量`c`的类型写成`Color``number`都可以。但是,`Color`类型的语义更好。
Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。
```typescript
// 编译前
enum Color {
Red, // 0
Green, // 1
Blue // 2
}
// 编译后
let Color = {
Red: 0,
Green: 1,
Blue: 2
};
```
上面示例是 Enum 结构编译前后的对比。
由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。
Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。
```typescript
enum Operator {
ADD,
DIV,
MUL,
SUB
}
function compute(
op:Operator,
a:number,
b:number
) {
switch (op) {
case Operator.ADD:
return a + b;
case Operator.DIV:
return a / b;
case Operator.MUL:
return a * b;
case Operator.SUB:
return a - b;
default:
throw new Error('wrong operator');
}
}
compute(Operator.ADD, 1, 3) // 4
```
上面示例中Enum 结构`Operator`的四个成员表示四则运算“加减乘除”。代码根本不需要用到这四个成员的值,只用成员名就够了。
Enum 作为类型有一个缺点,就是输入任何数值都不报错。
```typescript
enum Bool {
No,
Yes
}
function foo(noYes:Bool) {
// ...
}
func(33); // 不报错
```
上面代码中,函数`foo`的参数`noYes`只有两个可用的值,但是输入任意数值,编译都不会报错。
另外,由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等)。
```typescript
enum Color {
Red,
Green,
Blue
}
const Color = 'red'; // 报错
```
上面示例Enum 结构与变量同名,导致报错。
很大程度上Enum 结构可以被对象的`as const`断言替代。
```typescript
enum Foo {
A,
B,
C,
}
const Bar = {
A: 0,
B: 1,
C: 2,
} as const;
if x === Foo.A{}
// 等同于
if (x === Bar.A) {}
```
上面示例中,对象`Bar`使用了`as const`断言,作用就是使得它的属性无法修改。这样的话,`Foo``Bar`的行为就很类似了,前者完全可以用后者替代,而且后者还是 JavaScript 的原生数据结构。
## Enum 成员的值
Enum 成员默认不必赋值系统会从零开始逐一递增按照顺序为每个成员赋值比如0、1、2……
但是,也可以为 Enum 成员显式赋值。
```typescript
enum Color {
Red,
Green,
Blue
}
// 等同于
enum Color {
Red = 0,
Green = 1,
Blue = 2
}
```
上面示例中Enum 每个成员的值都是显式赋值。
成员的值可以是任意数值但不能是大整数Bigint
```typescript
enum Color {
Red = 90,
Green = 0.5,
Blue = 7n // 报错
}
```
上面示例中Enum 成员的值可以是小数,但不能是 Bigint。
成员的值甚至可以相同。
```typescript
enum Color {
Red = 0,
Green = 0,
Blue = 0
}
```
如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。
```typescript
enum Color {
Red = 7,
Green, // 8
Blue // 9
}
// 或者
enum Color {
Red, // 0
Green = 7,
Blue // 8
}
```
Enum 成员的值也可以使用计算式。
```typescript
enum Permission {
UserRead = 1 << 8,
UserWrite = 1 << 7,
UserExecute = 1 << 6,
GroupRead = 1 << 5,
GroupWrite = 1 << 4,
GroupExecute = 1 << 3,
AllRead = 1 << 2,
AllWrite = 1 << 1,
AllExecute = 1 << 0,
}
enum Bool {
No = 123,
Yes = Math.random(),
}
```
上面示例中Enum 成员的值等于一个计算式,或者等于函数的返回值,都是正确的。
Enum 成员值都是只读的,不能重新赋值。
```typescript
enum Color {
Red,
Green,
Blue
}
Color.Red = 4; // 报错
```
上面示例中,重新为 Enum 成员赋值就会报错。
为了让这一点更醒目,通常会在 enum 关键字前面加上`const`修饰,表示这是常量,不能再次赋值。
```typescript
const enum Color {
Red,
Green,
Blue
}
```
加上`const`还有一个好处,就是编译为 JavaScript 代码后,代码中 Enum 成员会被替换成对应的值,这样能提高性能表现。
```typescript
const enum Color {
Red,
Green,
Blue
}
const x = Color.Red;
const y = Color.Green;
const z = Color.Blue;
// 编译后
const x = 0 /* Color.Red */;
const y = 1 /* Color.Green */;
const z = 2 /* Color.Blue */;
```
上面示例中,由于 Enum 结构前面加了`const`关键字,所以编译产物里面就没有生成对应的对象,而是把所有 Enum 成员出现的场合,都替换成对应的常量。
如果希望加上`const`关键词后,运行时还能访问 Enum 结构(即编译后依然将 Enum 转成对象),需要在编译时打开`preserveConstEnums`参数。
## 同名 Enum 的合并
多个同名的 Enum 结构合并成一个 Enum 结构。
```typescript
enum Foo {
A,
}
enum Foo {
B = 1,
}
enum Foo {
C = 2,
}
// 等同于
enum Foo {
A,
B = 1
C = 2
}
```
上面示例中,`Foo`分成三段定义,系统会自动把它们合并。
Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。
```typescript
enum Foo {
A,
}
enum Foo {
B, // 报错
}
```
上面示例中,`Foo`的两段定义的第一个成员,都没有设置初始值,导致报错。
同名 Enum 合并时,不能有同名成员,否则报错。
```typescript
enum Foo {
A,
B
}
enum Foo {
B = 1, // 报错
C
}
```
上面示例中,`Foo`的两段定义有一个同名成员`B`,导致报错。
同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。
```typescript
// 正确
enum E {
A,
}
enum E {
B = 1,
}
// 正确
const enum E {
A,
}
const enum E {
B = 1,
}
// 报错
enum E {
A,
}
const enum E2 {
B = 1,
}
```
同名 Enum 的合并,最大用处就是补充外部定义的 Enum 结构。
## 字符串 Enum
Enum 成员的值除了设为数值还可以设为字符串。也就是说Enum 也可以用作一组相关字符串的集合。
```typescript
enum Direction {
Up = 'UP',
Down = 'DOWN',
Left = 'LEFT',
Right = 'RIGHT',
}
```
上面示例中,`Direction`就是字符串枚举,每个成员的值都是字符串。
注意,字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。
```typescript
enum Foo {
A, // 0
B = 'hello',
C // 报错
}
```
上面示例中,`A`之前没有其他成员,所以可以不设置初始值,默认等于`0``C`之前有一个字符串成员,必须有初始值,不赋值就报错了。
Enum 成员可以是字符串和数值混合赋值。
```typescript
enum Enum {
One = 'One',
Two = 'Two',
Three = 3,
Four = 4,
}
```
除了数值和字符串Enum 成员不允许使用其他值(比如 Symbol 值)。
变量类型如果是字符串 Enum就不能再赋值为字符串这跟数值 Enum 不一样。
```typescript
enum MyEnum {
One = 'One',
Two = 'Two',
}
let s = MyEnum.One;
s = 'One'; // 报错
```
上面示例中,变量`s`的类型是`MyEnum`,再赋值为字符串就报错。
由于这个原因,如果函数的参数类型是字符串 Enum传参时就不能直接传入字符串而要传入 Enum 成员。
```typescript
enum MyEnum {
One = 'One',
Two = 'Two',
}
function f(arg:MyEnum) {
return 'arg is ' + arg;
}
f('One') // 报错
```
上面示例中,参数类型是`MyEnum`,直接传入字符串会报错。
所以,字符串 Enum 作为一种类型,有限定函数参数的作用。
前面说过,数值 Enum 的成员值往往不重要。但是有些场合,开发者可能希望 Enum 成员值可以保存一些有用的信息,所以 TypeScript 才设计了字符串 Enum.
```typescript
const enum MediaTypes {
JSON = 'application/json',
XML = 'application/xml',
}
const url = 'localhost';
fetch(url, {
headers: {
Accept: MediaTypes.JSON,
},
}).then(response => {
// ...
});
```
上面示例中,函数`fetch()`的参数对象的属性`Accept`,只能接受一些指定的字符串。这时就很适合把字符串放进一个 Enum 结构,通过成员值来引用这些字符串。
字符串 Enum 可以使用联合类型union代替。
```typescript
function move(
where:'Up'|'Down'|'Left'|'Right'
) {
// ...
}
```
上面示例中,函数参数`where`属于联合类型,效果跟指定为字符串 Enum 是一样的。
注意,字符串 Enum 的成员值,不能使用表达式赋值。
```typescript
enum MyEnum {
A = 'one',
B = ['T', 'w', 'o'].join('') // 报错
}
```
上面示例中,成员`B`的值是一个字符串表达式,导致报错。
## keyof 运算符
keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。
```typescript
enum MyEnum {
A = 'a',
B = 'b'
}
// 'A'|'B'
type Foo = keyof typeof MyEnum;
```
上面示例中,`keyof typeof MyEnum`可以取出`MyEnum`的所有成员名,所以类型`Foo`等同于联合类型`'A'|'B'`
注意,这里的`typeof`是必需的,否则`keyof MyEnum`相当于`keyof number`
```typescript
// "toString" | "toFixed" | "toExponential" |
// "toPrecision" | "valueOf" | "toLocaleString"
type Foo = keyof MyEnum;
```
上面示例中,类型`Foo`等于类型`number`的所有原生属性名组成的联合类型。
这是因为 Enum 作为类型,本质上属于`number``string`的一种变体,而`typeof MyEnum`会将`MyEnum`当作一个值处理,从而先其转为对象类型,就可以再用`keyof`运算符返回该对象的所有属性名。
如果要返回 Enum 所有的成员值,可以使用`in`运算符。
```typescript
enum MyEnum {
A = 'a',
B = 'b'
}
// { aany, b: any }
type Foo = { [key in MyEnum]: any };
```
上面示例中,采用属性索引可以取出`MyEnum`的所有成员值。
## 反向映射
数值 Enum 存在反向映射,即可以通过成员值获得成员名。
```typescript
enum Weekdays {
Monday = 1,
Tuesday,
Wednesday,
Thursday,
Friday,
Saturday,
Sunday
}
console.log(Weekdays[3]) // Wednesday
```
上面示例中Enum 成员`Wednesday`的值等于3从而可以从成员值`3`取到对应的成员名`Wednesday`,这就叫反向映射。
这是因为 TypeScript 会将上面的 Enum 结构,编译成下面的 JavaScript 代码。
```javascript
var Weekdays;
(function (Weekdays) {
Weekdays[Weekdays["Monday"] = 1] = "Monday";
Weekdays[Weekdays["Tuesday"] = 2] = "Tuesday";
Weekdays[Weekdays["Wednesday"] = 3] = "Wednesday";
Weekdays[Weekdays["Thursday"] = 4] = "Thursday";
Weekdays[Weekdays["Friday"] = 5] = "Friday";
Weekdays[Weekdays["Saturday"] = 6] = "Saturday";
Weekdays[Weekdays["Sunday"] = 7] = "Sunday";
})(Weekdays || (Weekdays = {}));
```
上面代码中,实际进行了两组赋值,以第一个成员为例。
```javascript
Weekdays[
Weekdays["Monday"] = 1
] = "Monday";
```
上面代码有两个赋值运算符(`=`),实际上等同于下面的代码。
```javascript
Weekdays["Monday"] = 1;
Weekdays[1] = "Monday";
```
注意,这种情况只发生在数值 Enum对于字符串 Enum不存在反向映射。这是因为字符串 Enum 编译后只有一组赋值。
```typescript
enum MyEnum {
A = 'a',
B = 'b'
}
// 编译后
var MyEnum;
(function (MyEnum) {
MyEnum["A"] = "a";
MyEnum["B"] = "b";
})(MyEnum || (MyEnum = {}));
```

154
docs/es6.md Normal file
View File

@@ -0,0 +1,154 @@
# TypeScript 的 ES6 类型
## `Map<K, V>`
```typescript
let map2 = new Map(); // Key any, value any
let map3 = new Map<string, number>(); // Key string, value number
```
TypeScript 使用 Map 类型,描述 Map 结构。
```typescript
const myMap: Map<boolean,string> = new Map([
[false, 'no'],
[true, 'yes'],
]);
```
Map 是一个泛型,使用时,比如给出类型变量。
由于存在类型推断,也可以省略类型参数。
```typescript
const myMap = new Map([
[false, 'no'],
[true, 'yes'],
]);
```
## `Set<T>`
## `Promise<T>`
## async 函数
async 函数的的返回值是一个 Promise 对象。
```typescript
const p:Promise<number> = /* ... */;
async function fn(): Promise<number> {
var i = await p;
return i + 1;
}
```
## `Iterable<>`
对象只要部署了 Iterator 接口,就可以用`for...of`循环遍历。Generator 函数(生成器)返回的就是一个具有 Iterator 接口的对象。
TypeScript 使用泛型`Iterable<T>`表示具有 Iterator 接口的对象,其中`T`表示 Iterator 接口包含的值类型(每一轮遍历获得的值)。
```typescript
interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
}
```
上面是`Iterable<T>`接口的定义,表示一个具有`Symbol.iterator`属性的对象,该属性是一个函数,调用后返回的是一个 Iterator 对象。
Iterator 对象必须具有`next()`方法,另外还具有两个可选方法`return()``throw()`,类型表述如下。
```typescript
interface Iterator<T> {
next(value?: any): IteratorResult<T>;
return?(value?: any): IteratorResult<T>;
throw?(e?: any): IteratorResult<T>;
}
```
上面的类型定义中,可以看到`next()``return()``throw()`这三个方法的返回值是一个部署了`IteratorResult<T>`接口的对象。
`IteratorResult<T>`接口的定义如下。
```typescript
interface IteratorResult<T> {
done: boolean; //表示遍历是否结束
value: T; // 当前遍历得到的值
}
```
上面的类型定义表示Iterator 对象的`next()`等方法的返回值,具有`done``value`两个属性。
下面的例子是 Generator 函数返回一个具有 Iterator 接口的对象。
```typescript
function* g():Iterable<string> {
for (var i = 0; i < 100; i++) {
yield '';
}
yield* otherStringGenerator();
}
```
上面示例中,生成器`g()`返回的类型是`Iterable<string>`,其中`string`表示 Iterator 接口包含的是字符串。
这个例子的类型声明可以省略,因为 TypeScript 可以自己推断出来 Iterator 接口的类型。
```typescript
function* g() {
for (var i = 0; i < 100; i++) {
yield ""; // infer string
}
yield* otherStringGenerator();
}
```
另外,扩展运算符(`...`)后面的值必须具有 Iterator 接口,下面是一个例子。
```typescript
function toArray<X>(xs: Iterable<X>):X[] {
return [...xs]
}
```
## Generator 函数
Generator 函数返回一个同时具有 Iterable 接口(具有`[Symbol.iterator]`属性)和 Iterator 接口(具有`next()`方法)的对象,因此 TypeScript 提供了一个泛型`IterableIterator<T>`,表示同时满足`Iterable<T>``Iterator<T>`两个接口。
```typescript
interface IterableIterator<T> extends Iterator<T> {
[Symbol.iterator](): IterableIterator<T>;
}
```
上面类型定义中,`IterableIterator<T>`接口就是在`Iterator<T>`接口的基础上,加上`[Symbol.iterator]`属性。
下面是一个例子。
```typescript
function* createNumbers(): IterableIterator<number> {
let n = 0;
while (1) {
yield n++;
}
}
let numbers = createNumbers()
// {value: 0, done: false}
numbers.next()
// {value: 1, done: false}
numbers.next()
// {value: 2, done: false}
numbers.next()
```
上面示例中,`createNumbers()`返回的对象`numbers`即具有`next()`方法,也具有`[Symbol.iterator]`属性,所以满足`IterableIterator<T>`接口。
## 参考链接
- [Typing Iterables and Iterators with TypeScript](https://www.geekabyte.io/2019/06/typing-iterables-and-iterators-with.html)

931
docs/function.md Normal file
View File

@@ -0,0 +1,931 @@
# TypeScript 的函数类型
## 简介
函数的类型声明,需要在声明函数时,给出参数的类型和返回值的类型。
```typescript
function hello(
txt:string
):void {
console.log('hello ' + txt);
}
```
上面示例中,函数`hello()`在声明时,需要给出参数`txt`的类型string以及返回值的类型`void`),后者写在参数列表的圆括号后面。`void`类型表示没有返回值,详见后文。
如果不指定参数类型(比如上例不写`txt`的类型TypeScript 就会推断参数类型,如果缺乏足够信息,就会推断该参数的类型为`any`
返回值的类型通常可以不写,因为 TypeScript 自己会推断出来。
```typescript
function hello(txt:string) {
console.log('hello ' + txt);
}
```
上面示例中,由于没有`return`语句TypeScript 会推断出函数`hello()`没有返回值。
不过,有时候出于文档目的,或者为了防止不小心改掉返回值,还是会写返回值的类型。
如果变量被赋值为一个函数,变量的类型有两种写法。
```typescript
// 写法一
const hello = function (txt:string) {
console.log('hello ' + txt);
}
// 写法二
const hello:
(txt:string) => void
= function (txt) {
console.log('hello ' + txt);
};
```
上面示例中,变量`hello`被赋值为一个函数,它的类型有两种写法。写法一是通过等号右边的函数类型,推断出变量`hello`的类型;写法二则是使用箭头函数的形式,为变量`hello`指定类型,参数的类型写在箭头左侧,返回值的类型写在箭头右侧。
写法二有两个地方需要注意。
首先,函数的参数要放在圆括号里面,不放会报错。
其次,类型里面的参数名(本例是`txt`)是必须的。有的语言的函数类型可以不写参数名(比如 C 语言),但是 TypeScript 不行。如果写成`(string) => void`TypeScript 会理解成函数有一个名叫 string 的参数,并且这个`string`参数的类型是`any`
```typescript
type MyFunc = (string, number) => number;
// (string: any, number: any) => number
```
上面示例中,函数类型没写参数名,导致 TypeScript 认为参数类型都是`any`
函数类型里面的参数名与实际参数名,可以不一致。
```typescript
let f:(x:number) => number;
f = function (y:number) {
return y;
};
```
上面示例中,函数类型里面的参数名为`x`,实际的函数定义里面,参数名为`y`,两者并不相同。
如果有多个变量被赋值为同一种类型的函数,写法二用起来就很麻烦。因此,往往用`type`命令为函数类型定义一个别名,便于指定给其他变量。
```typescript
type MyFunc = (txt:string) => void;
const hello:MyFunc = function (txt) {
console.log('hello ' + txt);
};
```
上面示例中,`type`命令为函数类型定义了一个别名`MyFunc`,后面使用就很方便,变量可以指定为这个类型。
变量所赋值的函数的参数个数可以少于指定类型但是不能多于指定类型即这种情况下TypeScript 允许省略参数。
```typescript
let myFunc:
(a:number, b:number) => number;
myFunc = (a:number) => a; // 正确
myFunc = (
a:number, b:number, c:number
) => a + b + c; // 报错
```
上面示例中,变量`myFunc`的类型只能接受两个参数,如果被赋值只有一个参数的函数,并不报错。但是,被赋值为有三个参数的函数,就会报错。
这是因为 JavaScript 函数在声明时往往有多余的参数,实际使用时可以只传入一部分参数。比如,数组的`forEach()`方法的参数是一个函数,该函数默认有三个参数`(item, index, array) => void`,实际上往往只使用第一个参数`(item) => any`。因此TypeScript 允许参数较少的函数,兼容于参数较多的函数。
```typescript
let x = (a:number) => 0;
let y = (b:number, s:string) => 0;
y = x; // 正确
x = y; // 报错
```
上面示例中,函数`x`只有一个参数,函数`y`有两个参数,`x`可以赋值给`y`,反过来就不行。
如果一个变量要套用另一个函数的类型,有一个小技巧,就是使用`typeof`运算符。
```typescript
function add(
x:number,
y:number
) {
return x + y;
}
const myAdd:typeof add = function (x, y) {
return x + y;
}
```
上面示例中,函数`myAdd()`的类型与函数`add()`是一样的,那么就可以定义成`typeof add`。因为函数名`add`本身不是类型,而是一个值,所以要用`typeof`运算符返回它的类型。
这是一个很有用的技巧,任何需要类型的地方,都可以使用`typeof`运算符从一个值获取类型。
函数类型还可以采用对象的写法。
```typescript
let add:{
(x:number, y:number):number
};
add = function (x, y) {
return x + y;
};
```
上面示例中,变量`add`的类型就写成了一个对象。
函数类型的对象写法如下。
```typescript
{
():
}
```
注意,这种写法的函数参数与返回值之间,间隔符是冒号`:`,而不是正常写法的箭头`=>`,因为这里采用的是对象类型的写法,对象的属性名与属性值之间使用的是冒号。
这种写法平时很少用,但是非常合适用在一个场合:函数本身存在属性。
```typescript
function f(x:number) {
console.log(x);
}
f.version = '1.0';
```
上面示例中,函数`f()`本身还有一个属性`foo`。这时,`f`完全就是一个对象,类型就要使用对象的写法。
```typescript
let foo: {
(x:number): void;
version: string
} = f;
```
函数类型也可以使用 Interface 来声明这种写法就是对象写法的翻版详见《Interface》一章。
```typescript
interface myfn {
(a:number, b:number): number;
}
var add:myfn = (a, b) => a + b;
```
上面示例中interface 命令定义了接口`myfn`,这个接口的类型就是一个用对象表示的函数。
## Function 类型
TypeScript 提供 Function 类型表示函数,任何函数都属于这个类型。
```typescript
function doSomething(f:Function) {
return f(1, 2, 3);
}
```
上面示例中,参数`f`的类型就是`Function`,代表这是一个函数。
Function 类型的值都可以直接执行。
Function 类型的函数可以接受任意数量的参数,每个参数的类型都是`any`,返回值的类型也是`any`,代表没有任何约束,所以不建议使用这个类型,给出函数详细的类型声明会更好。
## 箭头函数
箭头函数是普通函数的一种简化写法,它的类型写法与普通函数类似。
```typescript
const repeat = (
str:string,
times:number
):string => str.repeat(times);
```
上面示例中,变量`repeat`被赋值为一个箭头函数,类型声明写在箭头函数的定义里面。其中,参数的类型写在参数名后面,返回值类型写在参数列表的圆括号后面。
注意,类型写在箭头函数的定义里面,与使用箭头函数表示函数类型,写法有所不同。
```typescript
function greet(
fn:(a:string) => void
):void {
fn('world');
}
```
上面示例中,函数`greet()`的参数`fn`是一个函数,类型就用箭头函数表示。这时,`fn`的返回值类型要写在箭头右侧,而不是写在参数列表的圆括号后面。
下面再看一个例子。
```typescript
type Person = { name: string };
const people = ['alice', 'bob', 'jan'].map(
(name):Person => ({name})
);
```
上面示例中,`Person`是一个类型别名,代表一个对象,该对象有属性`name`。变量`people`是数组的`map()`方法的返回值。
`map()`方法的参数是一个箭头函数`(name):Person => ({name})`,该箭头函数的参数`name`的类型省略了,因为可以从`map()`的类型定义推断出来,箭头函数的返回值类型为`Person`。相应地,变量`people`的类型是`Person[]`
至于箭头后面的`({name})`,表示返回一个对象,该对象有一个属性`name`,它的属性值为变量`name`的值。这里的圆括号是必须的,否则`(name):Person => {name}`的大括号表示函数体,即函数体内有一行语句`name`,同时由于没有`return`语句,这个函数不会返回任何值。
注意,下面两种写法都是不对的。
```typescript
// 错误
(name:Person) => ({name})
// 错误
name:Person => ({name})
```
上面的两种写法在本例中都是错的。第一种写法表示,箭头函数的参数`name`的类型是`Person`,同时没写函数返回值的类型,让 TypeScript 自己去推断。第二种写法中,函数参数缺少圆括号。
## 可选参数
如果函数的某个参数可以省略,则在参数名后面加问号表示。
```typescript
function f(x?:number) {
// ...
}
f(); // OK
f(10); // OK
```
上面示例中,虽然参数`x`后面有问号,表示该参数可以省略,不一定需要给出。
参数名带有问号,表示该参数的类型实际上是`原始类型|undefined`,它有可能为`undefined`。比如,上例的`x`虽然类型声明为`number`,但是实际上是`number|undefined`
```typescript
function f(x?:number) {
return x;
}
f(undefined) // 正确
```
上面示例中,参数`x`是可选的,等同于说`x`可以赋值为`undefined`
但是,反过来就不成立,类型显式设为`undefined`的参数,就不能省略。
```typescript
function f(x:number|undefined) {
return x;
}
f() // 报错
```
上面示例中,参数`x`的类型是`number|undefined`,表示要么传入一个数值,要么传入`undefined`,如果省略这个参数,就会报错。
函数的可选参数只能在参数列表的尾部,跟在必选参数的后面。
```typescript
let myFunc:
(a?:number, b:number) => number; // 报错
```
上面示例中,可选参数在必选参数前面,就报错了。
如果前部参数有可能为空,这时只能显示注明该参数类型可能为`undefined`
```typescript
let myFunc:
(
a:number|undefined,
b:number
) => number;
```
上面示例中,参数`a`有可能为空,就只能显式注明类型包括`undefined`,传参时也要显式传入`undefined`
函数体内部用到可选参数时,需要判断该参数是否为`undefined`
```typescript
let myFunc:
(a:number, b?:number) => number;
myFunc = function (x, y) {
if (y === undefined) {
return x;
}
return x + y;
}
```
上面示例中,由于函数的第二个参数为可选参数,所以函数体内部需要判断一下,该参数是否为空。
## 参数默认值
TypeScript 函数的参数默认值写法,与 JavaScript 一致。
设置了默认值的参数,就是可选的。如果不传入该参数,它就会等于默认值。
```typescript
function createPoint(
x:number = 0,
y:number = 0
):[number, number] {
return [x, y];
}
createPoint() // [0, 0]
```
上面示例中,参数`x``y`的默认值都是`0`,调用`createPoint()`时,这两个参数都是可以省略的。这里其实可以省略`x``y`的类型声明,因为可以从默认值推断出来。
```typescript
function createPoint(
x = 0, y = 0
) {
return [x, y];
}
```
可选参数与默认值不能同时使用。
```typescript
// 报错
function f(x?: number = 0) {
// ...
}
```
上面示例中,`x`是可选参数,还设置了默认值,结果就报错了。
设有默认值的参数,如果传入`undefined`,也会触发默认值。
```typescript
function f(x = 456) {
return x;
}
f2(undefined) // 456
```
具有默认值的参数如果不位于参数列表的末尾,调用时不能省略,如果要触发默认值,必须显式传入`undefined`
```typescript
function add(
x:number = 0,
y:number
) {
return x + y;
}
add(1) // 报错
add(undefined, 1) // 正确
```
## 参数解构
函数参数如果存在变量解构,类型写法如下。
```typescript
function f(
[x, y]: [number, number]
) {
// ...
}
function sum(
{ a, b, c }: {
a: number;
b: number;
c: number
}
) {
console.log(a + b + c);
}
```
参数结构可以结合类型别名type 命令)一起使用,代码会看起来简洁一些。
```typescript
type ABC = { a:number; b:number; c:number };
function sum({ a, b, c }:ABC) {
console.log(a + b + c);
}
```
## rest 参数
rest 参数表示函数剩余的所有参数,它可以是数组(剩余参数类型相同),也可能是元组(剩余参数类型不同)。
```typescript
// rest 参数为数组
function joinNumbers(...nums:number[]) {
// ...
}
// rest 参数为元组
function f(...args:[boolean, number]) {
// ...
}
```
注意,元组需要声明每一个剩余参数的类型。如果元组里面的参数是可选的,则要使用可选参数。
```typescript
function f(
...args: [boolean, string?]
) {}
```
下面是一个 rest 参数的例子。
```typescript
function multiply(n:number, ...m:number[]) {
return m.map((x) => n * x);
}
```
上面示例中,参数`m`就是 rest 类型,它的类型是一个数组。
rest 参数甚至可以嵌套。
```typescript
function f(...args:[boolean, ...string[]]) {
// ...
}
```
rest 参数可以与变量解构结合使用。
```typescript
function repeat(
...[str, times]: [string, number]
):string {
return str.repeat(times);
}
// 等同于
function repeat(
str: string,
times: number
):string {
return str.repeat(times);
}
```
## readonly 只读参数
如果函数内部不能修改某个参数,可以在函数定义时,在参数类型前面加上`readonly`关键字,表示这是只读参数。
```typescript
function arraySum(
arr:readonly number[]
) {
// ...
arr[0] = 0; // 报错
}
```
上面示例中,参数`arr`的类型是`readonly number[]`,表示为只读参数。如果函数体内部修改这个数组,就会报错。
## void 类型
void 类型表示函数没有返回值。
```typescript
function f():void {
console.log('hello');
}
```
上面示例中,函数`f`没有返回值,类型就要写成`void`
如果返回其他值,就会报错。
```typescript
function f():void {
return 123; // 报错
}
```
上面示例中,函数`f()`的返回值类型是`void`,但是实际返回了一个数值,编译时就报错了。
void 类型允许返回`undefined``null`
```typescript
function f():void {
return undefined; // 正确
}
function f():void {
return null; // 正确
}
```
如果打开了`--strictNullChecks`编译选项,那么 void 类型只允许返回`undefined`。如果返回`null`,就会报错。这是因为 JavaScript 规定,如果函数没有返回值,就等同于返回`undefined`
```typescript
// --strictNullChecks=true
function f():void {
return undefined; // 正确
}
function f():void {
return null; // 报错
}
```
需要特别注意的是,如果变量、对象方法、函数参数的类型是 void 类型的函数,那么并不代表不能赋值为有返回值的函数。恰恰相反,该变量、对象方法和函数参数可以接受返回任意值的函数,这时并不会报错。
```typescript
type voidFunc = () => void;
const f:voidFunc = () => {
return 123;
};
```
上面示例中,变量`f`的类型是`voidFunc`,是一个没有返回值的函数类型。但是实际上,`f`的值是一个有返回值的函数(返回`123`),编译时不会报错。
这是因为,这时 TypeScript 认为,这里的 void 类型只是表示该函数的返回值没有利用价值,或者说不应该使用该函数的返回值。只要不用到这里的返回值,就不会报错。
这样设计是有现实意义的。举例来说,数组方法`Array.prototype.forEach(fn)`的参数`fn`是一个函数,而且这个函数应该没有返回值,即返回值类型是`void`
但是,实际应用中,很多时候传入的函数是有返回值,但是它的返回值不重要,或者不产生作用。
```typescript
const src = [1, 2, 3];
const ret = [];
src.forEach(el => ret.push(el));
```
上面示例中,`push()`有返回值,表示新插入的元素在数组里面的位置。但是,对于`forEach()`方法来说,这个返回值是没有作用的,根本用不到,所以 TypeScript 不会报错。
如果后面使用了这个函数的返回值,就违反了约定,则会报错。
```typescript
type voidFunc = () => void;
const f:voidFunc = () => {
return 123;
};
f() * 2 // 报错
```
上面示例中,最后一行报错了,因为根据类型声明,`f()`没有返回值,但是却用到了它的返回值,因此报错了。
注意,这种情况仅限于变量、对象方法和函数参数,函数字面量如果声明了返回值是 void 类型,还是不能有返回值。
```typescript
function f():void {
return true; // 报错
}
const f3 = function ():void {
return true; // 报错
};
```
上面示例中,函数字面量声明了返回`void`类型,这时只要有返回值(除了`undefined``null`)就会报错。
除了函数,其他变量声明为`void`类型没有多大意义,因为这时只能赋值为`undefined`或者`null`(假定没有打开`strictNullChecks`) 。
```typescript
let foo:void = undefined;
// 没有打开 --strictNullChecks 的情况下
let bar:void = null;
```
## never 类型
`never`类型表示肯定不会出现的值。它用在函数的返回值,就表示某个函数肯定不会返回值,即函数不会正常执行结束。
它主要有以下两种情况。
1抛出错误的函数。
```typescript
function fail(msg:string):never {
throw new Error(msg);
}
```
上面示例中,函数`fail()`会抛错,不会正常退出,所以返回值类型是`never`
注意,只有抛出错误,才是 never 类型。如果显式用`return`语句返回一个 Error 对象,返回值就不是 never 类型。
```typescript
function fail():Error {
return new Error("Something failed");
}
```
上面示例中,函数`fail()`返回一个 Error 对象,所以返回值类型是 Error。
2无限执行的函数。
```typescript
const sing = function():never {
while (true) {
console.log('sing');
};
```
上面示例中,函数`sing()`会永远执行,不会返回,所以返回值类型是`never`
注意,`never`类型不同于`void`类型。前者表示函数没有执行结束,不可能有返回值;后者表示函数正常执行结束,但是不返回值,或者说返回`undefined`
```typescript
// 正确
function sing():void {
console.log('sing');
}
// 报错
function sing():never {
console.log('sing');
}
```
上面示例中,函数`sing()`虽然没有`return`语句,但实际上是省略了`return undefined`这行语句,真实的返回值是`undefined`。所以,它的返回值类型要写成`void`,而不是`never`,写成`never`会报错。
如果一个函数抛出了异常或者陷入了死循环,那么该函数无法正常返回一个值,因此该函数的返回值类型就是`never`。如果程序中调用了一个返回值类型为`never`的函数,那么就意味着程序会在该函数的调用位置终止,永远不会继续执行后续的代码。
```typescript
function neverReturns():never {
throw new Error();
}
function f(
x:string|undefined
) {
if (x === undefined) {
neverReturns();
}
x; // 推断为 string
}
```
上面示例中,函数`f()`的参数`x`的类型为`string|undefined`。但是,`x`类型为`undefined`时,调用了`neverReturns()`。这个函数不会返回,因此 TypeScript 可以推断出,判断语句后面的那个`x`,类型一定是`string`
## 局部类型
函数内部允许声明其他类型,该类型只在函数内部有效,称为局部类型。
```typescript
function hello(txt:string) {
type message = string;
let newTxt:message = 'hello ' + txt;
return newTxt;
}
const newTxt:message = hello('world'); // 报错
```
上面示例中,类型`message`是在函数`hello()`内部定义的,只能在函数内部使用。在函数外部使用,就会报错。
## 高阶函数
一个函数的返回值还是一个函数那么前一个函数就称为高阶函数higher-order function
下面就是一个例子,箭头函数返回的还是一个箭头函数。
```typescript
(someValue: number) => (multiplier: number) => someValue * multiplier;
```
## 函数重载
有些函数可以接受不同类型或不同个数的参数并且根据参数的不同会有不同的函数行为。这种根据参数类型不同执行不同逻辑的行为称为函数重载function overload
```javascript
reverse('abc') // 'cba'
reverse([1, 2, 3]) // [3, 2, 1]
```
上面示例中,函数`reverse()`可以将参数颠倒输出。参数可以是字符串,也可以是数组。
这意味着,该函数内部有处理字符串和数组的两套逻辑,根据参数类型的不同,分别执行对应的逻辑。这就叫“函数重载”。
TypeScript 对于“函数重载”的类型声明方法是,逐一定义每一种情况的类型。
```typescript
function reverse(str:string):string;
function reverse(arr:any[]):any[];
```
上面示例中,分别对函数`reverse()`的两种参数情况,给予了类型声明。但是,到这里还没有结束,后面还必须对函数`reverse()`给予完整的类型声明。
```typescript
function reverse(str:string):string;
function reverse(arr:any[]):any[];
function reverse(
stringOrArray:string|any[]
):string|any[] {
if (typeof stringOrArray === 'string')
return stringOrArray.split('').reverse().join('');
else
return stringOrArray.slice().reverse();
}
```
上面示例中,前两行类型声明列举了重载的各种情况。第三行是函数本身的类型声明,它必须与前面已有的重载声明兼容。
有一些编程语言允许不同的函数参数对应不同的函数实现。但是JavaScript 函数只能有一个实现,必须在这个实现当中,处理不同的参数。因此,函数体内部就需要判断参数的类型及个数,并根据判断结果执行不同的操作。
```typescript
function add(
x:number,
y:number
):number;
function add(
x:any[],
y:any[]
):any[];
function add(
x:number|any[],
y:number|any[]
):number|any[] {
if (typeof x === 'number' && typeof y === 'number') {
return x + y;
} else
if (Array.isArray(x) && Array.isArray(y)) {
return [...x, ...y];
}
throw new Error('wrong parameters');
}
```
上面示例中,函数`add()`内部使用`if`代码块,分别处理参数的两种情况。
注意,重载的个别类型描述与函数的具体实现之间,不能有其他代码,否则报错。
另外,虽然函数的具体实现里面,有完整的类型声明。但是,函数实际调用的类型,以前面的类型声明为准。比如,上例的函数实现,参数类型和返回值类型都是`number|any[]`,但不意味着参数类型为`number`时返回值类型为`any[]`
函数重载的每个类型声明之间,以及类型声明与函数实现的类型之间,不能有冲突。
```typescript
// 报错
function fn(x:boolean):void;
function fn(x:string):void;
function fn(x:number|string) {
console.log(x);
}
```
上面示例中,函数重载的类型声明与函数实现是冲突的,导致报错。
重载声明的排序很重要,因为 TypeScript 是按照顺序进行检查的,一旦发现符合某个类型声明,就不再往下检查了,所以类型最宽的声明应该放在最后面,防止覆盖其他类型声明。
```typescript
function f(x:any):number;
function f(x:string): 0|1;
function f(x:any):any {
// ...
}
const a:0|1 = f('hi'); // 报错
```
上面声明中,第一行类型声明`x:any`范围最宽,导致函数`f()`的调用都会匹配这行声明,无法匹配第二行类型声明,所以最后一行调用就报错了,因为等号两侧类型不匹配,左侧类型是`0|1`,右侧类型是`number`。这个函数重载的正确顺序是,第二行类型声明放到第一行的位置。
对象的方法也可以使用重载。
```typescript
class StringBuilder {
#data = '';
add(num:number): this;
add(bool:boolean): this;
add(str:string): this;
add(value:any): this {
this.#data += String(value);
return this;
}
toString() {
return this.#data;
}
}
```
上面示例中,方法`add()`也使用了函数重载。
函数重载也可以用来精确描述函数参数与返回值之间的对应关系。
```typescript
function createElement(
tag:'a'
):HTMLAnchorElement;
function createElement(
tag:'canvas'
):HTMLCanvasElement;
function createElement(
tag:'table'
):HTMLTableElement;
function createElement(
tag:string
):HTMLElement {
// ...
}
```
上面示例中,函数重载精确描述了参数`tag`的三个值,所对应的不同的函数返回值。
这个示例的函数重载,也可以用对象表示。
```typescript
type CreateElement = {
(tag:'a'): HTMLAnchorElement;
(tag:'canvas'): HTMLCanvasElement;
(tag:'table'): HTMLTableElement;
(tag:string): HTMLElement;
}
```
由于重载是一种比较复杂的类型声明方法,为了降低复杂性,一般来说,如果可以的话,应该优先使用联合类型替代函数重载。
```typescript
// 写法一
function len(s:string):number;
function len(arr:any[]):number;
function len(x:any):number {
return x.length;
}
// 写法二
function len(x:any[]|string):number {
return x.length;
}
```
上面示例中,写法二使用联合类型,要比写法一的函数重载简单很多。
## 构造函数
JavaScript 语言使用构造函数,生成对象的实例。
构造函数的最大特点,就是必须使用`new`命令调用。
```typescript
const d = new Date();
```
上面示例中,`date()`就是一个构造函数,使用`new`命令调用,返回 Date 对象的实例。
构造函数的类型声明,采用对象形式。
```typescript
type F = {
new (s:string): object;
};
```
上面示例中,类型 F 就是一个构造函数。类型写成一个可执行对象的形式,并且在参数列表前面要加上`new`命令。
某些函数既是构造函数,又可以当作普通函数使用,比如`Date()`。这时,类型声明可以写成下面这样。
```typescript
type F = {
new (s:string): object;
(n?:number): number;
}
```
上面示例中F 既可以当作普通函数执行,也可以当作构造函数使用。
下面是构造函数的一个例子。
```typescript
class Animal {
numLegs: number = 4;
}
function create(c:new () => Animal):Animal {
return new c();
}
const a = create(Animal);
```
上面示例中,函数`create()`的参数`c`是一个构造函数。在 JavaScript 中class本质上是构造函数所以可以传入`create()`
不过,构造函数在 TypeScript 里面实际上只能用类class的形式来实现详见《Class》一章。

548
docs/generics.md Normal file
View File

@@ -0,0 +1,548 @@
# TypeScript 泛型
## 简介
有些时候,函数返回值的类型与参数类型是相关的。
```javascript
function getFirst(arr) {
return arr[0];
}
```
上面示例中,函数`getFirst()`总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。
这个函数的类型声明只能写成下面这样。
```typescript
function f(arr:any[]):any {
return arr[0];
}
```
上面的类型声明,就反映不出参数与返回值之间的类型关系。
为了解决这个问题TypeScript 就引入了“泛型”generics。泛型的特点就是带有“类型参数”type parameter
```typescript
function getFirst<T>(arr:T[]):T {
return arr[0];
}
```
上面示例中,函数`getFirst()`的函数名后面尖括号的部分`<T>`,就是类型参数放在一对尖括号(`<>`)里面。本例只有一个类型参数`T`,可以将其视为类型声明需要的变量,具体的类型由调用时输入的参数类型决定。
参数类型是`T[]`,返回值类型是`T`,就清楚地表示了两者之间的关系。比如,输入的参数类型是`number[]`,那么 T 的值就是`number`,因此返回值类型也是`number`
函数调用时,需要提供类型参数。
```typescript
getFirst<number>([1, 2, 3])
```
上面示例中,调用函数`getFirst()`时,需要在函数名后面使用尖括号,给出类型参数`T`的值,本例是`<number>`
不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。
```typescript
getFirst([1, 2, 3])
```
上面示例中TypeScript 会从实际参数`[1, 2, 3]`,推断出类型参数 T 的值为`number`
有些复杂的使用场景TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。
```typescript
function comb<T>(arr1:T[], arr2:T[]):T[] {
return arr1.concat(arr2);
}
```
上面示例中,两个参数`arr1``arr2`和返回值都是同一个类型。如果不给出类型参数的值,下面的调用会报错。
```typescript
comb([1, 2], ['a', 'b']) // 报错
```
上面示例会报错TypeScript 认为两个参数不是同一个类型。但是,如果类型参数是一个联合类型,就不会报错。
```typescript
comb<number|string>([1, 2], ['a', 'b']) // 正确
```
上面示例中,类型参数是一个联合类型,使得两个参数都符合类型参数,就不报错了。这种情况下,类型参数是不能省略不写的。
类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。
一般会使用`T`type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号“,”分隔。
下面是多个类型参数的例子。
```typescript
function map<T, U>(
arr:T[],
f:(arg:T) => U
):U[] {
return arr.map(f);
}
// 用法实例
map<string, number>(
['1', '2', '3'],
(n) => parseInt(n)
); // 返回 [1, 2, 3]
```
上面示例将数组的实例方法`map()`改写成全局函数,它有两个类型参数`T``U`。含义是,原始数组的类型为`T[]`,对该数组的每个成员执行一个处理函数`f`,将类型`T`转成类型`U`,那么就会得到一个类型为`U[]`的数组。
总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。
## 泛型的写法
泛型主要用在四个场合:函数、接口、类和别名。
### 函数的泛型写法
上一节提到,`function`关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。
```typescript
function id<T>(arg:T):T {
return arg;
}
```
那么对于变量形式定义的函数,泛型有下面两种写法。
```typescript
// 写法一
let myId:<T>(arg:T) => T = id;
// 写法二
let myId:{ <T>(arg:T):T } = id;
```
### 接口的泛型写法
泛型函数也可以采用 inteface 的写法。
```typescript
interface Box<Type> {
contents: Type;
}
let box:Box<string>;
```
上面示例中,使用泛型接口时,需要给出类型参数的值(本例是`string`)。
下面是另一个例子。
```typescript
interface Comparator<T> {
compareTo(value:T):number;
}
class Rectangle implements Comparator<Rectangle> {
compareTo(value:Rectangle): number {
// ...
}
}
```
上面示例中,先定义了一个泛型接口,然后将这个接口用于一个类。
泛型接口还有第二种写法。
```typescript
interface Fn {
<Type>(arg:Type):Type;
}
function id<Type>(arg:Type):Type {
return arg;
}
let myId:Fn = id;
```
上面示例中,类型参数定义在接口内部,所以使用这个接口时(最后一行),不需要给出类型参数的值。
除了声明时不需要给出加类型参数,第二种写法还有一个区别。那就是它的类型参数定义在某个方法之上,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。
### 类的泛型写法
泛型类的类型参数写在类名后面。
```typescript
class Pair<K, V> {
key: K;
value: V;
}
```
下面是继承泛型类的例子。
```typescript
class A<T> {
value: T;
}
class B extends A<any> {
}
```
上面示例中,类`A`有一个类型参数`T`,使用时必须给出`T`的类型,所以类`B`继承时要写成`A<any>`
泛型也可以用在类表达式。
```typescript
const Container = class<T> {
constructor(private readonly data:T) {}
};
const a = new Container<boolean>(true);
const b = new Container<number>(0);
```
上面示例中,新建实例时,需要同时给出类型参数`T`和类参数`data`的值。
下面是另一个例子。
```typescript
class C<NumType> {
value!:NumType;
add!:(x: NumType, y: NumType) => NumType;
}
let foo = new C<number>();
foo.value = 0;
foo.add = function (x, y) {
return x + y;
};
```
上面示例中,先新建类`C`的实例`foo`,然后再定义示例的`value`属性和`add()`方法。类的定义中,属性和方法后面的感叹号是非空断言,告诉 TypeScript 它们都是非空的,后面会赋值。
JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。
```typescript
type Class<T> = new (...args: any[]) => T;
// 或者
interface Class<T> {
new(...args: any[]):T;
}
// 用法实例
function createInstance<T>(
AnyClass:Class<T>,
...args:any[]
):T {
return new AnyClass(...args);
}
```
泛型类描述的是类的实例,不包括静态属性,因为静态属性定义在类的本身。因此,类的静态属性不能引用类型参数。
```typescript
class C<T> {
static data:T; // 报错
constructor(public value:T) {}
}
```
上面示例中,静态属性`data`引用了类型参数`T`,这是不可以的,因为类型参数只能用于实例属性和实例方法,所以报错了。
### 类型别名的泛型写法
type 命令定义的类型别名,也可以使用泛型。
```typescript
type Nullable<T> = T | undefined | null;
```
上面示例中,`Nullable<T>`是一个泛型,只要传入一个类型,就可以得到这个类型与`undefined``null`的一个联合类型。
下面是另一个例子。
```typescript
type Container<T> = { value: T };
const a: Container<number> = { value: 0 };
const b: Container<string> = { value: 'b' };
```
下面是定义树形结构的例子。
```typescript
type Tree<T> = {
value: T;
left: Tree<T> | null;
right: Tree<T> | null;
};
```
上面示例中,类型别名`Tree`内部递归引用了`Tree`自身。
## 类型参数的默认值
类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。
```typescript
function getFirst<T = string>(
arr:T[]
):T {
return arr[0];
}
```
上面示例中,`T = string`表示类型参数的默认值是`string`。调用`getFirst()`时,如果不给出`T`的值TypeScript 就认为`T`等于`string`
但是,因为 TypeScript 会从实际参数推断出`T`的值,从而覆盖掉默认值,所以下面的代码不会报错。
```typescript
getFirst([1, 2, 3]) // 正确
```
上面示例中,实际参数是`[1, 2, 3]`TypeScript 推断 T 等于`number`,从而覆盖掉默认值`string`
类型参数的默认值,往往用在类中。
```typescript
class Generic<T = string> {
list:T[] = []
add(t:T) {
this.list.push(t)
}
}
```
上面示例中,类`Generic`有一个类型参数`T`,默认值为`string`。这意味着,实例方法`add()`的参数`t`的类型,默认是`string`
```typescript
const g = new Generic();
g.add(4) // 报错
g.add('hello') // 正确
```
上面示例中,新建`Generic`的实例`g`时,没有给出类型参数`T`的值,所以`T`就等于`string`。因此,向`add()`方法传入一个数值会报错,传入字符串就不会。
```typescript
const g = new Generic<number>();
g.add(4) // 正确
g.add('hello') // 报错
```
上面示例中,新建实例`g`时,给出了类型参数`T`的值是`number`,因此`add()`方法传入数值不会报错,传入字符串会报错。
一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。
```typescript
<T = boolean, U> // 错误
<T, U = boolean> // 正确
```
上面示例中,依次有两个类型参数`T``U`。如果`T`是可选参数,`U`不是,就会报错。
## 数组的泛型表示
《数组》一章提到过,数组类型有一种表示方法是`Array<T>`。这就是泛型的写法,`Array`是 TypeScript 原生的一个类型接口,`T`是它的类型参数。声明数组时,需要提供`T`的值。
```typescript
let arr:Array<number> = [1, 2, 3];
```
上面的示例中,`Array<number>`就是一个泛型,类型参数的值是`number`,表示该数组的全部成员都是数值。
同样的,如果数组成员都是字符串,那么类型就写成`Array<string>`。事实上,在 TypeScript 内部,数组类型的另一种写法`number[]``string[]`,只是`Array<number>``Array<string>`的简写形式。
在 TypeScript 内部,`Array`是一个泛型接口,类型定义基本是下面的样子。
```typescript
interface Array<Type> {
length:number;
pop():Type | undefined;
push(...items:Type[]): number;
// ...
}
```
上面代码中,`push()`方法的参数`item`的类型是`Type[]`,跟`Array()`的参数类型`Type`保持一致,表示只能添加同类型的成员。调用`push()`的时候TypeScript 就会检查两者是否一致。
其他的 TypeScript 内部数据结构,比如`Map``Set``Promise`,其实也是泛型接口,完整的写法是`Map<K, V>``Set<T>``Promise<T>`
TypeScript 默认还提供一个`ReadonlyArray<T>`接口,表示只读数组。
```typescript
function doStuff(
values: ReadonlyArray<string>
) {
values.push('hello!'); // 报错
}
```
上面示例中,参数`values`的类型是`ReadonlyArray<string>`,表示不能修改这个数组,所以函数体内部新增数组成员就会报错。因此,如果不希望函数内部改动参数数组,就可以将该参数数组声明为`ReadonlyArray`类型。
## 类型参数的约束条件
很多类型参数并不是无限制的,对于传入的类型存在约束条件。
```typescript
function comp<Type>(a:Type, b:Type) {
if (a.length >= b.length) {
return a;
}
return b;
}
```
上面示例中,类型参数 Type 有一个隐藏的约束条件Type 必须是对象,且存在`length`属性。如果不满足这个条件,就会报错。
TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行了说明。
```typescript
function comp<T extends { length: number }>(
a:T, b:T
) {
if (a.length >= b.length) {
return a;
}
return b;
}
```
上面示例中,`T extends { length: number }`就是约束条件,表示类型参数 T 必须满足`{ length: number }`,否则就会报错。
```typescript
comp([1, 2], [1, 2, 3]) // 正确
comp('ab', 'abc') // 正确
comp(1, 2) // 报错
```
上面示例中,只要传入的参数类型不满足约束条件,就会报错。
类型参数的约束条件采用下面的形式。
```typescript
<TypeParameter extends ConstraintType>
```
上面语法中,`TypeParameter`表示类型参数,`extends`是关键字,这是必须的,`ConstraintType`表示类型参数要满足的条件,即类型参数应该是`ConstraintType`的子类型。
类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。
```typescript
type Fn<A extends string, B extends string = 'world'>
= [A, B];
type Result = Fn<'hello'> // ["hello", "world"]
```
上面示例中,类型参数`A``B`都有约束条件,并且`B`还有默认值。所以,调用`Fn`的时候,可以只给出`A`的值,不给出`B`的值。
另外,上例也可以看出,泛型本质上是一个类型函数,通过输入参数,获得结果,两者是一一对应关系。
如果有多个类型参数,一个类型参数的约束条件,可以引用其他参数。
```typescript
<T, U extends T>
// 或者
<T extends U, U>
```
上面示例中,`U`的约束条件引用`T`,或者`T`的约束条件引用`U`,都是正确的。
但是,约束条件不能引用类型参数自身。
```typescript
<T extends T> // 报错
<T extends U, U extends T> // 报错
```
上面示例中,`T`的约束条件不能是`T`自身,因此多个类型参数也不能互相约束(即`T`的约束条件是`U``U`的约束条件是`T`),因为互相约束就意味着约束条件就是类型参数自身。
## 使用注意点
泛型有一些使用注意点。
**1尽量少用泛型。**
泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。
**2类型参数越少越好。**
多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好。
```typescript
function filter<
T,
Fn extends (arg:T) => boolean
>(
arr:T[],
func:Fn
):T[] {
return arr.filter(func);
}
```
上面示例有两个类型参数,但是第二个类型参数`Fn`是不必要的,完全可以直接写在函数参数的类型声明里面。
```typescript
function filter<T>(
arr:T[],
func:(arg:T) => boolean
):T[] {
return arr.filter(func);
}
```
上面示例中,类型参数简化成了一个,效果与前一个示例是一样的。
**3类型参数需要出现两次。**
如果类型参数只出现一次,那么很可能是不必要的。
```typescript
function greet<Str extends string>(
s:Str
) {
console.log('Hello, ' + s);
}
```
上面示例中,类型参数`Str`只在函数声明中出现一次(除了它的定义部分),这往往表明这个类型参数是不必要。
```typescript
function greet(s:string) {
console.log('Hello, ' + s);
}
```
上面示例把前面的类型参数省略了,效果与前一个示例是一样的。
也就是说,只有当类型参数用到两次或两次以上,才是泛型的适用场合。
**4泛型可以嵌套。**
类型参数可以是另一个类型参数。
```typescript
type OrNull<Type> = Type|null;
type OneOrMany<Type> = Type|Type[];
type OneOrManyOrNull<Type> = OrNull<OneOrMany<Type>>;
```
上面示例中,最后一行的泛型`OrNull`的类型参数,就是另一个泛型`OneOrMany`

30
docs/global.d.ts.md Normal file
View File

@@ -0,0 +1,30 @@
# global.d.ts
`global.d.ts`文件用来放置全局的 interfaces 和类型。一旦设置,该项目的所有文件,都可以读取这些类型。
在这个文件里面可以全局申明模块。
```typescript
// global.d.ts
declare module 'foo' {
// Some variable declarations
export var bar: number; /*sample*/
}
```
然后可以用模块名输入。
```typescript
// anyOtherTsFileInYourProject.ts
import * as foo from 'foo';
// TypeScript assumes (without doing any lookup) that
// foo is {bar:number}
```
该文件的另一个用途是声明一些编译时常量。
```typescript
declare const BUILD_MODE_PRODUCTION: boolean; // can be used for conditional compiling
declare const BUILD_VERSION: string;
```

695
docs/interface.md Normal file
View File

@@ -0,0 +1,695 @@
# TypeScript 的 interface 接口
## 简介
interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了这个模板的对象,就拥有了指定的类型结构。
```typescript
interface Person {
firstName: string;
lastName: string;
age: number;
}
```
上面示例中,定义了一个接口`Person`,它指定一个对象模板,拥有三个属性`firstName``lastName``age`。任何实现这个接口的对象,都必须部署这三个属性,并且必须符合规定的类型。
实现该接口很简单,只要指定它作为对象的类型即可。
```typescript
const p:Person = {
firstName: 'John',
lastName: 'Smith',
age: 25
};
```
上面示例中,变量`p`的类型就是接口`Person`,所以必须符合`Person`指定的结构。
方括号运算符可以取出 interface 某个属性的类型。
```typescript
interface Foo {
a: string;
}
type A = Foo['a']; // string
```
上面示例中,`Foo['a']`返回属性`a`的类型,所以类型`A`就是`string`
interface 可以表示对象的各种语法它的成员有5种形式。
- 对象属性
- 对象的属性索引
- 对象方法
- 函数
- 构造函数
1对象属性
```typescript
interface Point {
x: number;
y: number;
}
```
上面示例中,`x``y`都是对象的属性,分别使用冒号指定每个属性的类型。
属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略。
如果属性是可选的,就在属性名后面加一个问号。
```typescript
interface Foo {
x?: string;
}
```
如果属性是只读的,需要加上`readonly`修饰符。
```typescript
interface A {
readonly a: string;
}
```
2对象的属性索引
```typescript
interface A {
[prop: string]: number;
}
```
上面示例中,`[prop: string]`就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。
属性索引共有`string``number``symbol`三种类型。
一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。
```typescript
interface MyObj {
[prop: string]: number;
a: boolean; // 编译错误
}
```
上面示例中,属性索引指定所有名称为字符串的属性,它们的属性值必须是数值(`number`)。属性`a`的值为布尔值就报错了。
属性的数值索引,其实是指定数组的类型。
```typescript
interface A {
[prop: number]: string;
}
const obj:A = ['a', 'b', 'c'];
```
上面示例中,`[prop: number]`表示属性名的类型是数值,所以可以用数组对变量`obj`赋值。
同样的,一个接口中最多只能定义一个数值索引。数值索引会约束所有名称为数值的属性。
如果一个 interface 同时定义了字符串索引和数值索引,那么数值索性必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名。
```typescript
interface A {
[prop: string]: number;
[prop: number]: string; // 报错
}
interface B {
[prop: string]: number;
[prop: number]: number; // 正确
}
```
上面示例中,数值索引的属性值类型与字符串索引不一致,就会报错。数值索引必须兼容字符串索引的类型声明。
3对象的方法
对象的方法共有三种写法。
```typescript
// 写法一
interface A {
f(x: boolean): string;
}
// 写法二
interface B {
f: (x: boolean) => string;
}
// 写法三
interface C {
f: { (x: boolean): string };
}
```
属性名可以采用表达式,所以下面的写法也是可以的。
```typescript
const f = 'f';
interface A {
[f](x: boolean): string;
}
```
类型方法可以重载。
```typescript
interface A {
f(): number;
f(x: boolean): boolean;
f(x: string, y: string): string;
}
```
interface 里面的函数重载,不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现。
```typescript
interface A {
f(): number;
f(x: boolean): boolean;
f(x: string, y: string): string;
}
function MyFunc(): number;
function MyFunc(x: boolean): boolean;
function MyFunc(x: string, y: string): string;
function MyFunc(
x?:boolean|string, y?:string
):number|boolean|string {
if (x === undefined && y === undefined) return 1;
if (typeof x === 'boolean' && y === undefined) return true;
if (typeof x === 'string' && typeof y === 'string') return 'hello';
throw new Error('wrong parameters');
}
const a:A = {
f: MyFunc
}
```
上面示例中,接口`A`的方法`f()`有函数重载,需要额外定义一个函数`MyFunc()`实现这个重载,然后部署接口`A`的对象`a`的属性`f`等于函数`MyFunc()`就可以了。
4函数
interface 也可以用来声明独立的函数。
```typescript
interface Add {
(x:number, y:number): number;
}
const myAdd:Add = (x,y) => x + y;
```
上面示例中,接口`Add`声明了一个函数类型。
5构造函数
interface 内部可以使用`new`关键字,表示构造函数。
```typescript
interface ErrorConstructor {
new (message?: string): Error;
}
```
上面示例中,接口`ErrorConstructor`内部有`new`命令,表示它是一个构造函数。
TypeScript 里面,构造函数特指具有`constructor`属性的类详见《Class》一章。
## interface 的继承
interface 可以继承其他类型,主要有下面几种情况。
### interface 继承 interface
interface 可以使用`extends`关键字,继承其他 interface。
```typescript
interface Shape {
name: string;
}
interface Circle extends Shape {
radius: number;
}
```
上面示例中,`Circle`继承了`Shape`,所以`Circle`其实有两个属性`name``radius`。这时,`Circle`是子接口,`Shape`是父接口。
`extends`关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。
interface 允许多重继承。
```typescript
interface Style {
color: string;
}
interface Shape {
name: string;
}
interface Circle extends Style, Shape {
radius: number;
}
```
上面示例中,`Circle`同时继承了`Style``Shape`,所以拥有三个属性`color``name``radius`
多重接口继承,实际上相当于多个父接口的合并。
如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。
```typescript
interface Foo {
id: string;
}
interface Bar extends Foo {
id: number; // 报错
}
```
上面示例中,`Bar`继承了`Foo`,但是两者的同名属性`id`的类型不兼容,导致报错。
多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。
```typescript
interface Foo {
id: string;
}
interface Bar {
id: number;
}
// 报错
interface Baz extends Foo, Bar {
type: string;
}
```
上面示例中,`Baz`同时继承了`Foo``Bar`,但是后两者的同名属性`id`有类型冲突,导致报错。
### interface 继承 type
interface 可以继承`type`命令定义的对象类型。
```typescript
type Country = {
name: string;
capital: string;
}
interface CountryWithPop extends Country {
population: number;
}
```
上面示例中,`CountryWithPop`继承了`type`命令定义的`Country`对象,并且新增了一个`population`属性。
注意,如果`type`命令定义的类型不是对象interface 就无法继承。
### interface 继承 class
inteface 还可以继承 class即继承该类的所有成员。关于 class 的详细解释,参见下一章。
```typescript
class A {
x:string = '';
y():boolean {
return true;
}
}
interface B extends A {
z: number
}
```
上面示例中,`B`继承了`A`,因此`B`就具有属性`x``y()``z`
实现`B`接口的对象就需要实现这些属性。
```typescript
const b:B = {
x: '',
y: function(){ return true },
z: 123
}
```
上面示例中,对象`b`就实现了接口`B`,而接口`B`又继承了类`A`
某些类拥有私有成员和保护成员interface 可以继承这样的类,但是意义不大。
```typescript
class A {
private x: string = '';
protected y: string = '';
}
interface B extends A {
z: number
}
// 报错
const b:B = { /* ... */ }
// 报错
class C implements B {
// ...
}
```
上面示例中,`A`有私有成员和保护成员,`B`继承了`A`,但无法用于对象,因为对象不能实现这些成员。这导致`B`只能用于其他 class而这时其他 class 与`A`之间不构成父类和子类的关系,使得`x``y`无法部署。
## 接口合并
多个同名接口会合并成一个接口。
```typescript
interface Box {
height: number;
width: number;
}
interface Box {
length: number;
}
```
上面示例中,两个`Box`接口会合并成一个接口,同时有`height``width``length`三个属性。
这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便。
举例来说Web 网页开发经常会对`windows`对象和`document`对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface合并进原始定义。
```typescript
interface Document {
foo: string;
}
document.foo = 'hello';
```
上面示例中,接口`Document`增加了一个自定义属性`foo`,从而就可以在`document`对象上使用自定义属性。
同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。
```typescript
interface A {
a: number;
}
interface A {
a: string; // 报错
}
```
上面示例中,接口`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;
}
// 等同于
interface Cloner {
clone(animal: Dog): Dog;
clone(animal: Cat): Cat;
clone(animal: Sheep): Sheep;
clone(animal: Animal): Animal;
}
```
上面示例中,`clone()`方法有不同的类型声明,会发生函数重载。这时,越靠后的定义,优先级越高,排在函数重载的越前面。比如,`clone(animal: Animal)`是最先出现的类型声明,就排在函数重载的最后,属于`clone()`函数最后匹配的类型。
这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。
```typescript
interface A {
f(x:'foo'): boolean;
}
interface A {
f(x:any): void;
}
// 等同于
interface A {
f(x:'foo'): boolean;
f(x:any): void;
}
```
上面示例中,`f()`方法有一个类型声明是,参数`x`是字面量类型,这个类型声明的优先级最高,会排在函数重载的最前面。
一个实际的例子是 Document 对象的`createElement()`方法,它会根据参数的不同,而生成不同的 HTML 节点对象。
```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;
}
// 等同于
interface Document {
createElement(tagName: "canvas"): HTMLCanvasElement;
createElement(tagName: "div"): HTMLDivElement;
createElement(tagName: "span"): HTMLSpanElement;
createElement(tagName: string): HTMLElement;
createElement(tagName: any): Element;
}
```
上面示例中,`createElement()`方法的函数重载,参数为字面量的类型声明会排到最前面,返回具体的 HTML 节点对象。类型越不具体的参数,排在越后面,返回通用的 HTML 节点对象。
如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型。
```typescript
interface Circle {
area: bigint;
}
interface Rectangle {
area: number;
}
declare const s: Circle | Rectangle;
s.area; // bigint | number
```
上面示例中,接口`Circle``Rectangle`组成一个联合类型`Circle | Rectangle`。因此,这个联合类型的同名属性`area`,也是一个联合类型。本例中的`declare`命令表示变量`s`的具体定义由其他脚本文件给出详见《d.ts 文件》一章。
## interface 与 type 的异同
`interface`命令与`type`命令作用类似,都可以表示对象类型。
很多对象类型即可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。
它们的相似之处,首先表示在都能为对象类型起名。
```typescript
type Country = {
name: string;
capital: string;
}
interface Coutry {
name: string;
capital: string;
}
```
上面示例是`type`命令和`interface`命令,分别定义同一个类型。
`class`命令也有类似作用,通过定义一个类,同时定义一个对象类型。但是,它会创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用`type``interface`
interface 与 type 的区别有下面几点。
1`type`能够表示非对象类型,而`interface`只能表示对象类型(包括数组、函数等)。
2`interface`可以继承其他类型,`type`不支持继承。
继承的主要作用是添加属性,`type`定义的对象类型如果想要添加属性,只能使用`&`运算符,重新定义一个类型。
```typescript
type Animal = {
name: string
}
type Bear = Animal & {
honey: boolean
}
```
上面示例中,类型`Bear``Animal`的基础上添加了一个属性`honey`
上例的`&`运算符,表示同时具备两个类型的特征,因此可以起到两个对象类型合并的作用。
作为比较,`interface`添加属性,采用的是继承的写法。
```typescript
interface Animal {
name: string
}
interface Bear extends Animal {
honey: boolean
}
```
继承时type 和 interface 是可以换用的。interface 可以继承 type。
```typescript
type Foo = { x: number; };
interface Bar extends Foo {
y: number;
}
```
type 也可以继承 interface。
```typescript
interface Foo {
x: number;
}
type Bar = Foo & { y: number; };
```
3同名`interface`会自动合并,同名`type`则会报错。也就是说TypeScript 不允许使用`type`多次定义同一个类型。
```typescript
type A = { foo:number }; // 报错
type A = { bar:number }; // 报错
```
上面示例中,`type`两次定义了类型`A`,导致两行都会报错。
作为比较,`interface`则会自动合并。
```typescript
interface A { foo:number };
interface A { bar:number };
const obj:A = {
foo: 1,
bar: 1
};
```
上面示例中,`interface`把类型`A`的两个定义合并在一起。
这表明inteface 是开放的可以添加属性type 是封闭的,不能添加属性,只能定义新的 type。
4`interface`不能包含属性映射mapping`type`可以,详见《映射》一章。
```typescript
interface Point {
x: number;
y: number;
}
// 正确
type PointCopy1 = {
[Key in keyof Point]: Point[Key];
};
// 报错
interface PointCopy2 {
[Key in keyof Point]: Point[Key];
};
```
5`this`关键字只能用于`interface`
```typescript
// 正确
interface Foo {
add(num:number): this;
};
// 报错
type Foo = {
add(num:number): this;
};
```
上面示例中type 命令声明的方法`add()`,返回`this`就报错了。interface 命令没有这个问题。
下面是返回`this`的实际对象的例子。
```typescript
class Calculator implements Foo {
result = 0;
add(numnumber) {
this.result += num;
return this;
}
}
```
6type 可以扩展原始数据类型interface 不行。
```typescript
// 正确
type MyStr = string & {
type: 'new'
};
// 报错
interface MyStr extends string {
type: 'new'
}
```
上面示例中type 可以扩展原始数据类型 stringinterface 就不行。
7`interface`无法表达某些复杂类型(比如交叉类型和联合类型),但是`type`可以。
```typescript
type A = { /* ... */ };
type B = { /* ... */ };
type AorB = A | B;
type AorBwithName = AorB & {
name: string
};
```
上面示例中,类型`AorB`是一个联合类型,`AorBwithName`则是为`AorB`添加一个属性。这两种运算,`interface`都没法表达。
综上所述,如果有复杂的类型运算,那么没有选择只有使用`type`;一般情况下,`interface`灵活性比较高,便于扩充类型或自动合并,建议优先使用。

201
docs/intro.md Normal file
View File

@@ -0,0 +1,201 @@
# TypeScript 语言简介
## 概述
TypeScript简称 TS是微软公司开发的一种基于 JavaScript (简称 JS语言的编程语言。
它的目的并不是创造一种全新语言,而是增强 JavaScript 的功能,使其更适合多人合作的企业级项目。
TypeScript 可以看成是 JavaScript 的超集superset即它继承了后者的全部语法所有 JavaScript 脚本都可以当作 TypeScript 脚本(但是可能会报错),此外它再增加了一些自己的语法。
TypeScript 对 JavaScript 添加的最主要部分,就是一个独立的类型系统。
## 类型的概念
类型type指的是一组具有相同特征的值。如果两个值具有某种共同的特征就可以说它们属于同一种类型。
举例来说,`123``456`这两个值共同特征是都能进行数值运算所以都属于“数值”number这个类型。
一旦确定某个值的类型,就意味着,这个值具有该类型的所有特征,可以进行该类型的所有运算。凡是适用该类型的地方,都可以使用这个值;凡是不适用该类型的地方,使用这个值都会报错。
可以这样理解,**类型是人为添加的一种编程约束和用法提示。** 主要目的是在软件开发过程中,为编译器和开发工具提供更多的验证和帮助,帮助提高代码质量,减少错误。
下面是一段简单的 TypeScript 代码,演示一下类型系统的作用。
```typescript
function addOne(n:number) {
return n + 1;
}
```
上面示例中,函数`addOne()`有一个参数`n`类型为数值number表示这个位置只能使用数值传入其他类型的值就会报错。
```typescript
addOne('hello') // 报错
```
上面示例中,函数`addOne()`传入了一个字符串`hello`TypeScript 发现类型不对,就报错了,指出这个位置只能传入数值,不能传入字符串。
JavaScript 语言就没有这个功能,不会检查类型对不对。开发阶段很可能发现不了这个问题,代码也许就会原样发布,导致用户在使用时遇到错误。
作为比较TypeScript 是在开发阶段报错,这样有利于提早发现错误,避免使用时报错。另一方面,函数定义里面加入类型,具有提示作用,可以告诉开发者这个函数怎么用。
## 动态类型与静态类型
前面说了TypeScript 的主要功能是为 JavaScript 添加类型系统。大家可能知道JavaScript 语言本身就有一套自己的类型系统,比如数值`123`和字符串`Hello`
但是JavaScript 的类型系统非常弱而且没有使用限制运算符可以接受各种类型的值。在语法上JavaScript 属于动态类型语言。
请看下面的 JavaScript 代码。
```javascript
// 例一
let x = 1;
x = 'hello';
// 例二
let y = { foo: 1 };
delete y.foo;
y.bar = 2;
```
上面的例一,变量`x`声明时,值的类型是数值,但是后面可以改成字符串。所以,无法提前知道变量的类型是什么,也就是说,变量的类型是动态的。
上面的例二,变量`y`是一个对象,有一个属性`foo`,但是这个属性是可以删掉的,并且还可以新增其他属性。所以,对象有什么属性,这个属性还在不在,也是动态的,没法提前知道。
正是因为存在这些动态变化,所以 JavaScript 的类型系统是动态的,不具有很强的约束性。这对于提前发现代码错误,非常不利。
TypeScript 引入了一个更强大、更严格的类型系统,属于静态类型语言。
上面的代码在 TypeScript 里面都会报错。
```javascript
// 例一
let x = 1;
x = 'hello'; // 报错
// 例二
let y = { foo: 1 };
delete y.foo; // 报错
y.bar = 2; // 报错
```
上面示例中,例一的报错是因为变量一旦赋值了,就不允许再改变类型,即变量的类型是静态的。例二的报错是因为对象的属性也是静态的,不允许随意增删。
TypeScript 的作用,就是为 JavaScript 引入这种静态类型特征。
## 静态类型的优点
静态类型有很多好处,这也是 TypeScript 想要达到的目的。
1有利于代码的静态分析。
有了静态类型,不必运行代码,就可以确定变量的类型,从而推断代码有没有错误。这就叫做代码的静态分析。
这对于大型项目非常重要,单单在开发阶段运行静态检查,就可以发现很多问题,避免交付有问题的代码,大大降低了线上风险。
2有利于发现错误。
由于每个值、每个变量、每个运算符都有严格的类型约束TypeScript 就能轻松发现拼写错误、语义错误和方法调用错误,节省程序员的时间。
```typescript
let obj = { message: '' };
console.log(obj.messege); // 报错
```
上面示例中,不小心把`message`拼错了,写成`messege`。TypeScript 就会报错指出没有定义过这个属性。JavaScript 遇到这种情况是不报错的。
```typescript
const a = 0;
const b = true;
const result = a + b; // 报错
```
上面示例是合法的 JavaScript 代码,但是没有意义,不应该将数值`a`与布尔值`b`相加。TypeScript 就会直接报错,提示运算符`+`不能用于数值和布尔值的相加。
```typescript
function hello() {
return 'hello world';
}
hello().find('hello'); // 报错
```
上面示例中,`hello()`返回的是一个字符串TypeScript 发现字符串没有`find()`方法,所以报错了。如果是 JavaScript只有到运行阶段才会报错。
3更好的 IDE 支持,做到语法提示和自动补全。
IDE集成开发环境比如 VSCode一般都会利用类型信息提供语法提示功能编辑器自动提示函数用法、参数等和自动补全功能只键入一部分的变量名或函数名编辑器补全后面的部分
4提供了代码文档。
类型信息可以部分替代代码文档,解释应该如何使用这些代码,熟练的开发者往往只看类型,就能大致推断代码的作用。借助类型信息,很多工具能够直接生成文档。
5有助于代码重构。
修改他人的 JavaScript 代码,往往非常痛苦,项目越大越痛苦,因为不确定修改后是否会影响到其他部分的代码。
类型信息大大减轻了重构的成本。一般来说,只要函数或对象的参数和返回值保持类型不变,就能基本确定,重构后的代码也能正常运行。如果还有配套的单元测试,就完全可以放心重构。越是大型的、多人合作的项目,类型信息能够提供的帮助越大。
综上所述TypeScript 有助于提高代码质量,保证代码安全,更适合用在大型的企业级项目。这就是为什么大量 JavaScript 项目转成 TypeScript 的原因。
## 静态类型的缺点
静态类型也存在一些缺点。
1丧失了动态类型的代码灵活性。
动态类型有非常高的灵活性,给予程序员很大的自由,静态类型将这些灵活性都剥夺了。
2增加了编程工作量。
有了类型之后,程序员不仅需要编写功能,还需要编写类型声明,确保类型正确。这增加了不少工作量,有时会显著拖长项目的开发时间。
3更高的学习成本。
类型系统通常比较复杂,要学习的东西更多,要求开发者付出更高的学习成本。
4引入了独立的编译步骤。
原生的 JavaScript 代码,可以直接在 JavaScript 引擎运行。添加类型系统以后,就多出了一个单独的编译步骤,检查类型是否正确,并将 TypeScript 代码转成 JavaScript 代码,这样才能运行。
5兼容性问题。
TypeScript 依赖 JavaScript 生态,需要用到很多外部模块。但是,过去大部分 JavaScript 项目都没有做 TypeScript 适配,虽然可以自己动手做适配,不过使用时难免还是会有一些兼容性问题。
总的来说,这些缺点使得 TypeScript 不一定适合那些小型的、短期的个人项目。
## TypeScript 的历史
下面简要介绍 TypeScript 的发展历史。
2012年微软公司宣布推出 TypeScript 语言,设计者是著名的编程语言设计大师 Anders Hejlsberg他也是 C# 和 .Net 的设计师。
微软推出这门语言的主要目的,是让 JavaScript 程序员可以参与 Windows 8 应用程序的开发。
当时Windows 8 即将发布,它的应用程序开发除了使用 C# 和 Visual Basic还可以使用 HTML + JavaScript。微软希望TypeScript 既能让 JavaScript 程序员快速上手,也能让 .Net 程序员感到熟悉。
这就是说TypeScript 的最初动机是减少 .Net 程序员的转移和学习成本。所以,它的很多语法概念跟 .Net 很类似。
另外TypeScript 是一个开源项目,接受社区的参与,核心的编译器采用 Apache 2.0 许可证。微软希望通过这种做法,迅速提高这门语言在社区的接受度。
2013年微软的 Visual Studio 2013 开始内置支持 TypeScript 语言。
2014年TypeScript 1.0 版本发布。同年,代码仓库搬到了 GitHub。
2016年TypeScript 2.0 版本发布,引入了很多重大的语法功能。
2018年TypeScript 3.0 版本发布。
2020年TypeScript 4.0 版本发布。
2023年TypeScript 5.0 版本发布。
## 如何学习
学习 TypeScript必须先了解 JavaScript 的语法。因为真正的实际功能都是 JavaScript 引擎完成的TypeScript 只是添加了一个类型系统。
本书假定读者已经了解 JavaScript 语言,就不再介绍它的语法了,只介绍 TypeScript 引入的新语法,主要是类型系统。
如果你对 JavaScript 还不熟悉,建议先阅读[《JavaScript 教程》](https://wangdoc.com/javascript)和[《ES6 教程》](https://wangdoc.com/es6),再来阅读本书。

359
docs/mapping.md Normal file
View File

@@ -0,0 +1,359 @@
# TypeScript 的类型映射
## 简介
映射mapping指的是将一种类型按照映射规则转换成另一种类型通常用于对象类型。
举例来说,现有一个类型`A`和另一个类型`B`
```typescript
type A = {
foo: number;
bar: number;
};
type B = {
foo: string;
bar: string;
};
```
上面示例中,这两个类型的属性结构是一样的,但是属性的类型不一样。如果属性数量多的话,写起来就很麻烦。
使用类型映射,就可以从类型`A`得到类型`B`
```typescript
type A = {
foo: number;
bar: number;
};
type B = {
[prop in keyof A]: string;
};
```
上面示例中,类型`B`采用了属性名索引的写法,`[prop in keyof A]`表示依次得到类型`A`的所有属性名,然后将每个属性的类型改成`string`
在语法上,`[prop in keyof A]`是一个属性名表达式,表示这里的属性名需要计算得到。具体的计算规则如下:
- `prop`:属性名变量,名字可以随便起。
- `in`:运算符,用来取出右侧的联合类型的每一个成员。
- `Keyof A`:返回类型`A`的每一个属性名,组成一个联合类型。
下面是复制原始类型的例子。
```typescript
type A = {
foo: number;
bar: string;
};
type B = {
[prop in keyof A]: A[prop];
};
```
上面示例中,类型`B`原样复制了类型`A`
为了增加代码复用性,可以把常用的映射写成泛型。
```typescript
type ToBoolean<Type> = {
[Property in keyof Type]: boolean;
};
```
上面示例中,定义了一个泛型,可以将其他对象的所有属性值都改成 boolean 类型。
由于对象的属性名,只有 string、number、symbol 三种可能,所以 keyof 运算符返回的联合类型,应该是`string | number | symbol`的子类型。
下面是另一个例子。
```typescript
type MyObj = {
[P in 0|1|2]: string;
};
// 等同于
type MyObj = {
0: string;
1: string;
2: string;
};
```
上面示例中,联合类型`0|1|2`映射成了三个属性名。
不使用联合类型,直接使用某种具体类型进行属性名映射,也是可以的。
```typescript
type MyObj = {
[p in 'foo']: number;
};
// 等同于
type MyObj = {
foo: number;
};
```
上面示例中,`p in 'foo'`可以看成只有一个成员的联合类型,因此得到了只有这一个属性的对象类型。
甚至还可以写成`p in string`
```typescript
type MyObj = {
[p in string]: boolean;
};
// 等同于
type MyObj = {
[p: string]: boolean;
};
```
上面示例中,`[p in string]`就是属性名索引形式`[p: string]`的映射写法。
通过映射,可以某个对象的所有属性改成可选属性。
```typescript
type A = {
a: string;
b: number;
};
type B = {
[Prop in keyof A]?: A[Prop];
};
```
上面示例中,类型`B`在类型`A`的所有属性名后面添加问号,使得这些属性都变成了可选属性。
事实上TypeScript 的内置工具类型`Partial<T>`,就是这样实现的。
TypeScript内置的工具类型`Readonly<T>`可以将所有属性改为只读属性,实现也是通过映射。
```typescript
// 将 T 的所有属性改为只读属性
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
```
它的用法如下。
```typescript
type T = { a: string; b: number };
// {
// readonly a: string;
// readonly b: number;
// }
type ReadonlyT = Readonly<T>;
```
## 映射修饰符
映射会原样复制原始对象的可选属性和只读属性。
```typescript
type A = {
a?: string;
readonly b: number;
};
type B = {
[Prop in keyof A]: A[Prop];
};
// 等同于
type B = {
a?: string;
readonly b: number;
};
```
上面示例中,类型`B`是类型`A`的映射,把`A`的可选属性和只读属性都保留下来。
如果要删改可选和只读这两个特性并不是很方便。为了解决这个问题TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的`?`修饰符和`readonly`修饰符。
- `+`修饰符:写成`+``+readonly`,为映射属性添加`?`修饰符或`readonly`修饰符。
- ``修饰符:写成`-?``-readonly`,为映射属性移除`?`修饰符或`readonly`修饰符。
下面是添加或移除可选属性的例子。
```typescript
// 添加可选属性
type Optional<Type> = {
[Prop in keyof Type]+?: Type[Prop];
};
// 移除可选属性
type Concrete<Type> = {
[Prop in keyof Type]-?: Type[Prop];
};
```
注意,`+?``-?`要写在属性名的后面。
下面是添加或移除只读属性的例子。
```typescript
// 添加 readonly
type CreateImmutable<Type> = {
+readonly [Prop in keyof Type]: Type[Prop];
};
// 移除 readonly
type CreateMutable<Type> = {
-readonly [Prop in keyof Type]: Type[Prop];
};
```
注意,`+readonly``-readonly`要写在属性名的前面。
如果同时增删`?``readonly`这两个修饰符,写成下面这样。
```typescript
// 增加
type MyObj<T> = {
+readonly [P in keyof T]+?: T[P];
};
// 移除
type MyObj<T> = {
-readonly [P in keyof T]-?: T[P];
}
```
TypeScript 原生的工具类型`Required<T>`专门移除可选属性,就是使用`-?`修饰符实现的。
注意,`?`修饰符移除了可选属性以后,该属性就不能等于`undefined`了,实际变成必选属性了。但是,这个修饰符不会移除`null`类型。
另外,`+?`修饰符可以简写成`?``+readonly`修饰符可以简写成`readonly`
```typescript
type A<T> = {
+readonly [P in keyof T]+?: T[P];
};
// 等同于
type B<T> = {
readonly [P in keyof T]?: T[P];
};
```
## 键名重映射
### 语法
TypeScript 4.1 引入了键名重映射key remapping允许将键名指定为其他类型。
```typescript
type A = {
foo: number;
bar: number;
};
type B = {
[p in keyof A as `${p}ID`]: number;
};
// 等同于
type B = {
fooID: number;
barID: number;
}
```
上面示例中,类型`B`是类型`A`的映射,但在映射时把属性名改掉了,在原始属性名后面加上了字符串`ID`
可以看到,键名重映射的语法是在键名映射的后面加上`as + 新类型`子句。这里的“新类型”通常是一个模板字符串,里面可以对原始键名进行各种操作。
下面是另一个例子。
```typescript
interface Person {
name: string;
age: number;
location: string;
}
type Getters<T> = {
[P in keyof T
as `get${Capitalize<string & P>}`]: () => T[P];
};
type LazyPerson = Getters<Person>;
// 等同于
type LazyPerson = {
getName: () => string;
getAge: () => number;
getLocation: () => string;
}
```
上面示例中,类型`LazyPerson`是类型`Person`的映射,并且把键名改掉了。
它的修改键名的代码是一个模板字符串`get${Capitalize<string & P>}`,下面是各个部分的解释。
- `get`:为键名添加的前缀。
- `Capitalize<T>`:一个原生的工具泛型,用来将`T`的首字母变成大写。
- `string & P`:一个交叉类型,其中的`P`是 keyof 运算符返回的键名联合类型`string|number|symbol`,但是`Capitalize<T>`只能接受字符串作为类型参数,因此`string & P`只返回`P`的字符串属性名。
### 属性过滤
键名重映射还可以过滤掉某些属性。下面的例子是只保留字符串属性。
```typescript
type User = {
name: string,
age: number
}
type Filter<T> = {
[K in keyof T
as T[K] extends string ? K : never]: string
}
type FilteredUser = Filter<User> // { name: string }
```
上面示例中,映射`K in keyof T`获取类型`T`的每一个属性以后,然后使用`as Type`修改键名的类型。
它的键盘重映射`as T[K] extends string ? K : never]`,使用了条件运算符。如果属性值`T[K]`的类型是字符串,那么属性名不变,否则属性名类型改为`never`,即这个属性名不存在。这样就等于过滤了不符合条件的属性,只保留属性值为字符串的属性。
### 联合类型的映射
由于键名重映射可以修改键名类型,所以原始键名的类型不必是`string|number|symbol`,任意的联合类型都可以。
```typescript
type S = {
kind: 'square',
x: number,
y: number,
};
type C = {
kind: 'circle',
radius: number,
};
type MyEvents<Eevnts extends { kind: string }> = {
[E in Events as E['kind']]: (event: E) => void;
}
type Config = MyEvent<S|C>;
// 等同于
type Config = {
square: (event:S) => void;
circle: (event:C) => void;
}
```
上面示例中,原始键名的映射是`E in Events`,这里的`Events`是两个对象组成的联合类型`S|C`。所以,`E`是一个对象,然后再通过键名重映射,得到字符串键名`E['kind']`
## 参考链接
- [Mapped Type Modifiers in TypeScript](https://mariusschulz.com/blog/mapped-type-modifiers-in-typescript), Marius Schulz

375
docs/module.md Normal file
View File

@@ -0,0 +1,375 @@
# TypeScript 模块
## 简介
任何包含 import 或 export 语句的文件就是一个模块module。相应地如果文件不包含 export 语句,就是一个全局的脚本文件。
模块本身就是一个作用域,不属于全局作用域。模块内部的变量、函数、类只在内部可见,对于模块外部是不可见的。暴露给外部的接口,必须用 export 命令声明;如果其他文件要使用模块的接口,必须用 import 命令来输入。
如果一个文件不包含 export 语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在文件中添加一行语句。
```typescript
export {};
```
上面这行语句不产生任何实际作用,但会让当前文件被当作模块处理,所有它的代码都变成了内部代码。
ES 模块的详细介绍,请参考 ES6 教程,这里就不重复了。本章主要介绍 TypeScript 的模块处理。
TypeScript 模块除了支持所有 ES 模块的语法,特别之处在于允许输出和输入类型。
```typescript
export type Bool = true | false;
```
上面示例中,当前脚本输出一个类型别名`Bool`。这行语句把类型定义和接口输出写在一行,也可以写成两行。
```typescript
type Bool = true | false;
export { Bool };
```
假定上面的模块文件为`a.ts`,另一个文件`b.ts`就可以使用 import 语句,输入这个类型。
```typescript
import { Bool } from './a.js';
let foo:Bool = true;
```
上面示例中import 语句加载的是一个类型。注意,它是从文件`a.js`加载,而不是从`a.ts`加载,因为在代码运行环境是 JS 环境,所以要写成从 JS 文件加载,否则报错。
TypeScript 允许加载模块时,省略模块文件的后缀名,它会自动定位。
```typescript
import { Bool } from './a';
```
上面示例中,模块名写成`./a`TypeScript 会自动定位到`./a.ts`
编译时,可以两个脚本同时编译。
```bash
$ tsc a.ts b.ts
```
上面命令会将`a.ts``b.ts`分别编译成`a.js``b.js`
也可以只编译`b.ts`因为它是入口脚本tsc 会自动编译它依赖的所有脚本。
```bash
$ tsc b.ts
```
上面命令发现`b.ts`依赖`a.js`,就会自动寻找`a.ts`,也将其同时编译,因此编译产物还是`a.js``b.js`两个文件。
如果想将`a.ts``b.ts`编译成一个文件,可以使用`--outFile`参数。
```typescript
$ tsc --outFile result.js b.ts
```
上面示例将`a.ts``b.ts`合并编译为`result.js`
## import type 语句
import 在一条语句中,可以同时输入类型和正常接口。
```typescript
// a.ts
export interface A {
foo: string;
}
export let a = 123;
// b.ts
import { A, a } from './a.js';
```
上面示例中,文件`a.ts`的 export 语句输出了一个类型`A`和一个正常接口`a`,另一个文件`b.ts`则在同一条语句中输入了类型和正常接口。
这样很不利于区分类型和正常接口容易造成混淆。为了解决这个问题TypeScript 引入了两个解决方法。
第一个方法是在 import 语句输入的类型前面加上`type`关键字。
```typescript
import { type A, a } from './a.js';
```
上面示例中import 语句输入的类型`A`前面有`type`关键字,表示这是一个类型。
第二个方法是使用 import type 语句,这个语句只能输入类型,不能输入正常接口。
```typescript
// 正确
import type { A } from './a.js';
// 报错
import type { a } from './a.js';
```
上面示例中import type 输入类型`A`是正确的,但是输入正常接口`a`就会报错。
import type 语句也可以输入默认类型。
```typescript
import type DefaultType from 'moduleA';
```
import type 在一个名称空间下,输入所有类型的写法如下。
```typescript
import type * as TypeNS from 'moduleA';
```
同样的export 语句也有两种方法,表示输出的是类型。
```typescript
type A = 'a';
type B = 'b';
// 方法一
export {type A, type B};
// 方法二
export type {A, B};
```
上面示例中,方法一是使用`type`关键字作为前缀,表示输出的是类型;方法二是使用 export type 语句,表示整行输出的都是类型。
下面是 export type 将一个类作为类型输出的例子。
```typescript
class Point {
x: number;
y: number;
}
export type { Point };
```
上面示例中,由于使用了 export type 语句,输出的并不是 Point 这个类,而是 Point 代表的实例类型。输入时,只能作为类型输入。
```typescript
import type { Point } from './module.js';
const p:Point = { x: 0, y: 0 };
```
上面示例中,`Point`只能作为类型输入,不能当作正常接口使用。
## importsNotUsedAsValues
输入类型的 import 语句,编译时怎么处理?
TypeScript 提供了`importsNotUsedAsValues`编译设置项,有三个可能的值。
1`remove`:这是默认值,自动删除输入类型的 import 语句。
2`preserve`:保留输入类型的 import 语句。
3`error`:保留输入类型的 import 语句(与`preserve`相同),但是必须写成 import type 的形式,否则报错。
请看示例,下面是一个输入类型的 import 语句。
```typescript
import { TypeA } from './a.js';
```
上面示例中,`TypeA`是一个类型。
`remove`的编译结果会将该语句删掉。
`preserve`的编译结果会保留该语句,但会把删掉类型的部分。
```typescript
import './a.js';
```
上面示例中,编译后的 import 语句不从`a.js`输入任何接口,但是会引发`a.js`的执行,因此会保留`a.js`里面的副作用。
`error`的结果与`preserve`相同,但是编译过程会报错,因为输入类型的 import 语句必须写成 import type 的形式。原始语句改成下面的形式,就不会报错。
```typescript
import type { TypeA } from './a.js';
```
## CommonJS 模块
CommonJS 是 Node.js 的专用模块格式,与 ES 模块格式不兼容。
### import = 语句
TypeScript 使用 import = 语句输入 CommonJS 模块。
```typescript
import fs = require('fs');
const code = fs.readFileSync('hello.ts', 'utf8');
```
上面示例中,使用 import = 语句和`require()`命令输入了一个 CommonJS 模块。模块本身的用法跟 Node.js 是一样的。
除了使用`import =`语句TypeScript 还允许使用`import * as [接口名] from "模块文件"`输入 CommonJS 模块。
```typescript
import * as fs from 'fs';
// 等同于
import fs = require('fs');
```
### export = 语句
TypeScript 使用 export = 语句,输出 CommonJS 模块的对象,等同于 CommonJS 的`module.exports`对象。
```typescript
let obj = { foo: 123 };
export = obj;
```
export = 语句输出的对象,只能使用 import = 语句加载。
```typescript
import obj = require('./a');
console.log(obj.foo); // 123
```
## 模块定位
模块定位module resolution指的是确定 import 语句和 export 语句里面的模块文件位置。
```typescript
import { TypeA } from './a';
```
上面示例中TypeScript 怎么确定`./a`到底是指哪一个模块,这就叫做“模块定位”。
模块定位有两种方法,一种称为 Classic 方法,另一种称为 Node 方法。可以使用编译参数`moduleResolution`,指定使用哪一种方法。
没有指定定位方法时,就看原始脚本采用什么模块格式。如果模块格式是 CommonJS即编译时指定`--module commonjs`,那么模块定位采用 Node 方法,否则采用 Classic 方法(模块格式为 es2015、 esnext、amd, system, umd 等等)。
### 相对模块,非相对模块
加载模块时目标模块分为相对模块relative import和非相对模块两种non-relative import
相对模块指的是路径以`/``./``../`开头的模块。下面 import 语句加载的模块,都是相对模块。
- `import Entry from "./components/Entry";`
- `import { DefaultHeaders } from "../constants/http";`
- `import "/mod";`
非相对模块指的是不带有路径信息的模块。下面 import 语句加载的模块,都是非相对模块。
- `import * as $ from "jquery";`
- `import { Component } from "@angular/core";`
### Classic 方法
Classic 方法以当前脚本的路径作为“基准路径”,计算相对模块的位置。比如,脚本`a.ts`里面有一行代码`import { b } from "./b"`,那么 TypeScript 就会在`a.ts`所在的目录,查找`b.ts``b.d.ts`
至于非相对模块,也是以当前脚本的路径作为起点,一层层查找上级目录。比如,脚本`a.ts`里面有一行代码`import { b } from "b"`,那么就会查找`b.ts``b.d.ts`
### Node 方法
Node 方法就是模拟 Node.js 的模块加载方法。
相对模块依然是以当前脚本的路径作为“基准路径”。比如,脚本文件`a.ts`里面有一行代码`let x = require("./b");`TypeScript 按照以下顺序查找。
1. 当前目录是否包含`b.ts``b.tsx``b.d.ts`
1. 当前目录是否有子目录`b`,该子目录是否存在文件`package.json`,该文件的`types`字段是否指定了入口文件,如果是的就加载该文件。
1. 当前目录的子目录`b`是否包含`index.ts``index.tsx``index.d.ts`
非相对模块则是以当前脚本的路径作为起点,逐级向上层目录查找是否存在子目录`node_modules`。比如,脚本文件`a.js`有一行`let x = require("b");`TypeScript 按照以下顺序进行查找。
1. 当前目录的子目录`node_modules`是否包含`b.ts``b.tsx``b.d.ts`
2. 当前目录的子目录`node_modules`,是否存在文件`package.json`,该文件的`types`字段是否指定了入口文件,如果是的就加载该文件。
3. 当前目录的子目录`node_modules`里面,是否包含子目录`@types`,在该目录中查找文件`b.d.ts`
4. 当前目录的子目录`node_modules`里面,是否包含子目录`b`,在该目录中查找`index.ts``index.tsx``index.d.ts`
5. 进入上一层目录重复上面4步直到找到为止。
### 路径映射
TypeScript 允许开发者在`tsconfig.json`文件里面,手动指定模块的路径。
1baseUrl
`baseUrl`字段可以手动指定模块的基准目录。
```typescript
{
"compilerOptions": {
"baseUrl": "."
}
}
```
上面示例中,`baseUrl`是一个点,表示基准目录就是`tsconfig.json`所在的目录。
2paths
`paths`字段指定非相对模块与实际脚本的映射。
```typescript
{
"compilerOptions": {
"baseUrl": ".",
"paths": {
"jquery": ["node_modules/jquery/dist/jquery"]
}
}
}
```
上面示例中,加载模块`jquery`时,实际加载的脚本是`node_modules/jquery/dist/jquery`,它的位置要根据`baseUrl`字段计算得到。
注意,上例的`jquery`属性的值是一个数组,可以指定多个路径。如果第一个脚本路径不存在,那么就加载第二个路径,以此类推。
3rootDirs
`rootDirs`字段指定模块定位时必须查找的其他目录。
```typescript
{
"compilerOptions": {
"rootDirs": ["src/zh", "src/de", "src/#{locale}"]
}
}
```
上面示例中,`rootDirs`指定了模块定位时,需要查找的不同的国际化目录。
### tsc 的`--traceResolution`参数
由于模块定位的过程很复杂tsc 命令有一个`--traceResolution`参数,能够在编译时在命令行显示模块定位的每一步。
```bash
$ tsc --traceResolution
```
上面示例中,`traceResolution`会输出模块定位的判断过程。
### tsc 的`--noResolve`参数
tsc 命令的`--noResolve`参数,表示模块定位时,只考虑在命令行传入的模块。
举例来说,`app.ts`包含如下两行代码。
```typescript
import * as A from "moduleA";
import * as B from "moduleB";
```
使用下面的命令进行编译。
```bash
$ tsc app.ts moduleA.ts --noResolve
```
上面命令使用`--noResolve`参数,因此可以定位到`moduleA.ts`,因为它从命令行传入了;无法定位到`moduleB`,因为它没有传入,因此会报错。
## 参考链接
- [tsconfig 之 importsNotUsedAsValues 属性](https://blog.51cto.com/u_13028258/5754309)

302
docs/namespace.md Normal file
View File

@@ -0,0 +1,302 @@
# TypeScript namespace
namespace 是一种将相关代码组织在一起的方式,中文译为“命名空间”。
它出现在 ES 模块诞生之前,作为 TypeScript 自己的模块格式而发明的。但是,自从有了 ES 模块,官方已经不推荐使用 namespace 了。
## 基本用法
namespace 用来建立一个容器,内部的所有变量和函数,都必须在这个容器里面使用。
```typescript
namespace Utils {
function isString(value:any) {
return typeof value === 'string';
}
// 正确
isString('yes');
}
Utils.isString('no'); // 报错
```
上面示例中,命名空间`Utils`里面定义了一个函数`isString()`,它只能在`Utils`里面使用,如果用于外部就会报错。
如果要在命名空间以外使用内部成员,就必须为该成员加上`export`前缀,表示对外输出该成员。
```typescript
namespace Utility {
export function log(msg:string) {
console.log(msg);
}
export function error(msg:string) {
console.error(msg);
}
}
Utility.log('Call me');
Utility.error('maybe!');
```
上面示例中,只要加上`export`前缀,就可以在命名空间外部使用内部成员。
编译出来的 JavaScript 代码如下。
```typescript
var Utility;
(function (Utility) {
function log(msg) {
console.log(msg);
}
Utility.log = log;
function error(msg) {
console.error(msg);
}
Utility.error = error;
})(Utility || (Utility = {}));
```
上面代码中,命名空间`Utility`变成了 JavaScript 的一个对象,凡是`export`的内部成员,都成了该对象的属性。
这就是说namespace 会变成一个值,保留在编译后的代码中。这一点要小心,它不是纯的类型代码。
namespace 内部还可以使用`import`命令输入外部成员,相当于为外部成员起别名。当外部成员的名字比较长时,别名能够简化代码。
```typescript
namespace Utils {
export function isString(value:any) {
return typeof value === 'string';
}
}
namespace App {
import isString = Utils.isString;
isString('yes');
// 等同于
Utils.isString('yes');
}
```
上面示例中,`import`命令指定在命名空间`App`里面,外部成员`Utils.isString`的别名为`isString`
`import`命令也可以在 namespace 外部,指定别名。
```typescript
namespace Shapes {
export namespace Polygons {
export class Triangle {}
export class Square {}
}
}
import polygons = Shapes.Polygons;
// 等同于 new Shapes.Polygons.Square()
let sq = new polygons.Square();
```
上面示例中,`import`命令在命名空间`Shapes`的外部,指定` Shapes.Polygons`的别名为`polygons`
namespace 可以嵌套。
```typescript
namespace Utils {
export namespace Messaging {
export function log(msg:string) {
console.log(msg);
}
}
}
Utils.Messaging.log('hello') // "hello"
```
上面示例中,命名空间`Utils`内部还有一个命名空间`Messaging`。注意,如果要在外部使用`Messaging`,必须在它前面加上`export`命令。
使用嵌套的命名空间,必须从最外层开始引用,比如`Utils.Messaging.log()`
namespace 不仅可以包含实义代码,还可以包括类型代码。
```typescript
namespace N {
export interface MyInterface{}
export class MyClass{}
}
```
上面代码中,命令空间`N`不仅对外输出类,还对外输出一个接口,它们都可以用作类型。
namespace 与模块的作用是一致的,都是把相关代码组织在一起,对外输出接口。区别是一个文件只能有一个模块,但可以有多个 namespace。由于模块可以取代 namespace而且是 JavaScript 的标准语法,还不需要编译转换,所以建议总是使用模块,替代 namespace。
如果 namespace 代码放在一个单独的文件里,那么引入这个文件需要使用三斜杠的语法。
```typescript
/// <reference path = "SomeFileName.ts" />
```
## namespace 的输出
namespace 本身也可以使用`export`命令输出,供其他文件使用。
```typescript
// shapes.ts
export namespace Shapes {
export class Triangle {
// ...
}
export class Square {
// ...
}
}
```
上面示例是一个文件`shapes.ts`,里面使用`export`命令,输出了一个命名空间`Shapes`
其他脚本文件使用`import`命令,加载这个命名空间。
```typescript
// 写法一
import { Shapes } from './shapes';
let t = new Shapes.Triangle();
// 写法二
import * as shapes from "./shapes";
let t = new shapes.Shapes.Triangle(); // shapes.Shapes?
```
不过,更好的方法还是建议使用模块,采用模块的输出和输入。
```typescript
// shapes.ts
export class Triangle {
/* ... */
}
export class Square {
/* ... */
}
// shapeConsumer.ts
import * as shapes from "./shapes";
let t = new shapes.Triangle();
```
上面示例中,使用模块的输出和输入,改写了前面的例子。
## namespace 的合并
多个同名的 namespace 会自动合并,这一点跟 interface 一样。
```typescript
namespace Animals {
export class Cat {}
}
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Dog {}
}
// 等同于
namespace Animals {
export interface Legged {
numberOfLegs: number;
}
export class Cat {}
export class Dog {}
}
```
这样做的目的是如果同名的命名空间分布在不同的文件中TypeScript 最终会将它们合并在一起。这样就比较方便扩展别人的代码。
合并命名空间时,命名空间中的非`export`的成员不会被合并,但是它们只能在各自的命名空间中使用。
```typescript
namespace N {
const a = 0;
export function foo() {
console.log(a); // 正确
}
}
namespace N {
export function bar() {
foo(); // 正确
console.log(a); // 报错
}
}
```
上面示例中,变量`a`是第一个名称空间`N`的非对外成员,它只在第一个名称空间可用。
命名空间还可以跟同名函数合并,但是要求同名函数必须在命名空间之前声明。这样做是为了确保先创建出一个函数对象,然后同名的命名空间就相当于给这个函数对象添加额外的属性。
```typescript
function f() {
return f.version;
}
namespace f {
export const version = '1.0';
}
f() // '1.0'
f.version // '1.0'
```
上面示例中,函数`f()`与命名空间`f`合并,相当于命名空间为函数对象`f`添加属性。
命名空间也能与同名 class 合并同样要求class 必须在命名空间之前声明,原因同上。
```typescript
class C {
foo = 1;
}
namespace C {
export const bar = 2;
}
C.bar // 2
```
上面示例中,名称空间`C`为类`C`添加了一个静态属性`bar`
命名空间还能于同名 Enum 合并。
```typescript
enum E {
A,
B,
C,
}
namespace E {
export function foo() {
console.log(E.C);
}
}
E.foo() // 2
```
上面示例中,命名空间`E`为枚举`E`添加了一个`foo()`方法。
注意Enum 成员与命名空间导出成员不允许同名。
```typescript
enum E {
A, // 报错
B,
}
namespace E {
export function A() {} // 报错
}
```
上面示例中,同名 Enum 与命名空间有同名成员,结果报错。

303
docs/narrowing.md Normal file
View File

@@ -0,0 +1,303 @@
# TypeScript 类型缩小
TypeScript 变量的值可以变,但是类型通常是不变的。唯一允许的改变,就是类型缩小,就是将变量值的范围缩得更小。
## 手动类型缩小
如果一个变量属于联合类型,所以使用时一般需要缩小类型。
第一种方法是使用`if`判断。
```typescript
function getScore(value: number|string): number {
if (typeof value === 'number') { // (A)
// %inferred-type: number
value;
return value;
}
if (typeof value === 'string') { // (B)
// %inferred-type: string
value;
return value.length;
}
throw new Error('Unsupported value: ' + value);
}
```
如果一个值是`any``unknown`,你又想对它进行处理,就必须先缩小类型。
```typescript
function parseStringLiteral(stringLiteral: string): string {
const result: unknown = JSON.parse(stringLiteral);
if (typeof result === 'string') { // (A)
return result;
}
throw new Error('Not a string literal: ' + stringLiteral);
}
```
下面是另一个例子。
```typescript
interface Book {
title: null | string;
isbn: string;
}
function getTitle(book: Book) {
if (book.title === null) {
// %inferred-type: null
book.title;
return '(Untitled)';
} else {
// %inferred-type: string
book.title;
return book.title;
}
}
```
缩小类型的前提是,需要先获取类型。获取类型的几种方法如下。
```typescript
function func(value: Function|Date|number[]) {
if (typeof value === 'function') {
// %inferred-type: Function
value;
}
if (value instanceof Date) {
// %inferred-type: Date
value;
}
if (Array.isArray(value)) {
// %inferred-type: number[]
value;
}
}
```
### typeof 运算符
第二种方法是使用`switch`缩小类型。
```typescript
function getScore(value: number|string): number {
switch (typeof value) {
case 'number':
// %inferred-type: number
value;
return value;
case 'string':
// %inferred-type: string
value;
return value.length;
default:
throw new Error('Unsupported value: ' + value);
}
}
```
### instanceof 运算符
第三种方法是instanceof运算符。它能够检测实例对象与构造函数之间的关系。instanceof运算符的左操作数为实例对象右操作数为构造函数若构造函数的prototype属性值存在于实例对象的原型链上则返回true否则返回false。
```typescript
function f(x: Date | RegExp) {
if (x instanceof Date) {
x; // Date
}
if (x instanceof RegExp) {
x; // RegExp
}
}
```
instanceof类型守卫同样适用于自定义构造函数并对其实例对象进行类型细化。
```typescript
class A {}
class B {}
function f(x: A | B) {
if (x instanceof A) {
x; // A
}
if (x instanceof B) {
x; // B
}
}
```
### in 运算符
第四种方法是使用in运算符。
in运算符是JavaScript中的关系运算符之一用来判断对象自身或其原型链中是否存在给定的属性若存在则返回true否则返回false。in运算符有两个操作数左操作数为待测试的属性名右操作数为测试对象。
in类型守卫根据in运算符的测试结果将右操作数的类型细化为具体的对象类型。
```typescript
interface A {
x: number;
}
interface B {
y: string;
}
function f(x: A | B) {
if ('x' in x) {
x; // A
} else {
x; // B
}
}
```
```typescript
interface A { a: number }
interface B { b: number }
function pickAB(ab: A | B) {
if ('a' in ab) {
ab // Type is A
} else {
ab // Type is B
}
ab // Type is A | B
}
```
缩小对象的属性,要用`in`运算符。
```typescript
type FirstOrSecond =
| {first: string}
| {second: string};
function func(firstOrSecond: FirstOrSecond) {
if ('second' in firstOrSecond) {
// %inferred-type: { second: string; }
firstOrSecond;
}
}
// 错误
function func(firstOrSecond: FirstOrSecond) {
// @ts-expect-error: Property 'second' does not exist on
// type 'FirstOrSecond'. [...]
if (firstOrSecond.second !== undefined) {
// ···
}
}
```
`in`运算符只能用于联合类型,不能用于检查一个属性是否存在。
```typescript
function func(obj: object) {
if ('name' in obj) {
// %inferred-type: object
obj;
// 报错
obj.name;
}
}
```
### 特征属性
对于不同对象之间的区分,还可以人为地为每一类对象设置一个特征属性。
```typescript
interface UploadEvent {
type: 'upload';
filename: string;
contents: string
}
interface DownloadEvent { type: 'download'; filename: string; }
type AppEvent = UploadEvent | DownloadEvent;
function handleEvent(e: AppEvent) {
switch (e.type) {
case 'download':
e // Type is DownloadEvent
break;
case 'upload':
e; // Type is UploadEvent
break;
}
}
```
## any 类型的细化
TypeScript 推断变量类型时,会根据获知的信息,不断改变推断出来的类型,越来越细化。这种现象在`any`身上特别明显。
```typescript
function range(
start:number,
limit:number
) {
const out = []; // 类型为 any[]
for (let i = start; i < limit; i++) {
out.push(i);
}
return out; // 类型为 number[]
}
```
上面示例中,变量`out`的类型一开始推断为`any[]`,后来在里面放入数值,类型就变为`number[]`
再看下面的例子。
```typescript
const result = []; // 类型为 any[]
result.push('a');
result // 类型为 string[]
result.push(1);
result // 类型为 (string | number)[]
```
上面示例中,数组`result`随着成员类型的不同,而不断改变自己的类型。
注意,这种`any`类型的细化,只在打开了编译选项`noImplicitAny`时发生。
这时,如果在变量的推断类型还为`any`时(即没有任何写操作),就去输出(或读取)该变量,则会报错,因为这时推断还没有完成,无法满足`noImplicitAny`的要求。
```typescript
const result = []; // 类型为 any[]
console.log(typeof result); // 报错
result.push('a'); // 类型为 string[]
```
上面示例中,只有运行完第三行,`result`的类型才能完成第一次推断,所以第二行读取`result`就会报错。
## is 运算符
`is`运算符返回一个布尔值,用来判断左侧的值是否属于右侧的类型。
```typescript
function isInputElement(el: HTMLElement): el is HTMLInputElement {
return 'value' in el;
}
function getElementContent(el: HTMLElement) {
if (isInputElement(el)) {
el; // Type is HTMLInputElement
return el.value;
}
el; // Type is HTMLElement
return el.textContent;
}
```
```typescript
function isDefined<T>(x: T | undefined): x is T {
return x !== undefined;
}
```

87
docs/npm.md Normal file
View File

@@ -0,0 +1,87 @@
# TypeScript 项目使用 npm 模块
## 简介
npm 模块都是 JavaScript 代码。即使模块是用 TypeScript 写的,还是必须编译成 JavaScript 再发布,保证模块可以在没有 TypeScript 的环境运行。
问题就来了TypeScript 项目开发时,加载外部 npm 模块,如果拿不到该模块的类型信息,就会导致无法开发。所以,必须有一个方法,可以拿到模块的类型信息。
有些 npm 模块本身可能包含`.d.ts`文件甚至完整的 TypeScript 代码。它的`package.json`文件里面有一个`types`字段,指向一个`.d.ts`文件,这就是它的类型声明文件。
```javascript
{
"name": "left-pad",
"version": "1.3.0",
"description": "String left pad",
"main": "index.js",
"types": "index.d.ts",
// ...
}
```
如果某个模块没有`.d.ts`文件TypeScript 官方和社区就自发为常用模块添加类型描述,可以去[官方网站](https://www.typescriptlang.org/dt/search)搜索,然后安装网站给出的 npm 类型模块,通常是`@types/[模块名]`
```bash
$ npm install --save lodash
$ npm install --save @types/lodash
```
lodash 的类型描述就是`@types/lodash`的文件`index.d.ts`
## TS 模块转 npm 模块
TS 代码放在`ts`子目录,编译出来的 CommonJS 代码放在`dist`子目录。
## 如何写 TypeScript 模块
首先,创建模块目录,然后在该目录里面新建一个`tsconfig.json`
```json
{
"compilerOptions": {
"module": "commonjs",
"target": "es2015",
"declaration": true,
"outDir": "./dist"
},
"include": [
"src/**/*"
]
}
```
- `"declaration": true`:生成 .d.ts 文件,方便其他使用 TypeScript 的开发者加载你的库。
- `"module": "commonjs"`:编译后的模块格式为`commonjs`,表示该模块供 Node.js 使用。如果供浏览器使用,则要写成`"module": "esnext"`
- `"target": "es2015"`:生成的 JavaScript 代码版本为 ES2015需要 Node.js 8 以上版本。
- `"outDir": "./dist"`:编译后的文件放在`./dist`目录。
- `include`:指定需要编译的文件。
然后,使用 TypeScript 编写仓库代码。可以在`src`子目录里面,编写一个入口文件`index.ts`
最后,编写`package.json`
```typescript
{
"name": "hwrld",
"version": "1.0.0",
"description": "Can log \"hello world\" and \"goodbye world\" to the console!",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"files": [
"/dist"
]
}
```
里面的`"types": "dist/index.d.ts"`字段指定类型声明文件,否则使用这个库的 TypeScript 开发者找不到类型声明文件。`files`属性指定打包进入 npm 模块的文件。
然后,就是编译和发布。
```bash
$ tsc
$ npm publish
```
## 参考链接
- [How to Write a TypeScript Library](https://www.tsmean.com/articles/how-to-write-a-typescript-library/), by tsmean

771
docs/object.md Normal file
View File

@@ -0,0 +1,771 @@
# TypeScript 的对象类型
## 简介
除了原始类型,对象是 JavaScript 最基本的数据结构。TypeScript 对于对象类型有很多规则。
对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。
```typescript
const obj:{
x:number;
y:number;
} = { x: 1, y: 1 };
```
上面示例中,对象`obj`的类型就写在变量名后面,使用大括号描述,内部声明每个属性的属性名和类型。
属性的类型可以用分号结尾,也可以用逗号结尾。
```typescript
// 属性类型以分号结尾
type MyObj = {
x:number;
y:number;
};
// 属性类型以逗号结尾
type MyObj = {
x:number,
y:number,
};
```
最后一个属性后面,可以写分号或逗号,也可以不写。
一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。
```typescript
type MyObj = {
x:number;
y:number;
};
const o1:MyObj = { x: 1 }; // 报错
const o2:MyObj = { x: 1, y: 1, z: 1 }; // 报错
```
上面示例中,变量`o1`缺少了属性`y`,变量`o2`多出了属性`z`,都会报错。
读写不存在的属性也会报错。
```typescript
const obj:{
x:number;
y:number;
} = { x: 1, y: 1 };
console.log(obj.z); // 报错
obj.z = 1; // 报错
```
上面示例中,读写不存在的属性`z`都会报错。
同样地,也不能删除类型声明中存在的属性,修改属性值是可以的。
```typescript
const myUser = {
name: "Sabrina",
};
delete myUser.name // 报错
myUser.name = "Cynthia"; // 正确
```
上面声明中,删除类型声明中存在的属性`name`会报错,但是可以修改它的值。
对象的方法使用函数类型描述。
```typescript
const obj:{
x: number;
y: number;
add(x:number, y:number): number;
// 或者写成
// add: (x:number, y:number) => number;
} = {
x: 1,
y: 1,
add(x, y) {
return x + y;
}
};
```
上面示例中,对象`obj`有一个方法`add()`,需要定义它的参数类型和返回值类型。
对象类型可以使用方括号读取属性的类型。
```typescript
type User = {
name: string,
age: number
};
type Name = User['name']; // string
```
上面示例中,对象类型`User`使用方括号,读取了属性`name`的类型(`string`)。
除了`type`命令可以为对象类型声明一个别名TypeScript 还提供了`interface`命令,可以把对象类型提炼为一个接口。
```typescript
// 写法一
type MyObj = {
x:number;
y:number;
};
const obj:MyObj = { x: 1, y: 1 };
// 写法二
interface MyObj {
x: number;
y: number;
}
const obj:MyObj = { x: 1, y: 1 };
```
上面示例中,写法一是`type`命令的用法,写法二是`interface`命令的用法。`interface`命令的详细解释,以及与`type`命令的区别详见《Interface》一章。
注意TypeScript 不区分对象自身的属性和继承的属性,一律视为对象的属性。
```typescript
interface MyInterface {
toString(): string; // 继承的属性
prop: number; // 自身的属性
}
const obj:MyInterface = { // 正确
prop: 123,
};
```
上面示例中,`obj`只写了`prop`属性,但是不报错。因为它可以继承原型上面的`toString()`方法。
## 可选属性
如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。
```typescript
const obj: {
x: number;
y?: number;
} = { x: 1 };
```
上面示例中,属性`y`是可选的。
可选属性等同于允许赋值为`undefined`,下面两种写法是等效的。
```typescript
type User = {
firstName: string;
lastName?: string;
};
// 等同于
type User = {
firstName: string;
lastName: string|undefined;
};
```
上面示例中,类型`User`的属性`lastName`可以是字符串,也可以是`undefined`,就表示该属性可以省略不写。
同理,读取一个可选属性时,有可能返回`undefined`
```typescript
type MyObj = {
x: string,
y?: string
};
const obj:MyObj = { x: 'hello' };
obj.y.toLowerCase() // 报错
```
上面示例中,最后一行会报错,因为`obj.y`有可能是`undefined`,无法对其调用`toLowerCase()`
所以,读取可选属性之前,必须检查一下是否为`undefined`
```typescript
const user:{
firstName: string;
lastName?: string;
} = { firstName: 'Foo'};
if (user.lastName !== undefined) {
console.log(`hello ${user.firstName} ${user.lastName}`)
}
```
上面示例中,`lastName`是可选属性,需要判断是否为`undefined`以后,才能使用。建议可以使用下面的写法。
```typescript
// 写法一
let firstName = (user.firstName === undefined)
? 'Foo' : user.firstName;
let lastName = (user.lastName === undefined)
? 'Bar' : user.lastName;
// 写法二
let firstName = user.firstName ?? 'Foo';
let lastName = user.lastName ?? 'Bar';
```
上面示例中,写法一使用三元运算符`?:`,判断是否为`undefined`,并设置默认值。写法二使用 Null 判断运算符`??`,与写法一的作用完全相同。
## 只读属性
属性名前面加上`readonly`关键字,表示这个属性是只读属性,不能修改。
```typescript
interface MyInterface {
readonly prop: number;
}
```
上面示例中,`prop`属性是只读属性,不能修改它的值。
```typescript
const person:{
readonly age: number
} = { age: 20 };
person.age = 21; // 报错
```
上面示例中,最后一行修改了只读属性`age`,就报错了。
只读属性只能在对象初始化期间赋值,此后就不能修改该属性。
```typescript
type Point = {
readonly x: number;
readonly y: number;
};
const p:Point = { x: 0, y: 0 };
p.x = 100; // 报错
```
上面示例中,类型`Point`的属性`x``y`都带有修饰符`readonly`,表示这两个属性只能在初始化期间赋值,后面再修改就会报错。
注意,如果属性值是一个对象,`readonly`修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。
```typescript
interface Home {
readonly resident: {
name: string;
age: number
};
}
const h:Home = {
resident: {
name: 'Vicky',
age: 42
}
};
h.resident.age = 32; // 正确
h.resident = {
name: 'Kate',
age: 23
} // 报错
```
上面示例中,`h.resident`是只读属性,它的值是一个对象。修改这个对象的`age`属性是可以的,但是整个替换掉`h.resident`属性会报错。
另一个需要注意的地方是,如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量。
```typescript
interface Person {
name: string;
age: number;
}
interface ReadonlyPerson {
readonly name: string;
readonly age: number;
}
let w:Person = {
name: 'Vicky',
age: 42,
};
let r:ReadonlyPerson = w;
w.age += 1;
r.age // 43
```
上面示例中,变量`w``r`指向同一个对象,其中`w`是可写的,`r`的只读的。那么,对`w`的属性修改,会影响到`r`
如果希望属性值是只读的,除了声明时加上`readonly`关键字,还有一种方法,就是在赋值时,在对象后面加上只读断言`as const`
```typescript
const myUser = {
name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 报错
```
上面示例中,对象后面加了只读断言`as const`,就变成只读对象了,不能修改属性了。
注意,上面的`as const`属于 TypeScript 的类型推断,如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准。
```typescript
const myUser:{ name: string } = {
name: "Sabrina",
} as const;
myUser.name = "Cynthia"; // 正确
```
上面示例中,根据变量`myUser`的类型声明,`name`不是只读属性,但是赋值时又使用只读断言`as const`。这时会以声明的类型为准,因为`name`属性可以修改。
## 属性名的索引类型
如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。
索引类型里面,最常见的就是属性名的字符串索引。
```typescript
type MyObj = {
[property: string]: string
};
const obj:MyObj = {
foo: 'a',
bar: 'b',
baz: 'c',
};
```
上面示例中,类型`MyObj`的属性名类型就采用了表达式形式,写在方括号里面。`[property: string]``property`表示属性名,这个是可以随便起的,它的类型是`string`,即属性名类型为`string`。也就是说,不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。
属性名(即上例的`property`)的类型有三种可能,除了上例的`string`,还有`number``symbol`
```typescript
type T1 = {
[property: number]: string
};
type T2 = {
[property: symbol]: string
};
```
上面示例中,对象属性名的类型分别为`number``symbol`
```typescript
type MyArr = {
[n:number]: number;
};
const arr:MyArr = [1, 2, 3];
// 或者
const arr:MyArr = {
0: 1,
1: 2,
2: 3,
};
```
上面示例中,对象类型`MyArr`的属性名是`[n:number]`,就表示它的属性名都是数值,比如`0``1``2`
对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索性。但是,数值索性不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名。
```typescript
type MyType = {
[x: number]: boolean; // 报错
[x: string]: string;
}
```
上面示例中,类型`MyType`同时有两种属性名索引,但是数值索引与字符串索引冲突了,所以报错了。由于字符属性名的值类型是`string`,数值属性名的值类型只有同样为`string`,才不会报错。
同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名符合属性名索引的范围,两者不能有冲突,否则报错。
```typescript
type MyType = {
foo: boolean; // 报错
[x: string]: string;
}
```
上面示例中,属性名`foo`符合属性名的字符串索引,但是两者的属性值类型不一样,所以报错了。
属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及`length`属性,因为类型里面没有定义这些东西。
```typescript
type MyArr = {
[n:number]: number;
};
const arr:MyArr = [1, 2, 3];
arr.length // 报错
```
上面示例中,读取`arr.length`属性会报错,因为类型`MyArr`没有这个属性。
## 解构赋值
解构赋值用于直接从对象中提取属性。
```typescript
const {id, name, price} = product;
```
上面语句从对象`product`提取了三个属性,并声明属性名的同名变量。
解构赋值的类型写法,跟为对象声明类型是一样的。
```typescript
const {id, name, price}:{
id: string;
name: string;
price: number
} = product;
```
注意目前没法为解构变量指定类型因为对象解构里面的冒号JavaScript 指定了其他用途。
```typescript
let { x: foo, y: bar } = obj;
// 等同于
let foo = obj.x;
let bar = obj.y;
```
上面示例中,冒号不是表示属性`x``y`的类型,而是为这两个属性指定新的变量名。如果要为`x``y`指定类型,不得不写成下面这样。
```typescript
let { x: foo, y: bar }
: { x: string; y: number } = obj;
```
这一点要特别小心TypeScript 里面很容易搞糊涂。
```typescript
function draw({
shape: Shape,
xPos: number = 100,
yPos: number = 100
}) {
let myShape = shape; // 报错
let x = xPos; // 报错
}
```
上面示例中,函数`draw()`的参数是一个对象解构里面的冒号很像是为变量指定类型其实是为对应的属性指定新的变量名。所以TypeScript 就会解读成,函数体内不存在变量`shape`,而是属性`shape`的值被赋值给了变量`Shape`
## 结构类型原则
只要对象 B 满足 对象 A 的结构特征TypeScript 就认为对象 B 兼容对象 A 的类型这称为“结构类型”原则structual typing
```typescript
const A = {
x: number;
};
const B = {
x: number;
y: number;
};
```
上面示例中,对象`A`只有一个属性`x`,类型为`number`。对象`B`满足这个特征,因此兼容对象`A`,只要可以使用`A`的地方,就可以使用`B`
```typescript
const B = {
x: 1,
y: 1
};
const A:{ x: number } = B; // 正确
```
上面示例中,`A``B`并不是同一个类型,但是`B`可以赋值给`A`,因为`B`满足`A`的结构特征。
根据“结构类型”原则TypeScript 检查某个值是否符合指定类型时,并不是检查这个值的类型名(即“名义类型”),而是检查这个值的结构是否符合要求(即“结构类型”)。
TypeScript 之所以这样设计,是为了符合 JavaScript 的行为。JavaScript 并不关心对象是否严格相似,只要某个对象具有所要求的属性,就可以正确运行。
如果类型 B 可以赋值给类型 ATypeScript 就认为 B 是 A 的子类型subtypingA 是 B 的父类型。子类型满足父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即子类型兼容父类型。
这种设计有时会导致令人惊讶的结果。
```typescript
type myObj = {
x: number,
y: number,
};
function getSum(obj:myObj) {
let sum = 0;
for (const n of Object.keys(obj)) {
const v = obj[n]; // 报错
sum += Math.abs(v);
}
return sum;
}
```
上面示例中,函数`getSum()`要求传入参数的类型是`myObj`,但是实际上所有与`myObj`兼容的对象都可以传入。这会导致`const v = obj[n]`这一行会报错,原因是`obj[n]`取出的属性值不一定是数值(`number`),使得变量`v`的类型是`any`。如果不允许变量类型推断为`any`,代码就会报错。如果写成下面这样,就不会报错。
```typescript
type MyObj = {
x: number,
y: number,
};
function getSum(obj:MyObj) {
return Math.abs(obj.x) + Math.abs(obj.y);
}
```
上面示例就不会报错,因为函数体内部只使用了属性`x``y`,这两个属性有明确的类型声明,保证`obj.x``obj.y`肯定是数值。虽然与`MyObj`兼容的任何对象都可以传入函数`getSum()`,但是只要不使用其他属性,就不会有类型报错。
## 严格字面量检查
如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查strict object literal checking。如果字面量的结构跟类型定义的不一样比如多出了未定义的属性就会报错。
```typescript
const point:{
x:number;
y:number;
} = {
x: 1,
y: 1,
z: 1 // 报错
};
```
上面示例中,等号右边是一个对象的字面量,这时会触发严格字面量检查。只要有类型声明中不存在的属性(本例是`z`),就会导致报错。
如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的。
```typescript
const myPoint = {
x: 1,
y: 1,
z: 1
};
const point:{
x:number;
y:number;
} = myPoint; // 正确
```
上面示例中,等号右边是一个变量,就不会触发严格字面量检查,从而不报错。
TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用了 API。
```typescript
type Options = {
title:string;
darkMode?:boolean;
};
const Obj:Options = {
title: '我的网页',
darkmode: true, // 报错
};
```
上面示例中,属性`darkMode`拼写错了,成了`darkmode`。如果没有严格字面量规则,就不会报错,因为`darkMode`是可选属性,根据结构类型原则,任何对象只要有`title`属性,都认为符合`Options`类型。
规避严格字面量检查,可以使用中间变量。
```typescript
let myOptions = {
title: '我的网页',
darkmode: true,
};
const Obj:Options = myOptions;
```
上面示例中,创建了一个中间变量`myOptions`,就不会触发严格字面量规则,因为这时变量`obj`的赋值,不属于直接字面量赋值。
如果你确认字面量没有错误,也可以使用类型断言规避严格字面量检查。
```typescript
const Obj:Options = {
title: '我的网页',
darkmode: true,
} as Options;
```
上面示例使用类型断言`as Options`,告诉编译器,字面量符合 Options 类型,就能规避这条规则。
如果允许字面量有多余属性,可以像下面这样在类型里面定义一个通用属性。
```typescript
let x: {
foo: number,
[x: string]: any
};
x = { foo: 1, baz: 2 }; // Ok
```
上面示例中,变量`x`的类型声明里面,有一个属性的字符串索引(`[x: string]`),导致任何字符串属性名都是合法的。
由于严格字面量检查,字面量对象传入函数必须很小心,不能有多余的属性。
```typescript
interface Point {
x: number;
y: number;
}
function computeDistance(point: Point) { /*...*/ }
computeDistance({ x: 1, y: 2, z: 3 }); // 报错
computeDistance({x: 1, y: 2}); // 正确
```
上面示例中,对象字面量传入函数`computeDistance()`时,不能有多余的属性,否则就通不过严格字面量检查。
编译器选项`suppressExcessPropertyErrors`,可以在`tsconfig.json`文件里面关闭多余属性检查。
```typescript
{
"compilerOptions": {
"suppressExcessPropertyErrors": true
}
}
```
## 最小可选属性规则
如果一个对象的所有属性都是可选的,会触发最小可选属性规则。
```typescript
type Options = {
a?:number;
b?:number;
c?:number;
};
const obj:Options = {
d: 123 // 报错
};
```
上面示例中,类型`Options`是一个对象,它的所有属性都是可选的,这导致任何对象实际都符合`Options`类型。
为了避免这种情况TypeScript 添加了最小可选属性规则,规定这时属于`Options`类型的对象,必须至少存在一个可选属性,不能所有可选属性都不存在。这就是为什么上例的`myObj`对象会报错的原因。
这条规则无法通过中间变量规避。
```typescript
const myOptions = { d: 123 };
const obj:Options = myOptions; // 报错
```
上面示例中,即使使用了中间变量`myOptions`,由于存在最小可选属性规则,依然会报错。
## 空对象
空对象是 TypeScript 的一种特殊值,也是一种特殊类型。
```typescript
const obj = {};
obj.prop = 123; // 报错
```
上面示例中,变量`obj`的值是一个空对象,然后对`obj.prop`赋值就会报错。
原因是这时 TypeScript 会推断变量`obj`的类型为空对象,实际执行的是下面的代码。
```typescript
const obj:{} = {};
```
空对象没有自定义属性,所以对自定义属性赋值就会报错。空对象只能使用继承的属性,即继承自原型对象`Object.prototype`的属性。
```typescript
obj.toString() // 正确
```
上面示例中,`toString()`方法是一个继承自原型对象的方法TypeScript 允许在空对象上使用。
回到本节开始的例子,这种写法其实在 JavaScript 很常见先声明一个空对象然后向空对象添加属性。但是TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。
```typescript
// 错误
const pt = {};
pt.x = 3;
pt.y = 4;
// 正确
const pt = {
x: 3,
y: 4
};
```
如果确实需要分步声明,一个比较好的方法是,使用扩展运算符(`...`)合成一个新对象。
```typescript
const pt0 = {};
const pt1 = { x: 3 };
const pt2 = { y: 4 };
const pt = {
...pt0, ...pt1, ...pt2
};
```
上面示例中,对象`pt`是三个部分合成的,这样既可以分步声明,也符合 TypeScript 静态声明的要求。
空对象作为类型,其实是`Object`类型的简写形式。
```typescript
let d:{};
// 等同于
// let d:Object;
d = {};
d = { x: 1 };
d = 'hello';
d = 2;
```
上面示例中,各类类型的值(除了`null``undefined`)都可以赋值给空对象类型,跟`Object`类型的行为是一样的。
由于空对象是`Object`类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性。
```typescript
interface Empty { }
const b:Empty = {myProp: 1, anotherProp: 2}; // 正确
b.myProp // 报错
```
上面示例中,变量`b`的类型是空对象,视同`Object`类型,不会有严格字面量检查,但是读取多余的属性会报错。
如果想强制使用没有任何属性的对象,可以采用下面的写法。
```typescript
interface WithoutProperties {
[key: string]: never;
}
// 报错
const a:WithoutProperties = { prop: 1 };
```
上面的示例中,`[key: string]: never`表示字符串属性名是不存在的,因此其他对象进行赋值时就会报错。

627
docs/operator.md Normal file
View File

@@ -0,0 +1,627 @@
# TypeScript 类型运算符
TypeScript 提供强大的类型运算能力,可以使用各种类型运算符,对已有的类型进行计算,得到新类型。
## keyof 运算符
### 简介
keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。
```typescript
type MyObj = {
foo: number,
bar: string,
};
type Keys = keyof MyObj; // 'foo'|'bar'
```
上面示例中,`keyof MyObj`返回`MyObj`的所有键名组成的联合类型,即`'foo'|'bar'`
下面是另一个例子。
```typescript
interface T {
0: boolean;
a: string;
b(): void;
}
type KeyT = keyof T; // 0 | 'a' | 'b'
```
由于 JavaScript 对象的键名只有三种类型,所以对于任意键名的联合类型就是`string|number|symbol`
```typescript
// string | number | symbol
type KeyT = keyof any;
```
对于上面三种类型以外的类型使用 keyof 运算符,返回`never`类型,表示不可能有这样类型的键名。
```typescript
type KeyT = keyof object; // never
```
上面示例中,由于不可能有`object`类型的键名,所以`keyof object`返回`never`类型。
由于 keyof 返回的类型是`string|number|symbol`,如果有些场合只需要其中的一种类型,那么可以采用交叉类型的写法。
```typescript
type Capital<T extends string> = Capitalize<T>;
type MyKeys<Obj extends object> = Capital<keyof Obj>; // 报错
```
上面示例中,类型`Capital`只接受字符串作为类型参数,传入`keyof Obj`会报错,原因是这时的类型参数是`string|number|symbol`,跟字符串不兼容。
采用下面的交叉类型写法,就不会报错。
```typescript
type MyKeys<Obj extends object> = Capital<string & keyof Obj>;
```
上面示例中,`string & keyof Obj`等同于`string & string|number|symbol`进行交集运算,最后返回`string`,因此`Capital<T extends string>`就不会报错了。
如果对象属性名采用索引形式keyof 会返回属性名的索引类型。
```typescript
// 示例一
interface T {
[prop: number]: number;
}
// number
type KeyT = keyof T;
// 示例二
interface T {
[prop: string]: number;
}
// string|number
type KeyT = keyof T;
```
上面的示例二,`keyof T`返回的类型是`string|number`,原因是 JavaScript 属性名为字符串时,包含了属性名为数值的情况,因为数值属性名会自动转为字符串。
如果 keyof 运算符用于数组或元组类型,得到的结果可能出人意料。
```typescript
// 返回 number | "0" | "1" | "2"
// | "length" | "pop" | "push" | ···
type Result = keyof ['a', 'b', 'c'];
```
上面示例中keyof 会返回数组的所有属性名,包括字符串属性名和继承的属性名。
对于联合类型keyof 返回成员共有的键名。
```typescript
type A = { a: string; z: boolean };
type B = { b: string; z: boolean };
// 'z'
type KeyT = keyof (A | B);
```
对于交叉类型keyof 返回所有键名。
```typescript
type A = { a: string; x: boolean };
type B = { b: string; y: number };
// 返回 'a' | 'x' | 'b' | 'y'
type KeyT = keyof (A & B);
// 相当于
keyof (A & B) keyof A | keyof B
```
keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写。
```typescript
type MyObj = {
foo: number,
bar: string,
};
type Keys = keyof MyObj;
type Values = MyObj[Keys]; // number|string
```
上面示例中,`Keys`是键名组成的联合类型,而`MyObj[Keys]`会取出每个键名对应的键值类型,组成一个新的联合类型,即`number|string`
### keyof 运算符的用途
keyof 运算符往往用于精确表达对象的属性类型。
举例来说取出对象的某个指定属性的值JavaScript 版本可以写成下面这样。
```typescript
function prop(obj, key) {
return obj[key];
}
```
上面这个函数添加类型,只能写成下面这样。
```javascript
function prop(
obj:object, key:string
):any {
return obj[key];
}
```
上面的类型声明有两个问题,一是无法表示参数`key`与参数`obj`之间的关系,二是返回值类型只能写成`any`
有了 keyof 以后,就可以解决这两个问题,精确表达返回值类型。
```javascript
function prop<Obj, K extends keyof Obj>(
obj:Obj, key:K
):Obj[K] {
return obj[key];
}
```
上面示例中,`K extends keyof Obj`表示`K``Obj`的一个属性名,传入其他字符串会报错。返回值类型`Obj[K]`就表示`K`这个属性值的类型。
keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。
```typescript
type NewProps<Obj> = {
[Prop in keyof Obj]: boolean;
};
// 用法
type MyObj = { foo: number; };
// 等于 { foo: boolean; }
type NewObj = NewProps<MyObj>;
```
上面示例中,类型`NewProps`是类型`Obj`的映射类型,前者继承了后者的所有属性,但是把所有属性值类型都改成了`boolean`
下面的例子是去掉 readonly 修饰符。
```typescript
type Mutable<Obj> = {
-readonly [Prop in keyof Obj]: Obj[Prop];
};
// 用法
type MyObj = {
readonly foo: number;
}
// 等于 { foo: number; }
type NewObj = Mutable<MyObj>;
```
上面示例中,`[Prop in keyof Obj]``Obj`类型的所有属性名,`-readonly`表示去除这些属性的只读特性。对应地,还有`+readonly`的写法,表示添加只读属性设置。
下面的例子是让可选属性变成必有的属性。
```typescript
type Concrete<Obj> = {
[Prop in keyof Obj]-?: Obj[Prop];
};
// 用法
type MyObj = {
foo?: number;
}
// 等于 { foo: number; }
type NewObj = Concrete<MyObj>;
```
上面示例中,`[Prop in keyof Obj]`后面的`-?`表示去除可选属性设置。对应地,还有`+?`的写法,表示添加可选属性设置。
## in 运算符
JavaScript 语言中,`in`运算符用来确定对象是否包含某个属性名。
```javascript
const obj = { a: 123 };
if ('a' in obj)
console.log('found a');
```
上面示例中,`in`运算符用来判断对象`obj`是否包含属性`a`
`in`运算符的左侧是一个字符串,表示属性名,右侧是一个对象。它的返回值是一个布尔值。
TypeScript 语言的类型运算中,`in`运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。
```typescript
type U = 'a'|'b'|'c';
type Foo = {
[Prop in U]: number;
};
// 等同于
type Foo = {
a: number,
b: number,
c: number
};
```
上面示例中,`[Prop in U]`表示依次取出联合类型`U`的每一个成员。
上一小节的例子也提到,`[Prop in keyof Obj]`表示取出对象`Obj`的每一个键名。
## 方括号运算符
方括号运算符(`[]`)用于取出对象的键值类型,比如`T[K]`会返回对象`T`的属性`K`的类型。
```typescript
type Person = {
age: number;
name: string;
alive: boolean;
};
// Age 的类型是 number
type Age = Person['age'];
```
上面示例中,`Person['age']`返回属性`age`的类型,本例是`number`
方括号的参数如果是联合类型,那么返回的也是联合类型。
```typescript
type Person = {
age: number;
name: string;
alive: boolean;
};
// number|string
type T = Person['age'|'name'];
// number|string|boolean
type A = Person[keyof Obj];
```
上面示例中,方括号里面是属性名的联合类型,所以返回的也是对应的属性值的联合类型。
如果访问不存在的属性,会报错。
```typescript
type T = Person['notExisted']; // 报错
```
如果对象的属性是索引类型,那么方括号运算符的参数可以是属性名的类型。
```typescript
type Obj = {
[key:string]: number,
};
// number
type T = Obj[string];
```
上面示例中,`Obj`的属性名是字符串的索引类型,所以可以写成`Obj[string]`,代表所有字符串属性名,返回的就是它们的类型`number`
这个语法对于数组也适用,可以使用`number`作为方括号的参数。
```typescript
// MyArray 的类型是 { [key:number]string }
const MyArray = ['a','b','c'];
// 等同于 (typeof MyArray)[number]
// 返回 string
type Person = typeof MyArray[number];
```
上面示例中,`MyArray`是一个数组,它的类型实际上是属性名的数值索引,而`typeof MyArray[number]``typeof`运算优先级高于方括号,所以返回的是所有数值键名的键值类型`string`
注意,方括号里面不能有值的运算。
```typescript
// 示例一
const key = 'age';
type Age = Person[key]; // 报错
// 示例二
type Age = Person['a' + 'g' + 'e']; // 报错
```
上面两个示例,方括号里面都涉及值的运算,编译时不会进行这种运算,所以会报错。
## extends...?: 条件运算符
TypeScript 提供类似 JavaScript 的`?:`运算符这样的三元运算符,但多出了一个`extends`关键字。
条件运算符`extends...?:`可以根据当前类型是否符合某种条件,返回不同的类型。
```typescript
T extends U ? X : Y
```
上面式子中的`extends`用来判断,类型`T`是否可以赋值给类型`U`,即`T`是否为`U`的子类型,这里的`T``U`可以是任意类型。
如果`T`能够赋值给类型`U`,表达式的结果为类型`X`,否则结果为类型`Y`
```typescript
// true
type T = 1 extends number ? true : false;
```
上面示例中,`1``number`的子类型,所以返回`true`
下面是另外一个例子。
```typescript
interface Animal {
live(): void;
}
interface Dog extends Animal {
woof(): void;
}
// number
type T1 = Dog extends Animal ? number : string;
// string
type T2 = RegExp extends Animal ? number : string;
```
上面示例中,`Dog``Animal`的子类型,所以`T1`的类型是`number``RegExp`不是`Animal`的子类型,所以`T2`的类型是`string`
一般来说,调换`extends`两侧类型,会返回相反的结果。举例来说,有两个类`Dog``Animal`,前者是后者的子类型,那么`Cat extends Animal`就为真,而`Animal extends Cat`就为伪。
如果需要判断的类型是一个联合类型,那么条件运算符会展开这个联合类型。
```typescript
(A|B) extends U ? X : Y
// 等同于
(A extends U ? X : Y) |
(B extends U ? X : Y)
```
上面示例中,`A|B`是一个联合类型,进行条件运算时,相当于`A``B`分别进行运算符,返回结果组成一个联合类型。
如果不希望联合类型被条件运算符展开,可以把`extends`两侧的操作数都放在方括号里面。
```typescript
// 示例一
type ToArray<Type> =
Type extends any ? Type[] : never;
// string[]|number[]
type T = ToArray<string|number>;
// 示例二
type ToArray<Type> =
[Type] extends [any] ? Type[] : never;
// (string | number)[]
type T = ToArray<string|number>;
```
上面的示例一,传入的类型参数是联合类型,所以会被展开,返回的也是联合类型。示例二是`extends`两侧的运算数都放在方括号里面,所以传入的联合类型不会展示,返回的是一个数组。
条件运算符还可以嵌套使用。
```typescript
type LiteralTypeName<T> =
T extends undefined ? "undefined" :
T extends null ? "null" :
T extends boolean ? "boolean" :
T extends number ? "number" :
T extends bigint ? "bigint" :
T extends string ? "string" :
never;
```
上面示例是一个多重判断,返回一个字符串的值类型,对应当前类型。下面是它的用法。
```typescript
// "bigint"
type Result1 = LiteralTypeName<123n>;
// "string" | "number" | "boolean"
type Result2 = LiteralTypeName<true | 1 | 'a'>;
```
## infer 关键字
`infer`关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。
它通常跟条件运算符一起使用,用在`extends`关键字后面的父类型之中。
```typescript
type Flatten<Type> =
Type extends Array<infer Item> ? Item : Type;
```
上面示例中,`Type`是外部传入的类型参数,如果传入的是一个数组(`Array`),那么可以从该数组推断出它的成员类型,写成`infer Item`,表示`Item`这个类型参数是从当前信息中推断出来的。
一旦定义了`Item`,后面的代码就可以使用这个类型参数了。
下面是这个泛型`Flatten<Type>`的用法。
```typescript
// string
type Str = Flatten<string[]>;
// number
type Num = Flatten<number>;
```
上面示例中,第一个例子`Flatten<string[]>`传入的类型参数是`string[]`,可以推断出`Item`的类型是`string`,所以返回的是`string`。第二个例子`Flatten<number>`传入的类型参数是`number`,它不是数组,所以直接返回本身。
如果不用`infer`定义类型参数,那么就要传入两个类型参数。
```typescript
type Flatten<Type, Item> =
Type extends Array<Item> ? Item : Type;
```
上面是不用`infer`的写法,每次使用`Fleatten`的时候,都要传入两个参数,就非常麻烦。
下面的例子使用`infer`,推断函数的参数类型和返回值类型。
```typescript
type ReturnPromise<T> =
T extends (...args: infer A) => infer R
? (...args: A) => Promise<R>
: T;
```
上面示例中,如果`T`是函数,就返回这个函数的 Promise 版本,否则原样返回。`infer A`表示该函数的参数类型为`A``infer R`表示该函数的返回值类型为`R`
如果不使用`infer`,就不得不把`ReturnPromise<T>`写成`ReturnPromise<T, A, R>`,这样就很麻烦。
下面是`infer`提取对象指定属性的例子。
```typescript
type MyType<T> =
T extends {
a: infer M,
b: infer N
} ? [M, N] : never;
// [string, number]
type T = MyType<{ a: string; b: number }>;
```
上面示例中,`infer`可以提取参数对象的属性`a`和属性`b`的值。
下面是`infer`通过正则匹配提取类型参数的例子。
```typescript
type Str = 'foo-bar';
type Bar = Str extends `foo-${infer rest}` ? rest : never // 'bar'
```
上面示例中,`rest`是从模板字符串提取的类型参数。
## is 运算符
函数返回布尔值的时候,可以使用`is`运算符,限定返回值与参数之间的关系。
`is`运算符用来描述返回值属于`true`还是`false`
```typescript
function isFish(
pet:Fish|Bird
):pet is Fish {
return (pet as Fish).swim !== undefined;
}
```
上面示例中,函数`isFish()`的返回值类型为`pet is Fish`,表示如果参数`pet`类型为`Fish`,则返回`true`,否则返回`false`
`is`运算符总是用于描述函数的返回值类型,写法采用`parameterName is Type`的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。
```typescript
type A = { a: string };
type B = { b: string };
function isTypeA(x: A|B): x is A {
if ('a' in x) return true;
return false;
}
```
上面示例中,返回值类型`x is A`可以准确描述函数体内部的运算逻辑。
`is`运算符可以用于类型保护。
```typescript
function isCat(a:any): a is Cat {
return a.name === 'kitty';
}
let x:Cat|Dog;
if (isCat(x)) {
x.meow(); // 正确,因为 x 肯定是 Cat 类型
}
```
上面示例中,需要保证`x``meow()`方法,`isCat()`的返回值是`a is Cat``if`结合,就能起到类型保护的作用,确保`x`是 Cat 类型。
`is`运算符还有一种特殊用法就是用在类class的内部描述类的方法的返回值。
```typescript
class Teacher {
isStudent():this is Student {
return false;
}
}
class Student {
isStudent():this is Student {
return true;
}
}
```
上面示例中,`isStudent()`方法的返回值类型,取决于该方法内部的`this`是否为`Student`对象。
注意,`this is T`这种写法,只能用来描述方法的返回值类型,而不能用来描述属性的类型。
## 模板字符串
TypeScript 允许使用模板字符串,构建类型。
模板字符串的最大特点,就是内部可以引用其他类型。
```typescript
type World = "world";
// "hello world"
type Greeting = `hello ${World}`;
```
上面示例中,类型`Greeting`是一个模板字符串,里面引用了另一个字符串类型`world`,因此`Greeting`实际上是字符串`hello world`
注意模板字符串可以引用的类型一共6种分别是 string、number、bigint、boolean、null、undefined。引用其他类型会报错。
```typescript
type N = 123;
type O = { n : 123 };
type T1 = `${N} received`; // 正确
type T2 = `${O} received`; // 报错
```
上面示例中,模板字符串引用数值类型(`N`)是可以的,但是引用对象类型(`O`)就会报错。
模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型。
```typescript
type T = 'A'|'B';
// "A_id"|"B_id"
type U = `${T}_id`;
```
上面示例中,类型`U`是一个模板字符串,里面引用了一个联合类型`T`,导致最后得到的也是一个联合类型。
如果模板字符串引用两个联合类型,它会交叉展开这两个类型。
```typescript
type T = 'A'|'B';
type U = '1'|'2';
// 'A1'|'A2'|'B1'|'B2'
type V = `${T}${U}`;
```
上面示例中,`T``U`都是联合类型各自有两个成员模板字符串里面引用了这两个类型最后得到的就是一个4个成员的联合类型。

82
docs/react.md Normal file
View File

@@ -0,0 +1,82 @@
# TypeScript 的 React 支持
## JSX 语法
JSX 是 React 库引入的一种语法,可以在 JavaScript 脚本中直接书写 HTML 风格的标签。
TypeScript 支持 JSX 语法,但是必须将脚本后缀名改成`.tsx`
`.tsx`文件中,类型断言一律使用`as`形式,因为尖括号的写法会与 JSX 冲突。
```typescript
// 使用
var x = foo as any;
// 不使用
var x = <any>foo;
```
上面示例中,变量`foo`被断言为类型`any`,在`.tsx`文件中只能使用第一种写法,不使用第二种写法。
## React 库
TypeScript 使用 React 库必须引入 React 的类型定义。
```typescript
/// <reference path="react.d.ts" />
interface Props {
name: string;
}
class MyComponent extends React.Component<Props, {}> {
render() {
return <span>{this.props.name}</span>;
}
}
<MyComponent name="bar" />; // OK
<MyComponent name={0} />; // error, `name` is not a number
```
## 内置元素
内置元素使用`JSX.IntrinsicElements`接口。默认情况下,内置元素不进行类型检查。但是,如果给出了接口定义,就会进行类型检查。
```typescript
declare namespace JSX {
interface IntrinsicElements {
foo: any;
}
}
<foo />; // ok
<bar />; // error
```
上面示例中,`<bar />`不符合接口定义,所以报错。
一种解决办法就是,在接口中定义一个通用元素。
```typescript
declare namespace JSX {
interface IntrinsicElements {
[elemName: string]: any;
}
}
```
上面示例中, 元素名可以是任意字符串。
## 组件的写法
```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 />
);
```

163
docs/symbol.md Normal file
View File

@@ -0,0 +1,163 @@
# TypeScript 的 symbol 类型
## 简介
Symbol 是 ES2015 新引入的一种原始类型的值。它类似于字符串,但是每一个 Symbol 值都是独一无二的,与其他任何值都不相等。
Symbol 值通过`Symbol()`函数生成。在 TypeScript 里面Symbol 的类型使用`symbol`表示。
```typescript
let x:symbol = Symbol();
let y:symbol = Symbol();
x === y // false
```
上面示例中,变量`x``y`的类型都是`symbol`,且都用`Symbol()`生成,但是它们是不相等的。
## unique symbol
`symbol`类型包含所有的 Symbol 值,但是无法表示某一个具体的 Symbol 值。
比如,`5`是一个具体的数值,就用`5`这个字面量来表示这也是它的值类型。但是Symbol 值不存在字面量,必须通过变量来引用,所以写不出只包含单个 Symbol 值的那种值类型。
为了解决这个问题TypeScript 设计了`symbol`的一个子类型`unique symbol`,它表示单个的、某个具体的 Symbol 值。
因为`unique symbol`表示单个值,所以这个类型的变量是不能修改值的,只能用`const`命令声明,不能用`let`声明。
```typescript
// 正确
const x:unique symbol = Symbol();
// 报错
let y:unique symbol = Symbol();
```
上面示例中,`let`命令声明的变量,不能是`unique symbol`类型,会报错。
`const`命令为变量赋值 Symbol 值时,变量类型默认就是`unique symbol`,所以类型可以省略不写。
```typescript
const x:unique symbol = Symbol();
// 等同于
const x = Symbol();
```
每个声明为`unique symbol`类型的变量,它们的值都是不一样的,其实属于两个类型。
```typescript
const a:unique symbol = Symbol();
const b:unique symbol = Symbol();
a === b // 报错
```
上面示例中,变量`a`和变量`b`的类型虽然都是`unique symbol`,但其实是两个值类型。不同类型的值肯定是不相等的,所以最后一行就报错了。
由于 Symbol 类似于字符串,可以参考下面的例子来理解。
```typescript
const a:'hello' = 'hello';
const b:'world' = 'world';
a === b // 报错
```
上面示例中,变量`a``b`都是字符串,但是属于不同的值类型,不能使用严格相等运算符进行比较。
而且,由于变量`a``b`是两个类型,就不能把一个赋值给另一个。
```typescript
const a:unique symbol = Symbol();
const b:unique symbol = a; // 报错
```
上面示例中,变量`a`和变量`b`的类型都是`unique symbol`,但是其实类型不同,所以把`a`赋值给`b`会报错。
上例变量`b`的类型,如果要写成与变量`a`同一个`unique symbol`值类型,只能写成类型为`typeof a`
```typescript
const a:unique symbol = Symbol();
const b:typeof a = a; // 正确
```
不过我们知道,相同参数的`Symbol.for()`方法会返回相同的 Symbol 值。TypeScript 目前无法识别这种情况,所以可能出现多个 unique symbol 类型的变量,等于同一个 Symbol 值的情况。
```typescript
const a:unique symbol = Symbol.for('foo');
const b:unique symbol = Symbol.for('foo');
```
上面示例中,变量`a``b`是两个不同的值类型,但是它们的值其实是相等的。
unique symbol 类型是 symbol 类型的子类型,所以可以将前者赋值给后者,但是反过来就不行。
```typescript
const a:unique symbol = Symbol();
const b:symbol = a; // 正确
const c:unique symbol = b; // 报错
```
上面示例中unique symbol 类型(变量`a`)赋值给 symbol 类型(变量`b`)是可以的,但是 symbol 类型(变量`b`)赋值给 unique symbol 类型(变量`c`)会报错。
unique symbol 类型的一个作用,就是用作属性名,这可以保证不会跟其他属性名冲突。如果要把某一个特定的 Symbol 值当作属性名,那么它的类型只能是 unique symbol不能是 symbol。
```typescript
const x:unique symbol = Symbol();
const y:symbol = Symbol();
interface Foo {
[x]: string; // 正确
[y]: string; // 报错
}
```
上面示例中,变量`y`当作属性名,但是`y`的类型是 symbol不是固定不变的值导致报错。
`unique symbol`类型也可以用作类class的属性值但只能赋值给类的`readonly static`属性。
```typescript
class C {
static readonly foo:unique symbol = Symbol();
}
```
上面示例中,静态只读属性`foo`的类型就是`unique symbol`。注意,这时`static``readonly`两个限定符缺一不可,这是为了保证这个属性是固定不变的。
## 类型推断
如果变量声明时没有给出类型TypeScript 会推断某个 Symbol 值变量的类型。
`let`命令声明的变量,推断类型为 symbol。
```typescript
// 类型为 symbol
let x = Symbol();
```
`const`命令声明的变量,推断类型为 unique symbol。
```typescript
// 类型为 unique symbol
const x = Symbol();
```
但是,`const`命令声明的变量,如果赋值为另一个 symbol 类型的变量,则推断类型为 symbol。
```typescript
let x = Symbol();
// 类型为 symbol
const y = x;
```
`let`命令声明的变量,如果赋值为另一个 unique symbol 类型的变量,则推断类型还是 symbol。
```typescript
const x = Symbol();
// 类型为 symbol
let y = x;
```

187
docs/tsc.md Normal file
View File

@@ -0,0 +1,187 @@
# tsc 命令行编译器
## 简介
tsc 是 TypeScript 官方的命令行编译器,用来检查代码,并将其编译成 JavaScript 代码。
tsc 默认使用当前目录下的配置文件`tsconfig.json`,但也可以接受独立的命令行参数。
如果命令行指定了所要编译的文件,那么 tsc 会忽略`tsconfig.json``files`属性。
它的基本用法如下。
```bash
# 按照 tsconfig.json 编译
$ tsc
# 只编译 index.ts
$ tsc index.ts
# 编译 src 目录的所有 .ts 文件
$ tsc src/*.ts
# 指定编译配置文件
$ tsc --project tsconfig.production.json
# 只类型生明文件,不编译出 JS 文件
$ tsc index.js --declaration --emitDeclarationOnly
# 多个 TS 文件编译成单个 JS 文件
$ tsc app.ts util.ts --target esnext --outfile index.js
```
## 命令行参数
tsc 的命令行参数,大部分与 tsconfig.json 的属性一一对应。
下面只是简单罗列主要的一些参数详细解释可以参考《tsconfig.json 配置文件》一章。
`--all`:输出所有可用的参数。
`--allowJs`:允许 TS 脚本加载 JS 模块,编译时将 JS 一起拷贝到输出目录。
`--allowUnreachableCode`:如果 TS 脚本有不可能运行到的代码,不报错。
`--allowUnusedLabels`:如果 TS 脚本有没有用到的标签,不报错。
`--alwaysStrict`:总是在编译产物的头部添加`use strict`
`--baseUrl`:指定非相对的模块的基准 URL。
`--build`:启用增量编译。
`--checkJs`:对 JS 脚本进行类型检查。
`--declaration`:为 TS 脚本生成一个类型生成文件。
`--declarationDir`:指定生成的类型声明文件的目录。
`--declarationMap`:为`.d.ts`文件生成 SourceMap 文件。
`--diagnostics`:构建后输出编译性能信息。
`--emitBOM`:在编译输出的 UTF-8 文件头部加上 BOM 标志。
`--emitDeclarationOnly`:只编译输出类型声明文件,不输出 JS 文件。
`--esModuleInterop`:更容易使用 import 命令加载 CommonJS 模块。
`--exactOptionalPropertyTypes`:不允许将可选属性设置为`undefined`
`--experimentalDecorators`:支持早期的装饰器语法。
`--explainFiles`:输出进行编译的文件信息。
`--forceConsistentCasingInFileNames`:文件名大小写敏感,默认打开。
`--help`:输出帮助信息。
`--importHelpers`:从外部库(比如 tslib输入辅助函数。
`--incremental`:启用增量构建。
`--init`:在当前目录创建一个全新的`tsconfig.json`文件,里面是预设的设置。
`--inlineSourceMap`SourceMap 信息嵌入 JS 文件,而不是生成独立的`.js.map`文件。
`--inlineSources`:将 TypeScript 源码作为 SourceMap 嵌入编译出来的 JS 文件。
`--isolatedModules`:确保每个模块能够独立编译,不依赖其他输入的模块。
`--jsx`:设置如何处理 JSX 文件。
`--lib`:设置目标环境需要哪些内置库的类型描述。
`--listEmittedFiles`:编译后输出编译产物的文件名。
`--listFiles`:编译过程中,列出读取的文件名。
`--listFilesOnly`:列出编译所要处理的文件,然后停止编译。
`--locale`:指定编译时输出的语言,不影响编译结果。
`--mapRoot`:指定 SourceMap 文件的位置。
`--module`:指定编译生成的模块格式。
`--moduleResolution`:指定如何根据模块名找到模块的位置。
`--moduleSuffixes`:指定模块文件的后缀名。
`--newLine`:指定编译产物的换行符,可以设为`crlf`或者`lf`
`--noEmit`:不生成编译产物,只进行类型检查。
`--noEmitHelpers`:不在编译产物中加入辅助函数。
`--noEmitOnError`:一旦报错,就停止编译,没有编译产物。
`--noFallthroughCasesInSwitch`Switch 结构的`case`分支必须有终止语句(比如`break`)。
`--noImplicitAny`:类型推断只要为`any`类型就报错。
`--noImplicitReturns`:函数内部没有显式返回语句(比如`return`)就报错。
`--noImplicitThis`:如果`this`关键字是`any`类型,就报错。
`--noImplicitUseStrict`:编译产生的 JS 文件头部不添加`use strict`语句。
`--noResolve`:不进行模块定位,除非该模块是由命令行传入。
`--noUnusedLocals`:如果有未使用的局部变量就报错。
`--noUnusedParameters`:如果有未使用的函数参数就报错。
`--outDir`:指定编译产物的存放目录。
`--outFile`:所有编译产物打包成一个指定文件。
`--preserveConstEnums`:不将`const enum`结构在生成的代码中,替换成常量。
`--preserveWatchOutput` watch 模式下不清屏。
`--pretty`:美化显示编译时的终端输出。这是默认值,但是可以关闭`--pretty false`
`--project`(或者`-p`):指定编译配置文件,或者该文件所在的目录。
`--removeComments`:编译结果中移除代码注释。
`--resolveJsonModule`:允许加载 JSON 文件。
`--rootDir`:指定加载文件所在的根目录,该目录里面的目录结构会被复制到输出目录。
`--rootDirs`:允许模块定位时,多个目录被当成一个虚拟目录。
`--skipDefaultLibCheck`:跳过 TypeScript 内置类型声明文件的类型检查。
`--skipLibCheck`:跳过`.d.ts`类型声明文件的类型检查。这样可以加快编译速度。
`--showConfig`:终端输出编译配置信息,而不进行配置。
`--sourcemap`:为编译产生的 JS 文件生成 SourceMap 文件(.map 文件)。
`--sourceRoot`:指定 SourceMap 文件里面的 TypeScript 源码根目录位置。
`--strict`:打开 TypeScript 严格检查模式。
`--strictBindCallApply`bind, call、apply 这三个函数的类型,匹配原始函数。
`--strictFunctionTypes`:如果函数 B 的参数是函数 A 参数的子类型,那么函数 B 不能替代函数 A。
`--strictNullChecks`:对`null``undefined`进行严格类型检查。
`--strictPropertyInitialization`:类的属性必须进行初始值,但是允许在构造函数里面赋值。
`--suppressExcessPropertyErrors`:关闭对象字面量的多余参数的报错。
`--target`:指定编译出来的 JS 代码的版本TypeScirpt 还会在编译时自动加入对应的库类型声明文件。
`--traceResolution`编译时在终端输出模块解析moduleResolution的具体步骤。
`--typeRoots`:设置类型模块所在的目录,替代默认的`node_modules/@types`
`--types`:设置`node_modules/@types`目录下需要包括在编译之中的类型模块。
`--version`:终端输出 tsc 的版本号。
`--watch`(或者`-w`):进入观察模式,只要文件有修改,就会自动重新编译。

896
docs/tsconfig.json.md Normal file
View File

@@ -0,0 +1,896 @@
# tsconfig.json
## 简介
`tsconfig.json`是 TypeScript 项目的配置文件,放在项目的根目录。反过来说,如果一个目录里面有`tsconfig.json`TypeScript 就认为这是项目的根目录。
如果项目源码是 JavaScript但是想用 TypeScript 处理,那么配置文件的名字是`jsconfig.json`,它跟`tsconfig`的写法是一样的。
`tsconfig.json`文件主要供`tsc`编译器使用,它的命令行参数`--project``-p`可以指定`tsconfig.json`的位置(目录或文件皆可)。
```bash
$ tsc -p ./dir
```
如果不指定配置文件的位置,`tsc`就会在当前目录下搜索`tsconfig.json`文件,如果不存在,就到上一级目录搜索,直到找到为止。
`tsconfig.json`文件的格式,是一个 JSON 对象,最简单的情况可以只放置一个空对象`{}`。下面是一个示例。
```json
{
"compilerOptions": {
"outDir": "./built",
"allowJs": true,
"target": "es5"
},
"include": ["./src/**/*"]
}
```
- include读入`src`目录中的所有文件。
- allowJs接受 JavaScript 文件作为输入。
- outDir输出的编译结果放在`built`目录。
- target编译产物的 JS 版本为 ECMAScript 5。
下面是它的一些基本属性。
- include指定包含哪些文件夹或文件
- exclude排除哪些文件夹或文件。
```typescript
{
"include":[
"./folder"
],
"exclude":[
"./folder/**/*.spec.ts",
"./folder/someSubFolder"
]
}
```
- files指定编译哪些文件
```typescript
{
"files":[
"./some/file.ts"
]
}
```
在运行tsc命令时若没有使用“--project”或“-p”编译选项那么编译器将在tsc命令的运行目录下查找是否存在文件名为“tsconfig.json”的配置文件。若存在“tsconfig.json”配置文件则使用该配置文件来编译工程若不存在则继续在父级目录下查找“tsconfig.json”配置文件直到搜索到系统根目录为止如果最终也未能找到一个可用的“tsconfig.json”配置文件那么就会停止编译工程。
TypeScript提供了一个“--init”编译选项在命令行上运行tsc命令并使用“--init”编译选项会初始化一个“tsconfig.json”配置文件。
```bash
$ tsc --init
```
tsc命令的运行结果是在当前目录下新生成了一个“tsconfig.json”配置文件里面有一些默认配置。
你也可以使用其他项目的 tsconfig.json 文件。 @tsconfig/, such as `@tsconfig/recommended` or @tsconfig/node16.
```bash
npm install --save-dev @tsconfig/deno
# or
yarn add --dev @tsconfig/deno
```
安装以后,就可以在`tsconfig.json`里面引用这个设置。
```json
{
"extends": "@tsconfig/deno/tsconfig.json"
}
```
@tsconfig 包含的完整 tsconfig 文件目录可以查看 https://github.com/tsconfig/bases/tree/main/bases。
## exclude
`exclude`属性是一个数组,必须与`include`属性一起使用,用来从编译列表中去除指定的文件。它也支持使用与`include`属性相同的通配符。
```typescript
{
"include": ["**/*"],
"exclude": ["**/*.spec.ts"]
}
```
## extends
`tsconfig.json`可以继承另一个`tsconfig.json`文件的配置。
如果一个项目有多个配置,可以把共同的配置写成`tsconfig.base.json`,其他的配置文件继承该文件,这样便于维护和修改。
`extends`属性用来指定所要继承的配置文件。它可以是本地文件。
```json
{
"extends": "../tsconfig.base.json"
}
```
如果`extends`属性指定的路径不是以`./``../`开头,那么编译器将在`node_modules`目录下查找指定的配置文件。
`extends`属性也可以继承已发布的 npm 模块里面的 tsconfig 文件。
```json
{
"extends": "@tsconfig/node12/tsconfig.json"
}
```
`extends`指定的依赖会先加载,然后加载当前配置。如果当前配置与继承的配置有重名的属性,前者会覆盖后者。
## files
`files`指定编译的文件列表,如果其中有一个文件不存在,就会报错。
它的文件列表是一个数组,排在前面的文件先编译。
```javascript
{
"files": ["a.ts", "b.ts"]
}
```
该属性必须逐一列出文件,不支持文件匹配。如果文件较多,建议使用`include``exclude`属性。
## include
`include`属性指定所要编译的文件列表,既支持逐一列出文件,也支持通配符。文件位置相对于当前配置文件而定。
```typescript
{
"include": ["src/**/*", "tests/**/*"]
}
```
`include`属性支持三种通配符。
- `?`:指代单个字符
- `*`:指代任意字符,不含路径分隔符
- `**`:指定任意目录层级。
如果不指定文件后缀名,默认包括`.ts``.tsx``.d.ts`文件。如果打开了`allowJs`,那么还包括`.js``.jsx`
## references
`references`属性是一个数组,数组成员为对象,适合一个大项目由许多小项目构成的情况,用来设置需要引用的底层项目。
```javascript
{
"references": [
{ "path": "../pkg1" },
{ "path": "../pkg2/tsconfig.json" }
]
}
```
`references`数组成员对象的`path`属性,既可以是含有文件`tsconfig.json`的目录,也可以直接是该文件。
与此同时,引用的底层项目的`tsconfig.json`必须启用`composite`属性。
```javascript
{
"compilerOptions": {
"composite": true
}
}
```
## compileOptions
`compilerOptions`属性用来定制编译行为。
这个属性可以省略,这时编译器将使用默认设置。
```javascript
{
"compilerOptions": {
"rootDir": "./src",
"outDir": "./build",
"lib": [ "es2021", "esnext" ],
"target": "es2021",
"module": "esnext",
"esModuleInterop": true,
"moduleResolution": "Node"
}
}
```
示例
```json
{
"compilerOptions": {
"target": "es5",
"moduleResolution": "node",
"module": "esnext",
"strict": true,
"importHelpers": true
}
}
```
### allowJs
`allowJs`允许 TypeScript 项目加载 JS 脚本。编译时,也会将 JS 文件,一起拷贝到输出目录。
```json
{
"compilerOptions": {
"allowJs": true
}
}
```
### alwaysStrict
`alwaysStrict`确保脚本以 ECMAScript 严格模式进行解析,因此脚本头部不用写`"use strict"`。它的值是一个布尔值,默认为`true`
### allowSyntheticDefaultImports
`allowSyntheticDefaultImports`允许`import`命令默认加载没有`default`输出的模块。
比如,打开这个设置,就可以写`import React from "react";`,而不是`import * as React from "react";`
### allowUnreachableCode
`allowUnreachableCode`设置是否允许存在不可能执行到的代码。它的值有三种可能。
- `undefined` 默认值,编辑器显示警告。
- `true`:忽略不可能执行到的代码。
- `false`:编译器报错。
### allowUnusedLabels
`allowUnusedLabels`设置是否允许存在没有用到的代码标签label。它的值有三种可能。
- `undefined` 默认值,编辑器显示警告。
- `true`:忽略没有用到的代码标签。
- `false`:编译器报错。
### baseUrl
`baseUrl`的值为字符串,指定 TypeScript 项目的基准目录。
由于默认是以 tsconfig.json 的位置作为基准目录,所以一般情况不需要使用该属性。
```typescript
{
"compilerOptions": {
"baseUrl": "./"
}
}
```
上面示例中,`baseUrl`为当前目录`./`。那么当遇到下面的语句TypeScript 将以`./`为起点,寻找`hello/world.ts`
```typescript
import { helloWorld } from "hello/world";
```
### checkJs
`checkJS`设置对 JS 文件同样进行类型检查。打开这个属性,也会自动打开`allowJs`。它等同于在 JS 脚本的头部添加`// @ts-check`命令。
```json
{
"compilerOptions":{
"checkJs": true
}
}
```
### composite
`composite`打开某些设置,使得 TypeScript 项目可以进行增量构建,往往跟`incremental`属性配合使用。
### declaration
`declaration`设置编译时是否为每个脚本生成类型声明文件`.d.ts`
```javascript
{
"compilerOptions": {
"declaration": true
}
}
```
### declarationDir
`declarationDir`设置生成的`.d.ts`文件所在的目录。
```typescript
{
"compilerOptions": {
"declaration": true,
"declarationDir": "./types"
}
}
```
### declarationMap
`declarationMap`设置生成`.d.ts`类型声明文件的同时,还会生成对应的 Source Map 文件。
```javascript
{
"compilerOptions": {
"declaration": true,
"declarationMap": true
}
}
```
### emitBOM
`emitBOM`设置是否在编译结果的文件头添加字节顺序标志 BOM。默认值是`false`
### emitDeclarationOnly
`emitDeclarationOnly`设置编译后只生成`.d.ts`文件,不生成`.js`文件。
### esModuleInterop
`esModuleInterop`修复了一些 CommonJS 和 ES6 模块之间的兼容性问题。
如果`module`属性为`node16``nodenext`,则`esModuleInterop`默认为`true`,其他情况默认为`false`
打开这个属性,使用`import`命令加载 CommonJS 模块时TypeScript 会严格检查兼容性问题是否存在。
```typescript
import * as moment from 'moment'
moment(); // 报错
```
上面示例中,根据 ES6 规范,`import * as moment`里面的`moment`是一个对象,不能当作函数调用,所以第二行报错了。
解决方法就是改写上面的语句,将`import *`改成加载默认接口。
```typescript
import moment from 'moment'
moment(); // 不报错
```
打开`esModuleInterop`以后,如果将上面的代码编译成 CommonJS 模块格式,就会加入一些辅助函数,保证编译后的代码行为正确。
注意,打开`esModuleInterop`,将自动打开`allowSyntheticDefaultImports`
### exactOptionalPropertyTypes
`exactOptionalPropertyTypes`设置可选属性不能赋值为`undefined`
```typescript
// 打开 exactOptionalPropertyTypes
interface MyObj {
foo?: 'A' | 'B';
}
let obj:MyObj = { foo: 'A' };
obj.foo = undefined; // 报错
```
上面示例中,`foo`是可选属性,打开`exactOptionalPropertyTypes`以后,该属性就不能显式赋值为`undefined`
### forceConsistentCasingInFileNames
`forceConsistentCasingInFileNames`设置文件名是否为大小写敏感,默认为`true`
### incremental
`incremental`让 TypeScript 项目构建时产生文件`tsbuildinfo`,从而完成增量构建。
### inlineSourceMap
`inlineSourceMap`设置将 SourceMap 文件写入编译后的 JS 文件中,否则会单独生称一个`.js.map`文件。
### inlineSources
`inlineSources`设置将原始的`.ts`代码嵌入编译后的 JS 中。
它要求`sourceMap``inlineSourceMap`至少打开一个。
### isolatedModules
`isolatedModules`设置如果当前 TypeScript 脚本作为单个模块编译,是否会因为缺少其他脚本的类型信息而报错。
### jsx
`jsx`设置如何处理`.tsx`文件。它一般以下三个值。
- `preserve`:保持 jsx 语法不变,输出的文件名为 jsx。
- `react`:将`<div />`编译成`React.createElement("div")`,输出的文件名为`.js`
- `react-native`:保持 jsx 语法不变,输出的文件后缀名为`.js`
```javascript
{
"compilerOptions": {
"jsx": "preserve"
}
}
```
### lib
`lib`值是一个数组,描述项目需要加载的 TypeScript 内置类型描述文件,跟三斜线指令`/// <reference lib="" />`作用相同。
```javascript
{
"compilerOptions": {
"lib": ["dom", "es2021"]
}
}
```
TypeScript 内置的类型描述文件,主要有一些,可以参考 [TypeScript 源码](https://github.com/microsoft/TypeScript/tree/main/src/lib)。
- ES5
- ES2015
- ES6
- ES2016
- ES7
- ES2017
- ES2018
- ES2019
- ES2020
- ES2021
- ES2022
- ESNex
- DOM
- WebWorker
- ScriptHost
### listEmittedFiles
`listEmittedFiles`设置编译时在终端显示,生成了哪些文件。
```typescript
{
"compilerOptions": {
"listFiles": true
}
}
```
### listFiles
`listFiles`设置编译时在终端显示,参与本次编译的文件列表。
```javascript
{
"compilerOptions": {
"listFiles": true
}
}
```
### mapRoot
`mapRoot`指定 SourceMap 文件的位置,而不是默认的生成位置。
```typescript
{
"compilerOptions": {
"sourceMap": true,
"mapRoot": "https://my-website.com/debug/sourcemaps/"
}
}
```
### module
`module`指定编译产物的模块格式。它的默认值与`target`属性有关,如果`target``ES3``ES5`,它的默认值是`commonjs`,否则就是`ES6/ES2015`
```json
{
"compilerOptions": {
"module": "commonjs"
}
}
```
它可以取以下值none、commonjs、amd、umd、system、es6/es2015、es2020、es2022、esnext、node16、nodenext。
### moduleResolution
`moduleResolution`确定模块路径的算法,即如何查找模块。它可以取以下四种值。
- `node`:采用 Node.js 的 CommonJS 模块算法。
- `node16``nodenext`:采用 Node.js 的 ECMAScript 模块算法,从 TypeScript 4.7 开始支持。
- `classic`TypeScript 1.6 之前的算法,新项目不建议使用。
它的默认值与`module`属性有关,如果`module``AMD``UMD``System``ES6/ES2015`,默认值为`classic`;如果`module``node16``nodenext`,默认值为这两个值;其他情况下,默认值为`Node`
### moduleSuffixes
`moduleSuffixes`指定模块的后缀名。
```typescript
{
"compilerOptions": {
"moduleSuffixes": [".ios", ".native", ""]
}
}
```
上面的设置使得 TypeScript 对于语句`import * as foo from "./foo";`,会搜索以下脚本`./foo.ios.ts``./foo.native.ts``./foo.ts`
### newLine
`newLine`设置换行符为`CRLF`Windows还是`LF`Linux
### noEmit
`noEmit`设置是否产生编译结果。如果不生成TypeScript 编译就纯粹作为类型检查了。
### noEmitHelpers
`noEmitHelpers`设置在编译结果文件不插入 TypeScript 辅助函数,而是通过外部引入辅助函数来解决,比如 NPM 模块`tslib`
### noEmitOnError
`noEmitOnError`指定一旦编译报错,就不生成编译产物,默认为`false`
### noFallthroughCasesInSwitch
`noFallthroughCasesInSwitch`设置是否对没有`break`语句(或者`return``throw`语句)的 switch 分支报错,即`case`代码里面必须有终结语句(比如`break`)。
### noImplicitAny
`noImplicitAny`设置当一个表达式没有明确的类型描述、且编译器无法推断出具体类型时,是否允许将它推断为`any`类型。
它是一个布尔值,默认为`true`,即只要推断出`any`类型就报错。
### noImplicitReturns
`noImplicitReturns`设置是否要求函数任何情况下都必须返回一个值,即函数必须有`return`语句。
### noImplicitThis
`noImplicitThis`设置如果`this`被推断为`any`类型是否报错。
### noUnusedLocals
`noUnusedLocals`设置是否允许未使用的局部变量。
### noUnusedParameters
`noUnusedParameters`设置是否允许未使用的函数参数。
### outDir
`outDir`指定编译产物的存放目录。如果不指定,编译出来的`.js`文件存放在对应的`.ts`文件的相同位置。
### outFile
`outFile`设置将所有非模块的全局文件,编译在同一个文件里面。它只有在`module`属性为`None``System``AMD`时才生效,并且不能用来打包 CommonJS 或 ES6 模块。
### paths
`paths`设置模块名和模块路径的映射,也就是 TypeScript 如何导入`require``imports`语句加载的模块。
`paths`基于`baseUrl`进行加载,所以必须同时设置后者。
```typescript
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"b": ["bar/b"]
}
}
}
```
它还可以使用通配符“*”。
```typescript
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@bar/*": ["bar/*"]
}
}
}
```
### preserveConstEnums
`preserveConstEnums``const enum`结构保留下来,不替换成常量值。
```javascript
{
"compilerOptions": {
"preserveConstEnums": true
}
}
```
### pretty
`pretty`设置美化输出终端的编译信息,默认为`true`
### removeComments
`removeComments`移除 TypeScript 脚本里面的注释,默认为`false`
### resolveJsonModule
`resolveJsonModule`运行 import 命令导入 JSON 文件。
### rootDir
`rootDir`设置源码脚本所在的目录,主要跟编译后的脚本结构有关。`rootDir`对应目录下的所有脚本,会成为输出目录里面的顶层脚本。
### rootDirs
`rootDirs`把多个不同目录,合并成一个目虚拟目录,便于模块定位。
```typescript
{
"compilerOptions": {
"rootDirs": ["bar", "foo"]
}
}
```
上面示例中,`rootDirs``bar``foo`组成一个虚拟目录。
### sourceMap
`sourceMap`设置编译时是否生成 SourceMap 文件。
### sourceRoot
`sourceRoot`在 SourceMap 里面设置 TypeScript 源文件的位置。
```typescript
{
"compilerOptions": {
"sourceMap": true,
"sourceRoot": "https://my-website.com/debug/source/"
}
}
```
### strict
`strict`用来打开 TypeScript 的严格检查。它的值是一个布尔值,默认是关闭的。
```typescript
{
"compilerOptions": {
"strict": true
}
}
```
这个设置相当于同时打开以下的一系列设置。
- alwaysStrict
- strictNullChecks
- strictBindCallApply
- strictFunctionTypes
- strictPropertyInitialization
- noImplicitAny
- noImplicitThis
- useUnknownInCatchVaria
打开`strict`的时候,允许单独关闭其中一项。
```json
{
"compilerOptions": {
"strict": true,
"alwaysStrict": false
}
}
```
### strictBindCallApply
`strictBindCallApply`设置是否对函数的`call()``bind()``apply()`这三个方法进行类型检查。
如果不打开`strictBindCallApply`编译选项,编译器不会对以三个方法进行类型检查,参数类型都是`any`,传入任何参数都不会产生编译错误。
```typescript
function fn(x: string) {
return parseInt(x);
}
// strictBindCallApply:false
const n = fn.call(undefined, false);
// 以上不报错
```
### strictFunctionTypes
`strictFunctionTypes`允许对函数更严格的参数检查。具体来说,如果函数 B 的参数是函数 A 参数的子类型,那么函数 B 不能替代函数 A。
```typescript
function fn(x:string) {
console.log('Hello, ' + x.toLowerCase());
}
type StringOrNumberFunc = (ns:string|number) => void;
// 打开 strictFunctionTypes下面代码会报错
let func:StringOrNumberFunc = fn;
```
上面示例中,函数`fn()`的参数是`StringOrNumberFunc`参数的子集,因此`fn`不能替代`StringOrNumberFunc`
### strictNullChecks
`strictNullChecks`设置对`null``undefined`进行严格类型检查。如果打开`strict`属性,这一项就会自动设为`true`,否则为`false`
```bash
let value:string;
// strictNullChecks:false
// 下面语句不报错
value = null;
```
它可以理解成只要打开,就需要显式检查`null``undefined`
```typescript
function doSomething(x:string|null) {
if (x === null) {
// do nothing
} else {
console.log("Hello, " + x.toUpperCase());
}
}
```
### strictPropertyInitialization
`strictPropertyInitialization`设置类的实例属性都必须初始化,包括以下几种情况。
- 设为`undefined`类型
- 显式初始化
- 构造函数中赋值
注意,使用该属性的同时,必须打开`strictNullChecks`
```typescript
// strictPropertyInitializationtrue
class User {
// 报错,属性 username 没有初始化
username: string;
}
// 解决方法一
class User {
username = '张三';
}
// 解决方法二
class User {
username:string|undefined;
}
// 解决方法三
class User {
username:string;
constructor(username:string) {
this.username = username;
}
}
// 或者
class User {
constructor(public username:string) {}
}
// 解决方法四:赋值断言
class User {
username!:string;
constructor(username:string) {
this.initialize(username);
}
private initialize(username:string) {
this.username = username;
}
}
```
### suppressExcessPropertyErrors
`suppressExcessPropertyErrors`关闭对象字面量的多余参数的报错。
### target
`target`指定编译出来的 JavaScript 代码的 ECMAScript 版本,比如`es2021`,默认是`es3`
它可以取以下值。
- es3
- es5
- es6/es2015
- es2016
- es2017
- es2018
- es2019
- es2020
- es2021
- es2022
- esnext
注意,如果编译的目标版本过老,比如`"target": "es3"`,有些语法可能无法编译,`tsc`命令会报错。
### traceResolution
`traceResolution`设置编译时,在终端输出模块解析的具体步骤。
```typescript
{
"compilerOptions": {
"traceResolution": true
}
}
```
### typeRoots
`typeRoots`设置类型模块所在的目录,默认是`node_modules/@types`
```typescript
{
"compilerOptions": {
"typeRoots": ["./typings", "./vendor/types"]
}
}
```
### types
`types`设置`node_modules/@types`目录下需要包括在编译之中的类型模块。默认情况下,该目录下的所有类型模块,都会自动包括在编译之中。
```typescript
{
"compilerOptions": {
"types": ["node", "jest", "express"]
}
}
```
### useUnknownInCatchVariables
`useUnknownInCatchVariables`设置`catch`语句捕获的`try`抛出的返回值类型,从`any`变成`unknown`
```typescript
try {
someExternalFunction();
} catch (err) {
err; // 类型 any
}
```
上面示例中,默认情况下,`catch`语句的参数`err`类型是`any`,即可以是任何值。
打开`useUnknownInCatchVariables`以后,`err`的类型抛出的错误将是`unknown`类型。这带来的变化就是使用`err`之前,必须缩小它的类型,否则会报错。
```typescript
try {
someExternalFunction();
} catch (err) {
if (err instanceof Error) {
console.log(err.message);
}
}
```
## 参考链接
- [Strict Property Initialization in TypeScript](https://mariusschulz.com/blog/strict-property-initialization-in-typescript), Marius Schulz

262
docs/tuple.md Normal file
View File

@@ -0,0 +1,262 @@
# TypeScript 的元组类型
## 简介
元组tuple是 TypeScript 特有的数据类型JavaScript 没有这种类型。它表示成员类型可以自由设置的数组,即各个成员的类型可以不同。
元组必须明确声明每个成员的类型。
```typescript
const s:[string, string, boolean]
= ['a', 'b', true];
```
上面示例中,元组`s`的前两个成员的类型是`string`,最后一个成员的类型是`boolean`
元组类型的写法,与上一章的数组有一个重大差异。数组的成员类型写在方括号外面(`number[]`),元组的成员类型是写在方括号里面(`[number]`)。
TypeScript 的区分方法是,成员类型写在方括号里面的就是元组,写在外面的就是数组。
```typescript
let a:[number] = [1];
```
上面示例中,变量`a`是一个元组,只有一个成员,类型是`number`
使用元组时,必须明确给出类型声明(上例的`[number]`),不能省略,否则 TypeScript 会把一个值自动推断为数组。
```typescript
// a 的类型为 (number | boolean)[]
let a = [1, true];
```
上面示例中,变量`a`的值其实是一个元组,但是 TypeScript 会将其推断为一个联合类型的数组,即`a`的类型为`(number | boolean)[]`
元组成员的类型可以添加问号后缀(`?`),表示该成员是可选的。
```typescript
let a:[number, number?] = [1];
```
上面示例中,元组`a`的第二个成员就是可选的,可以省略。
注意,问号只能用于元组的尾部成员,也就是说,所有可选成员必须在必选成员之后。
```typescript
type myTuple = [
number,
number,
number?,
string?
];
```
上面示例中,元组`myTuple`的最后两个成员是可选的。也就是说,两个成员、三个成员、四个成员都有可能。
由于需要声明每个成员的类型,所以大多数情况下,元组的成员数量是有限的。从类型声明就可以明确知道,元组包含多少个成员。
如果元组的成员数量是明确的,越界的成员就会报错。
```typescript
let x:[string, string] = ['a', 'b'];
x[2] = 'c'; // 报错
```
上面示例中,变量`x`是一个只有两个成员的元组,如果对第三个成员赋值就报错了。
但是,可以使用扩展运算符(`...`),表示不限成员数量的元组。
```typescript
type NamedNums = [
string,
...number[]
];
const a:NamedNums = ['A', 1, 2];
const b:NamedNums = ['B', 1, 2, 3];
```
上面示例中,元组类型`NamedNums`的第一个成员是字符串,后面的成员使用扩展运算符来展开一个数组,从而实现了不定数量的成员。
扩展运算符用在元组的任意位置都可以,但是它后面只能是数组或元组。
```typescript
type t1 = [string, number, ...boolean[]];
type t2 = [string, ...boolean[], number];
type t3 = [...boolean[], string, number];
```
上面示例中,扩展运算符分别在元组的尾部、中部和头部。
如果不确定元组成员的类型和数量,可以写成下面这样。
```typescript
type Tuple = [...any[]];
```
上面示例中,元组`Tuple`可以放置任意数量和类型的成员。但是这样写,也就失去了使用元组和 TypeScript 的意义。
元组可以通过方括号,读取成员类型。
```typescript
type Tuple = [string, number];
type Age = Tuple[1]; // number
```
上面示例中,`Tuple[1]`返回1号位置的成员类型。
由于元组的成员都是数值索引,即索引类型都是`number`,所以可以像下面这样读取。
```typescript
type Tuple = [string, number, Date];
type TupleEl = Tuple[number]; // string|number|Date
```
上面示例中,`Tuple[number]`表示元组`Tuple`的所有数值索引的成员类型,所以返回`string|number|Date`,即这个类型有三种可能。
## 只读元组
元组也可以是只读的,不允许修改,有两种写法。
```typescript
// 写法一
type t = readonly [number, string]
// 写法二
type t = Readonly<[number, string]>
```
上面示例中,两种写法都可以得到只读元组,其中写法二是一个泛型,用到了工具类型`Readonly<T>`
跟数组一样,只读元组是元组的父类型。所以,元组可以替代只读元组,而只读元组不能替代元组。
```typescript
type t1 = readonly [number, number];
type t2 = [number, number];
const x:t2 = [1, 2];
const y:t1 = x; // 正确
x = y; // 报错
```
上面示例中,类型`t1`是只读元组,类型`t2`是普通元组。`t2`类型可以赋值给`t1`类型,反过来就会报错。
由于只读元组不能替代元组,所以会产生一些令人困惑的报错。
```typescript
function distanceFromOrigin([x, y]:[number, number]) {
return Math.sqrt(x**2 + y**2);
}
let point = [3, 4] as const;
distanceFromOrigin(point); // 报错
```
上面示例中,函数`distanceFromOrigin()`的参数是一个元组,传入只读元组就会报错,因为只读元组不能替代元组。
读者可能注意到了,上例中`[3, 4] as const`的写法,在上一章讲到,生成的是只读数组,其实生成的同时也是只读元组。因为它生成的实际上是一个只读的“值类型”`readonly [3, 4]`,把它解读成只读数组或只读元组都可以。
上面示例报错的解决方法,就是使用类型断言,详见《类型断言》一章。
```typescript
distanceFromOrigin(
point as [number, number]
)
```
## 成员数量的推断
如果没有可选成员和扩展运算符TypeScript 会推断出元组的成员数量(即元组长度)。
```typescript
function f(
point: [number, number]
) {
if (point.length === 3) { // 报错
// ...
}
}
```
上面示例会报错,原因是 TypeScript 发现元组`point`的长度是`2`,不可能等于`3`,这个判断无意义。
如果包含了可选成员TypeScript 会推断出可能的成员数量。
```typescript
function f(
point:[number, number?, number?]
) {
if (point.length === 4) { // 报错
// ...
}
}
```
上面示例会报错,原因是 TypeScript 发现`point.length`的类型是`1|2|3`,不可能等于`4`
如果使用了扩展运算符TypeScript 就无法推断出成员数量。
```typescript
const myTuple:[...string[]]
= ['a', 'b', 'c'];
if (myTuple.length === 4) { // 正确
// ...
}
```
上面示例中,`myTuple`只有三个成员,但是 TypeScript 推断不出它的成员数量,因为它把`myTuple`当成数组看待,而数组的成员数量是不确定的。
一旦扩展运算符使得元组的成员数量无法推断TypeScript 内部就会把该元组当成数组处理。
## 扩展运算符与成员数量
扩展运算符(`...`)将数组(注意,不是元组)转换成一个逗号分隔的序列,这时 TypeScript 会认为这个序列的成员数量是不确定的,因为数组的成员数量是不确定的。
这导致如果函数调用时,使用扩展运算符传入函数参数,可能发生参数数量与数组长度不匹配的报错。
```typescript
const arr = [1, 2];
function add(x:number, y:number){
// ...
}
add(...arr) // 报错
```
上面示例会报错,原因是函数`add()`只能接受两个参数,但是传入的是`...arr`TypeScript 认为转换后的参数个数是不确定的。
有些函数可以接受任意数量的参数,这时使用扩展运算符就不会报错。
```typescript
const arr = [1, 2, 3];
console.log(...arr) // 正确
```
上面示例中,`console.log()`可以接受任意数量的参数,所以传入`...arr`就不会报错。
解决这个问题的一个方法,就是把成员数量不确定的数组,写成成员数量确定的元组,再使用扩展运算符。
```typescript
const arr:[number, number] = [1, 2];
function add(x:number, y:number){
// ...
}
add(...arr) // 正确
```
上面示例中,`arr`是一个拥有两个成员的元组,所以 TypeScript 能够确定`...arr`可以匹配函数`add()`的参数数量,就不会报错了。
另一种写法是使用`as const`断言。
```typescript
const arr = [1, 2] as const;
```
上面这种写法也可以,因为 TypeScript 会认为`arr`的类型是`readonly [1, 2]`,这是一个只读的值类型,可以当作数组,也可以当作元组。

302
docs/type-operations.md Normal file
View File

@@ -0,0 +1,302 @@
# 类型运算
## 运算律
改变成员类型的顺序不影响联合类型的结果类型。
```typescript
type T0 = string | number;
type T1 = number | string;
```
对部分类型成员使用分组运算符不影响联合类型的结果类型。
```typescript
type T0 = (boolean | string) | number;
type T1 = boolean | (string | number);
```
联合类型的成员类型可以进行化简。假设有联合类型“U = T0 | T1”如果T1是T0的子类型那么可以将类型成员T1从联合类型U中消去。最后联合类型U的结果类型为“U = T0”。例如有联合类型“boolean | true | false”。其中true类型和false类型是boolean类型的子类型因此可以将true类型和false类型从联合类型中消去。最终联合类型“boolean | true | false”的结果类型为boolean类型。
```typescript
type T0 = boolean | true | false;
// 所以T0等同于 T1
type T1 = boolean;
```
### 优先级
`&`的优先级高于`|`
```typescript
A & B | C & D
// 该类型等同于如下类型:
(A & B) | (C & D)
```
分配律
```typescript
A & (B | C)
// 等同于
(A & B) | (A & C)
```
一个稍微复杂的类型等式。
```typescript
(A | B) & (C | D) A & C | A & D | B & C | B & D
```
```typescript
T = (string | 0) & (number | 'a');
T = (string & number) | (string & 'a') | (0 & number) | (0 & 'a');
T = never | 'a' | 0 | never;
T = 'a' | 0;
```
```typescript
function extend<T extends object, U extends object>(first: T, second: U): T & U {
const result = <T & U>{};
for (let id in first) {
(<T>result)[id] = first[id];
}
for (let id in second) {
if (!result.hasOwnProperty(id)) {
(<U>result)[id] = second[id];
}
}
return result;
}
const x = extend({ a: 'hello' }, { b: 42 });
```
## never 类型
never 可以视为空集。
```typescript
type NeverIntersection = never & string; // Type: never
type NeverUnion = never | string; // Type: string
```
很适合在交叉类型中用作过滤。
```typescript
type OnlyStrings<T> = T extends string ? T : never;
type RedOrBlue = OnlyStrings<"red" | "blue" | 0 | false>;
// Equivalent to: "red" | "blue"
```
范例https://www.typescriptlang.org/play#example/conditional-types
## unknown 类型
在联合类型中unknown吸收所有类型。这意味着如果任何组成类型是unknown则联合类型的计算结果为unknown。
```typescript
// In an intersection everything absorbs unknown
type T00 = unknown & null; // null
type T01 = unknown & undefined; // undefined
type T02 = unknown & null & undefined; // null & undefined (which becomes never)
type T03 = unknown & string; // string
type T04 = unknown & string[]; // string[]
type T05 = unknown & unknown; // unknown
type T06 = unknown & any; // any
// In a union an unknown absorbs everything
type T10 = unknown | null; // unknown
type T11 = unknown | undefined; // unknown
type T12 = unknown | null | undefined; // unknown
type T13 = unknown | string; // unknown
type T14 = unknown | string[]; // unknown
type T15 = unknown | unknown; // unknown
type T16 = unknown | any; // any
// Type variable and unknown in union and intersection
type T20<T> = T & {}; // T & {}
type T21<T> = T | {}; // T | {}
type T22<T> = T & unknown; // T
type T23<T> = T | unknown; // unknown
// unknown in conditional types
type T30<T> = unknown extends T ? true : false; // Deferred
type T31<T> = T extends unknown ? true : false; // Deferred (so it distributes)
type T32<T> = never extends T ? true : false; // true
type T33<T> = T extends never ? true : false; // Deferred
```
```typescript
type UnionType1 = unknown | null; // unknown
type UnionType2 = unknown | undefined; // unknown
type UnionType3 = unknown | string; // unknown
type UnionType4 = unknown | number[]; // unknown
```
该规则的一个例外是any。如果至少有一种构成类型是any则联合类型的计算结果为any
```typescript
type UnionType5 = unknown | any; // any
```
在交叉类型中每种类型都吸收unknown. 这意味着与任何类型相交unknown不会改变结果类型
```typescript
type IntersectionType1 = unknown & null; // null
type IntersectionType2 = unknown & undefined; // undefined
type IntersectionType3 = unknown & string; // string
type IntersectionType4 = unknown & number[]; // number[]
type IntersectionType5 = unknown & any; // any
```
除非使用`as`断言,首先缩小类型`unknown`类型的范围,然后才可以用于其他类型。
```typescript
const value: unknown = "Hello World";
const someString: string = value as string;
const otherString = someString.toUpperCase(); // "HELLO WORLD"
```
## 联合类型
如果类型是多个值的联合,甚至可以产生插值的效果。
```typescript
type EmailLocaleIDs = "welcome_email" | "email_heading";
type FooterLocaleIDs = "footer_title" | "footer_sendoff";
// 等同于 type AllLocaleIDs = "welcome_email_id" | "email_heading_id" | "footer_title_id" | "footer_sendoff_id"
type AllLocaleIDs = `${EmailLocaleIDs | FooterLocaleIDs}_id`;
```
## 交叉类型
```typescript
type Brightness = "dark" | "light";
type Color = "blue" | "red";
type BrightnessAndColor = `${Brightness}-${Color}`;
// Equivalent to: "dark-red" | "light-red" | "dark-blue" | "light-blue"
```
如果交叉类型中存在多个相同的成员类型,那么相同的成员类型将被合并为单一成员类型。
```typescript
type T0 = boolean;
type T1 = boolean & boolean;
type T2 = boolean & boolean & boolean;
```
上面示例中T0、T1和T2都表示同一种类型boolean。
改变成员类型的顺序不影响交叉类型的结果类型。
```typescript
interface Clickable {
click(): void;
}
interface Focusable {
focus(): void;
}
type T0 = Clickable & Focusable;
type T1 = Focusable & Clickable;
```
注意,当交叉类型涉及调用签名重载或构造签名重载时便失去了“加法交换律”的性质。因为交叉类型中成员类型的顺序将决定重载签名的顺序,进而将影响重载签名的解析顺序。
```typescript
interface Clickable {
register(x: any): void;
}
interface Focusable {
register(x: string): boolean;
}
type ClickableAndFocusable = Clickable & Focusable;
type FocusableAndFocusable = Focusable & Clickable;
function foo(
clickFocus: ClickableAndFocusable,
focusClick: FocusableAndFocusable
) {
let a: void = clickFocus.register('foo');
let b: boolean = focusClick.register('foo');
}
```
此例第8行和第9行使用不同的成员类型顺序定义了两个交叉类型。第15行调用“register()”方法的返回值类型为void说明在ClickableAndFocusable类型中Clickable接口中定义的“register()”方法具有更高的优先级。第16行调用“register()”方法的返回值类型为boolean说明FocusableAndFocusable类型中Focusable接口中定义的“register()”方法具有更高的优先级。此例也说明了调用签名重载的顺序与交叉类型中成员类型的定义顺序是一致的。
对部分类型成员使用分组运算符不影响交叉类型的结果类型。
```typescript
interface Clickable {
click(): void;
}
interface Focusable {
focus(): void;
}
interface Scrollable {
scroll(): void;
}
type T0 = (Clickable & Focusable) & Scrollable;
type T1 = Clickable & (Focusable & Scrollable);
```
上面示例的T0和T1类型是同一种类型。
```typescript
type Combined = { a: number } & { b: string };
type Conflicting = { a: number } & { a: string };
```
只要交叉类型I中任意一个成员类型包含了属性签名M那么交叉类型I也包含属性签名M。
```typescript
interface A {
a: boolean;
}
interface B {
b: string;
}
// 交叉类型如下
{
a: boolean;
b: string;
}
```
若交叉类型的属性签名M在所有成员类型中都是可选属性那么该属性签名在交叉类型中也是可选属性。否则属性签名M是一个必选属性。
```typescript
interface A {
x: boolean;
y?: string;
}
interface B {
x?: boolean;
y?: string;
}
// 交叉类型如下
{
x: boolean;
y?: string;
}
```
## 字符串操作类型
- Uppercase: Converts a string literal type to uppercase.
• Lowercase: Converts a string literal type to lowercase.
• Capitalize: Converts a first character of string literal type to uppercase.
• Uncapitalize: Converts a first character of string literal type to lowercase.
```typescript
type FormalGreeting = Capitalize<"hello.">; // Type: "Hello."
```

730
docs/types.md Normal file
View File

@@ -0,0 +1,730 @@
# TypeScript 的类型系统
TypeScript 首先继承了 JavaScript 的类型,在这个基础上,发展出自己的类型系统。
## 基本类型
### 概述
JavaScript 语言(注意,不是 TypeScript将值分成8种类型。
- boolean
- string
- number
- bigint
- symbol
- object
- undefined
- null
TypeScript 继承了 JavaScript 的类型设计以上8种类型可以看作 TypeScript 的基本类型。
注意,上面所有类型的名称都是小写字母,首字母大写的`Number``String``Boolean`等都是 JavaScript 语言内置的对象,而不是类型名称。
另外undefined 和 null 既可以作为值,也可以作为类型,取决于在哪里使用它们。
这8种基本类型作为 TypeScript 类型系统的基础,组合起来就可以形成复杂类型。
以下是它们的简单介绍。
### boolean 类型
`boolean`类型只有`true``false`两个布尔值。
```typescript
const x:boolean = true;
const y:boolean = false;
```
上面示例中,变量`x``y`就属于 boolean 类型。
### string 类型
`string`类型包含所有字符串。
```typescript
const x:string = 'hello';
const y:string = `${x} world`;
```
上面示例中,普通字符串和模板字符串都属于 string 类型。
### number 类型
`number`类型包含所有整数和浮点数。
```typescript
const x:number = 123;
const y:number = 3.14;
const z:number = 0xffff;
```
上面示例中,整数、浮点数和非十进制数都属于 number 类型。
### bigint 类型
bigint 类型包含所有的大整数。
```typescript
const x:bigint = 123n;
const y:bigint = 0xffffn;
```
上面示例中,变量`x``y`就属于 bigint 类型。
bigint 与 number 类型不兼容。
```typescript
const x:bigint = 123; // 报错
const y:bigint = 3.14; // 报错
```
上面示例中,`bigint`类型赋值为整数和小数,都会报错。
注意bigint 类型是 ES2020 标准引入的。如果使用这个类型TypeScript 编译的目标 JavaScript 版本不能低于 ES2020编译参数`--target`不低于`es2020`)。
### symbol 类型
symbol 类型包含所有的 Symbol 值。
```typescript
const x:symbol = Symbol();
```
上面示例中,`Symbol()`函数的返回值就是 symbol 类型。
symbol 类型的详细介绍参见《Symbol》一章。
### object 类型
根据 JavaScript 的设计object 类型包含了所有对象、数组和函数。
```typescript
const x:object = { foo: 123 };
const y:object = [1, 2, 3];
const z:object = (n:number) => n + 1;
```
上面示例中,对象、数组、函数都属于 object 类型。
### undefined 类型null 类型
undefined 和 null 是两种独立类型,它们各自都只有一个值。
undefined 类型只包含一个值`undefined`,表示未定义(即还给出定义,以后可能会有定义)。
```typescript
let x:undefined = undefined;
```
上面示例中,变量`x`就属于 undefined 类型。两个`undefined`里面,第一个是类型,第二个是值。
null 类型也只包含一个值`null`,表示为空(即此处没有值)。
```typescript
const x:null = null;
```
上面示例中,变量`x`就属于 null 类型。
注意,如果没有声明类型的变量,被赋值为`undefined``null`,它们的类型会被推断为`any`
```typescript
let a = undefined; // any
const b = undefined; // any
let c = null; // any
const d = null; // any
```
如果希望避免这种情况,则需要打开编译选项`strictNullChecks`
```typescript
// 打开编译设置 strictNullChecks
let a = undefined; // undefined
const b = undefined; // undefined
let c = null; // null
const d = null; // null
```
上面示例中,打开编译设置`strictNullChecks`以后,赋值为`undefined`的变量会被推断为`undefined`类型,赋值为`null`的变量会被推断为`null`类型。
## 包装对象类型
### 包装对象的概念
JavaScript 的8种类型之中`undefined``null`其实是两个特殊值,`object`属于复合类型剩下的五种属于原始类型primitive value代表最基本的、不可再分的值。
- boolean
- string
- number
- bigint
- symbol
上面这五种原始类型的值都有对应的包装对象wrapper object。所谓“包装对象”指的是这些值在需要时会自动产生的对象。
```javascript
'hello'.charAt(1) // 'e'
```
上面示例中,字符串`hello`执行了`charAt()`方法。但是,在 JavaScript 语言中,只有对象才有方法,原始类型的值本身没有方法。这行代码之所以可以运行,就是因为在调用方法时,字符串会自动转为包装对象,`charAt()`方法其实是定义在包装对象上。
这样的设计大大方便了字符串处理,省去了将原始类型的值手动转成对象实例的麻烦。
五种包装对象之中symbol 类型和 bigint 类型无法直接获取它们的包装对象(即`Symbol()``BigInt()`不能作为构造函数使用),但是剩下三种可以。
- `Boolean()`
- `String()`
- `Number()`
以上三个构造函数,执行后可以直接获取某个原始类型值的包装对象。
```javascript
const s = new String('hello');
typeof s // 'object'
s.charAt(1) // 'e'
```
上面示例中,`s`就是字符串`hello`的包装对象,`typeof`运算符返回`object`,不是`string`,但是本质上它还是字符串,可以使用所有的字符串方法。
注意,`String()`只有当作构造函数使用时(即带有`new`命令调用),才会返回包装对象。如果当作普通函数使用(不带有`new`命令),返回就是一个普通字符串。其他两个构造函数`Number()``Boolean()`也是如此。
### 包装对象类型与字面量类型
由于包装对象的存在,导致每一种原始类型都有包装对象和字面量两种情况。
```javascript
'hello' // 字面量
new String('hello') // 包装对象
```
上面示例中,第一行是字面量,第二行是包装对象,它们都是字符串。
为了区分这两种情况TypeScript 对五种原始类型分别提供了大写和小写两种类型。
- Boolean 和 boolean
- String 和 string
- Number 和 number
- BigInt 和 bigint
- Symbol 和 symbol
其中,大写类型同时包含包装对象和字面量两种情况,小写类型只包含字面量,不包含包装对象。
```typescript
const s1:String = 'hello'; // 正确
const s2:String = new String('hello'); // 正确
const s3:string = 'hello'; // 正确
const s4:string = new String('hello'); // 报错
```
上面示例中,`String`类型可以赋值为字符串的字面量,也可以赋值为包装对象。但是,`string`类型只能赋值为字面量,赋值为包装对象就会报错。
建议只使用小写类型不使用大写类型。因为绝大部分使用原始类型的场合都是使用字面量不使用包装对象。而且TypeScript 把很多内置方法的参数,定义成小写类型,使用大写类型会报错。
```typescript
const n1:number = 1;
const n2:Number = 1;
Math.abs(n1) // 1
Math.abs(n2) // 报错
```
上面示例中,`Math.abs()`方法的参数类型被定义成小写的`number`,传入大写的`Number`类型就会报错。
上一小节说过,`Symbol()``BigInt()`这两个函数不能当作构造函数使用,所以没有办法直接获得 symbol 类型和 bigint 类型的包装对象,因此`Symbol``BigInt`这两个类型虽然存在,但是完全没有使用的理由。
## Object 类型与 object 类型
TypeScript 的对象类型也有大写`Object`和小写`object`两种。
### Object 类型
大写的`Object`类型代表 JavaScript 语言里面的广义对象。所有可以转成对象的值,都是`Object`类型,这囊括了几乎所有的值。
```typescript
let obj:Object;
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
```
上面示例中,原始类型值、对象、数组、函数都是合法的`Object`类型。
事实上,除了`undefined``null`这两个值不能转为对象,其他任何值都可以赋值给`Object`类型。
```typescript
let obj:Object;
obj = undefined; // 报错
obj = null; // 报错
```
上面示例中,`undefined``null`赋值给`Object`类型,就会报错。
另外,空对象`{}``Object`类型的简写形式,所以使用`Object`时常常用空对象代替。
```typescript
let obj:{};
obj = true;
obj = 'hi';
obj = 1;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
```
上面示例中,变量`obj`的类型是空对象`{}`,就代表`Object`类型。
显然,无所不包的`Object`类型既不符合直觉,也不方便使用。
### object 类型
小写的`object`类型代表 JavaScript 里面的狭义对象,即可以用字面量表示的对象,只包含对象、数组和函数,不包括原始类型的值。
```typescript
let obj:object;
obj = { foo: 123 };
obj = [1, 2];
obj = (a:number) => a + 1;
obj = true; // 报错
obj = 'hi'; // 报错
obj = 1; // 报错
```
上面示例中,`object`类型不包含原始类型值,只包含对象、数组和函数。
大多数时候,我们使用对象类型,只希望包含真正的对象,不希望包含原始类型。所以,建议总是使用小写类型`object`,不使用大写类型`Object`
注意,无论是大写的`Object`类型,还是小写的`object`类型,都只能表示 JavaScript 内置的原型对象(即`Object.prototype`),用户自定义的属性都不存在于这两个类型之中。
```typescript
const o1:Object = { foo: 0 };
const o2:object = { foo: 0 };
o1.toString() // 正确
o1.foo // 报错
o2.toString() // 正确
o2.foo // 报错
```
上面示例中,`toString()`是对象的原生方法,可以正确访问。`foo`是自定义属性,访问就会报错。如何描述对象的自定义属性,详见《对象类型》一章。
## undefined 和 null 的特殊性
`undefined``null`既是值,又是类型。
作为值,它们有一个特殊的地方:任何其他类型的变量都可以赋值为`undefined``null`
```typescript
let age:number = 24;
age = null; // 正确
age = undefined; // 正确
```
上面代码中,变量`age`的类型是`number`,但是赋值为`null``undefined`并不报错。
这并不是因为`undefined``null`包含在`number`类型里面,而是故意这样设计,任何类型的变量都可以赋值为`undefined``null`,以便跟 JavaScript 的行为保持一致。
JavaScript 的行为是,变量如果等于`undefined`就表示还没有赋值,如果等于`null`就表示值为空。所以TypeScript 就允许了任何类型的变量都可以赋值为这两个值。
但是有时候,这并不是开发者想要的行为,也不利于发挥类型系统的优势。
```typescript
const obj:object = undefined;
obj.toString() // 错误,但能通过编译
```
上面示例中,变量`obj`等于`undefined`,编译不会报错。但是,实际执行时,调用`obj.toString()`就报错了,因为`undefined`不是对象,没有这个方法。
为了避免这种情况及早发现错误TypeScript 提供了一个编译选项`--strictNullChecks`。只要打开这个选项,`undefined``null`就不能赋值给其他类型的变量(除了`any`类型和`unknown`类型)。
下面是 tsc 命令打开这个编译选项的例子。
```typescript
// tsc --strictNullChecks app.ts
let age:number = 24;
age = null; // 报错
age = undefined; // 报错
```
上面示例中,打开`--strictNullChecks`以后,`number`类型的变量`age`就不能赋值为`undefined``null`
这个选项在配置文件`tsconfig.json`的写法如下。
```json
{
"compilerOptions": {
"strictNullChecks": true
// ...
}
}
```
打开`--strictNullChecks`以后,`undefined``null`这两种值也不能互相赋值了。
```typescript
// 打开 --strictNullChecks
let x:undefined = null; // 报错
let y:null = undefined; // 报错
```
上面示例中,`undefined`类型的变量赋值为`null`,或者`null`类型的变量赋值为`undefind`,都会报错。
总之,打开`--strictNullChecks`以后,`undefined``null`只能赋值给自身,或者`any`类型和`unknown`类型的变量。
```typescript
let x:any = undefined;
let y:unknown = null;
```
## 值类型
TypeScript 规定,单个值也是一种类型,称为“值类型”。
```typescript
let x:'hello';
x = 'hello'; // 正确
x = 'world'; // 报错
```
上面示例中,变量`x`的类型是字符串`hello`,导致它只能赋值为这个字符串,赋值为其他字符串就会报错。
TypeScript 推断类型时,遇到`const`命令声明的变量,如果代码里面没有注明类型,就会推断该变量是值类型。
```typescript
// x 的类型是 “https”
const x = 'https';
// y 的类型是 string
const y:string = 'https';
```
上面示例中,变量`x``const`命令声明的TypeScript 就会推断它的类型是值`https`,而不是`string`类型。
这样推断是合理的,因为`const`命令声明的变量,一旦声明就不能改变,相当于常量。值类型就意味着不能赋为其他值。
注意,`const`命令声明的变量,如果赋值为对象,并不会推断为值类型。
```typescript
// x 的类型是 { foo: number }
const x = { foo: 1 };
```
上面示例中,变量`x`没有被推断为值类型,而是推断属性`foo`的类型是`number`。这是因为 JavaScript 里面,`const`变量赋值为对象时,属性值是可以改变的。
值类型可能会出现一些很奇怪的报错。
```typescript
const x:5 = 4 + 1; // 报错
```
上面示例中,等号左侧的类型是数值`5`,等号右侧`4 + 1`的类型TypeScript 推测为`number`。由于`5``number`的子类型,子类型不能赋值为父类型的值,所以报错了。
但是,反过来是可以的,父类型可以赋值为子类型的值。
```typescript
let x:5 = 5;
let y:number = 4 + 1;
x = y; // 报错
y = x; // 正确
```
上面示例中,子类型`x`不能赋值为父类型`y`,但是反过来是可以的。
如果一定要让子类型可以赋值为父类型的值,就要用到类型断言(详见《类型断言》一章)。
```typescript
const x:5 = (4 + 1) as 5; // 正确
```
上面示例中,在`4 + 1`后面加上`as 5`,就是告诉编译器,可以把`4 + 1`的类型视为值类型`5`,这样就不会报错了。
只包含单个值的值类型,用处不大。实际开发中,往往将多个值结合,作为联合类型使用。
## 联合类型
联合类型union types指的是多个类型组成的一个新类型使用符号`|`表示。
联合类型`A|B`表示,任何一个类型只要属于`A``B`,就属于联合类型`A|B`
```typescript
let x:string|number;
x = 123; // 正确
x = 'abc'; // 正确
```
上面示例中,变量`x`就是联合类型`string|number`,表示它的值既可以是字符串,也可以是数值。
联合类型可以与值类型相结合,表示一个变量的值有若干种可能。
```typescript
let setting:true|false;
let gender:'male'|'female';
let rainbowColor:'赤'|'橙'|'黄'|'绿'|'青'|'蓝'|'紫';
```
上面的示例都是由值类型组成的联合类型,非常清晰地表达了变量的取值范围。其中,`true|false`其实就是布尔类型`boolean`
前面提到,打开编译选项`--strictNullChecks`后,其他类型的变量不能赋值为`undefined``null`。这时,如果某个变量确实可能包含空值,就可以采用联合类型的写法。
```typescript
let name:string|null;
name = 'John';
name = null;
```
上面示例中,变量`name`的值可以是字符串,也可以是`null`
联合类型的第一个成员前面,也可以加上竖杠`|`,这样便于多行书写。
```typescript
let x:
| 'one'
| 'two'
| 'three'
| 'four';
```
上面示例中,联合类型的第一个成员`one`前面,也可以加上竖杠。
如果一个变量有多种类型读取该变量时往往需要进行“类型缩小”type narrowing区分该值到底属于哪一种类型然后再进一步理。
```typescript
function printId(
id:number|string
) {
console.log(id.toUpperCase()); // 报错
}
```
上面示例中,参数变量`id`可能是数值,也可能是字符串,这时直接对这个变量调用`toUpperCase()`方法会报错,因为这个方法只存在于字符串,不存在于数值。
解决方法就是对参数`id`做一下类型缩小,确定它的类型以后再进行处理。这在 TypeScript 里面叫做“类型缩小”。
```typescript
function printId(
id:number|string
) {
if (typeof id === 'string') {
console.log(id.toUpperCase());
} else {
console.log(id);
}
}
```
上面示例中,函数体内部会判断一下变量`id`的类型,如果是字符串,就对其执行`toUpperCase()`方法。
“类型缩小”是 TypeScript 处理联合类型的标准方法凡是遇到可能为多种类型的场合都需要先缩小类型再进行处理。实际上联合类型本身可以看成是一种“类型放大”type widening处理时就需要“类型缩小”type narrowing
下面是“类型缩小”的另一个例子。
```typescript
function getPort(
scheme: 'http'|'https'
) {
switch (scheme) {
case 'http':
return 80;
case 'https':
return 443;
}
}
```
上面示例中,函数体内部对参数变量`scheme`进行类型缩小,根据不同的值类型,返回不同的结果。
## 交叉类型
交叉类型intersection types指的多个类型组成的一个新类型使用符号`&`表示。
交叉类型`A&B`表示,任何一个类型必须同时属于`A``B`,才属于交叉类型`A&B`,即交叉类型同时满足`A``B`的特征。
```typescript
let x:number&string;
```
上面示例中,变量`x`同时是数值和字符串,这当然是不可能的,所以 TypeScript 会认为`x`的类型实际是`never`
交叉类型的主要用途是表示对象的合成。
```typescript
let obj:
{ foo: string } &
{ bar: string };
obj = {
foo: 'hello',
bar: 'world'
};
```
上面示例中,变量`obj`同时具有属性`foo`和属性`bar`
交叉类型常常用来为对象类型添加新属性。
```typescript
type A = { foo: number };
type B = A & { bar: number };
```
上面示例中,类型`B`是一个交叉类型,用来在`A`的基础上增加了属性`bar`
## type 命令
`type`命令用来定义一个类型的别名。
```typescript
type Age = number;
let age:Age = 55;
```
上面示例中,`type`命令为`number`类型生成一个别名`Age`。这样就能像使用`number`一样,使用`Age`当作类型。
别名可以让类型的名字变得更有意义,也能增加代码的可读性,还可以使复杂类型用起来更方便,便于以后修改变量的类型。
别名不允许有重名。
```typescript
type Color = 'red';
type Color = 'blue'; // 报错
```
上面示例中,同一个别名`Color`声明了两次,就报错了。
别名的作用域是块级作用域。这意味着,代码块内部定义的别名,影响不到外部。
```typescript
type Color = 'red';
if (Math.random() < 0.5) {
type Color = 'blue';
}
```
上面示例中,`if`代码块内部的类型别名`Color`,跟外部的`Color`是不一样的。
别名支持使用表达式,也可以在定义一个别名时,使用另一个别名,即别名允许嵌套。
```typescript
type World = "world";
type Greeting = `hello ${World}`;
```
上面示例中,别名`Greeting`使用了模板字符串,读取另一个别名`World`
`type`命令属于类型相关的代码,编译成 JavaScript 的时候,会被全部删除。
## typeof 运算符
JavaScript 语言中typeof 运算符是一个一元运算符,返回一个字符串,代表操作数的类型。
```javascript
typeof 'foo'; // 'string'
```
上面示例中,`typeof`运算符返回字符串`foo`的类型是`string`
注意,这时 typeof 的操作数是一个值。
JavaScript 里面,`typeof`运算符只可能返回八种结果,而且都是字符串。
```javascript
typeof undefined; // "undefined"
typeof true; // "boolean"
typeof 1337; // "number"
typeof "foo"; // "string"
typeof {}; // "object"
typeof parseInt; // "function"
typeof Symbol(); // "symbol"
typeof 127n // "bigint"
```
上面示例是`typeof`运算符在 JavaScript 语言里面,可能返回的八种结果。
TypeScript 将`typeof`运算符移植到了类型运算,它的操作数依然是一个值,但是返回的不是字符串,而是该值的 TypeScript 类型。
```typescript
const a = { x: 0 };
type T0 = typeof a; // { x: number }
type T1 = typeof a.x; // number
```
上面示例中,`typeof a`表示返回变量`a`的 TypeScript 类型(`{ x: number }`)。同理,`typeof a.x`返回的是属性`x`的类型(`number`)。
这种用法的`typeof`返回的是 TypeScript 类型,所以只能用在类型运算之中(即跟类型相关的代码之中),不能用在值运算。
也就是说,同一段代码可能存在两种`typeof`运算符,一种用在值相关的 JavaScript 代码部分,另一种用在类型相关的 TypeScript 代码部分。
```typescript
let a = 1;
let b:typeof a;
if (typeof a === 'number') {
b = a;
}
```
上面示例中,用到了两个`typeof`,第一个是类型运算,第二个是值运算。它们是不一样的,不要混淆。
JavaScript 的 typeof 遵守 JavaScript 规则TypeScript 的 typeof 遵守 TypeScript 规则。它们的一个重要区别在于,编译后,前者会保留,后者会被全部删除。
上例的代码编译结果如下。
```typescript
let a = 1;
let b;
if (typeof a === 'number') {
b = a;
}
```
上面示例中,只保留了原始代码的第二个 typeof删除了第一个 typeof。
由于编译时不会进行 JavaScript 的值运算所以TypeScript 规定typeof 的参数只能是标识符,不能是需要运算的表达式。
```typescript
type T = typeof Date(); // 报错
```
上面示例会报错,原因是 typeof 的参数不能是一个值的运算式,而`Date()`需要运算才知道结果。
typeof 是一个很重要的 TypeScript 运算符,有些场合不知道某个变量`foo`的类型,这时使用`typeof foo`就可以获得它的类型。
## 块级类型声明
TypeScript 支持块级类型声明,即类型可以声明在代码块(用大括号表示)里面,并且只在当前代码块有效。
```typescript
if (true) {
type T = number;
let v:T = 5;
} else {
type T = string;
let v:T = 'hello';
}
```
上面示例中,存在两个代码块,其中分别有一个类型`T`的声明。这两个声明都只在自己的代码块内部有效,在代码块外部无效。

743
docs/utility.md Normal file
View File

@@ -0,0 +1,743 @@
# TypeScript 类型工具
TypeScript 提供了一些内置的类型工具,用来方便地处理各种类型,以及生成新的类型。
TypeScript 内置了17个类型工具可以直接使用。
## `Awaited<Type>`
`Awaited<Type>`用来取出 Promise 的返回值类型,适合用在描述`then()`方法和 await 命令的参数类型。
```typescript
// string
type A = Awaited<Promise<string>>;
```
上面示例中,`Awaited<Type>`会返回 Promise 的返回值类型string
它也可以返回多重 Promise 的返回值类型。
```typescript
// number
type B = Awaited<Promise<Promise<number>>>;
```
如果它的类型参数不是 Promise 类型,那么就会原样返回。
```typescript
// number | boolean
type C = Awaited<boolean | Promise<number>>;
```
上面示例中,类型参数是一个联合类型,其中的`boolean`会原样返回,所以最终返回的是`number|boolean`
`Awaited<Type>`的实现如下。
```typescript
type Awaited<T> =
T extends null | undefined ? T :
T extends object & {
then(
onfulfilled: infer F,
...args: infer _
): any;
} ? F extends (
value: infer V,
...args: infer _
) => any ? Awaited<...> : never:
T;
```
## `ConstructorParameters<Type>`
`ConstructorParameters<Type>`提取构造方法`Type`的参数类型,组成一个元组类型返回。
```typescript
type T1 = ConstructorParameters<
new (x: string, y: number) => object
>; // [x: string, y: number]
type T2 = ConstructorParameters<
new (x?: string) => object
>; // [x?: string | undefined]
```
它可以返回一些内置构造方法的参数类型。
```typescript
type T1 = ConstructorParameters<
ErrorConstructor
>; // [message?: string]
type T2 = ConstructorParameters<
FunctionConstructor
>; // string[]
type T3 = ConstructorParameters<
RegExpConstructor
>; // [pattern:string|RegExp, flags?:string]
```
如果参数类型不是构造方法,就会报错。
```typescript
type T1 = ConstructorParameters<string>; // 报错
type T2 = ConstructorParameters<Function>; // 报错
```
`any`类型和`never`类型是两个特殊值,分别返回`unknown[]``never`
```typescript
type T1 = ConstructorParameters<any>; // unknown[]
type T2 = ConstructorParameters<never>; // never
```
`ConstructorParameters<Type>`的实现如下。
```typescript
type ConstructorParameters<
T extends abstract new (...args: any) => any
> = T extends abstract new (...args: infer P)
=> any ? P : never
```
## `Exclude<UnionType, ExcludedMembers>`
`Exclude<UnionType, ExcludedMembers>`用来从联合类型`UnionType`里面,删除某些类型`ExcludedMembers`,组成一个新的类型返回。
```typescript
type T1 = Exclude<'a'|'b'|'c', 'a'>; // 'b'|'c'
type T2 = Exclude<'a'|'b'|'c', 'a'|'b'>; // 'c'
type T3 = Exclude<string|(() => void), Function>; // string
```
`Exclude<UnionType, ExcludedMembers>`的实现如下。
```typescript
type Exclude<T, U> = T extends U ? never : T;
```
上面代码中,等号右边的部分,表示先判断`T`是否兼容`U`,如果是的就返回`never`类型,否则返回当前类型`T`。由于`never`类型是任何其他类型的子类型,它跟其他类型组成联合类型时,可以直接将`never`类型从联合类型中“消掉”,因此`Exclude<T, U>`就相当于删除兼容的类型,剩下不兼容的类型。
## `Extract<Type, Union>`
`Extract<UnionType, Union>`用来从联合类型`UnionType`之中,提取指定类型`Union`,组成一个新类型返回。它与`Exclude<T, U>`正好相反。
```typescript
type T1 = Extract<'a'|'b'|'c', 'a'>; // 'a'
type T2 = Extract<'a'|'b'|'c', 'a'|'b'>; // 'a'|'b'
type T3 = Extract<'a'|'b'|'c', 'a'|'d'>; // 'a'
```
如果参数类型`Union`不包含在联合类型`UnionType`之中,则返回`never`类型。
```typescript
type T = Extract<string|number, boolean>; // never
```
`Extract<UnionType, Union>`的实现如下。
```typescript
type Extract<T, U> = T extends U ? T : never;
```
## `InstanceType<Type>`
`InstanceType<Type>`的参数`Type`是一个构造函数,返回该构造函数对应的实例类型。
```typescript
type T = InstanceType<
new () => object
>; // object
```
上面示例中,类型参数是一个构造函数`new () => object`,返回值是该构造函数的实例类型(`object`)。
由于 Class 作为类型,代表实例类型。要获取它的构造方法,必须把它当成值,然后用`typeof`运算符获取它的构造方法类型。
```typescript
class C {
x = 0;
y = 0;
}
type T = InstanceType<typeof C>; // C
```
上面示例中,`typeof C`是`C`的构造方法类型,然后 InstanceType 就能获得实例类型,即`C`本身。
如果类型参数不是构造方法,就会报错。
```typescript
type T1 = InstanceType<string>; // 报错
type T2 = InstanceType<Function>; // 报错
```
如果类型参数是`any`或`never`两个特殊值,分别返回`any`和`never`。
```typescript
type T1 = InstanceType<any>; // any
type T2 = InstanceType<never>; // never
```
`InstanceType<Type>`的实现如下。
```typescript
type InstanceType<
T extends abstract new (...args:any) => any
> = T extends abstract new (...args: any) => infer R ? R :
any;
```
## `NonNullable<Type>`
`NonNullable<Type>`用来从联合类型`Type`删除`null`类型和`undefined`类型,组成一个新类型返回,也就是返回`Type`的非空类型版本。
```typescript
// string|number
type T1 = NonNullable<string|number|undefined>;
// string[]
type T2 = NonNullable<string[]|null|undefined>;
```
`NonNullable<Type>`的实现如下。
```typescript
type NonNullable<T> = T & {}
```
上面代码中,`T & {}`等同于求`T & Object`的交叉类型。由于 TypeScript 的非空值都属于`Object`的子类型,所以会返回自身;而`null`和`undefined`不属于`Object`,会返回`never`类型。
## `Omit<Type, Keys>`
`Omit<Type, Keys>`用来从对象类型`Type`中,删除指定的属性`Keys`,组成一个新的对象类型返回。
```typescript
interface A {
x: number;
y: number;
}
type T1 = Omit<A, 'x'>; // { y: number }
type T2 = Omit<A, 'y'>; // { x: number }
type T3 = Omit<A, 'x' | 'y'>; // { }
```
上面示例中,`Omit<Type, Keys>`从对象类型`A`里面删除指定属性,返回剩下的属性。
指定删除的键名`Keys`可以是对象类型`Type`中不存在的属性,但必须兼容`string|number|symbol`。
```typescript
interface A {
x: number;
y: number;
}
type T = Omit<A, 'z'>; // { x: number; y: number }
```
上面示例中,对象类型`A`中不存在属性`z`,所以就原样返回了。
`Omit<Type, Keys>`的实现如下。
```typescript
type Omit<T, K extends keyof any>
= Pick<T, Exclude<keyof T, K>>;
```
## `OmitThisParameter<Type>`
`OmitThisParameter<Type>`从函数类型中移除 this 参数。
```typescript
function toHex(this: Number) {
return this.toString(16);
}
type T = OmitThisParameter<typeof toHex>; // () => string
```
如果函数没有 this 参数,则返回原始函数类型。
`OmitThisParameter<Type>`的实现如下。
```typescript
type OmitThisParameter<T> =
unknown extends ThisParameterType<T> ? T :
T extends (...args: infer A) => infer R ?
(...args: A) => R : T;
```
## `Parameters<Type>`
`Parameters<Type>`从函数类型`Type`里面提取参数类型,组成一个元组返回。
```typescript
type T1 = Parameters<() => string>; // []
type T2 = Parameters<(s:string) => void>; // [s:string]
type T3 = Parameters<<T>(arg: T) => T>; // [arg: unknown]
type T4 = Parameters<
(x:{ a: number; b: string }) => void
>; // [x: { a: number, b: string }]
type T5 = Parameters<
(a:number, b:number) => number
>; // [a:number, b:number]
```
上面示例中,`Parameters<Type>`的返回值会包括函数的参数名,这一点需要注意。
如果参数类型`Type`不是带有参数的函数形式,会报错。
```typescript
// 报错
type T1 = Parameters<string>;
// 报错
type T2 = Parameters<Function>;
```
由于`any`和`never`是两个特殊值,会返回`unknown[]`和`never`。
```typescript
type T1 = Parameters<any>; // unknown[]
type T2 = Parameters<never>; // never
```
`Parameters<Type>`主要用于从外部模块提供的函数类型中,获取参数类型。
```typescript
interface SecretName {
first: string;
last: string;
}
interface SecretSanta {
name: SecretName;
gift: string;
}
export function getGift(
name: SecretName,
gift: string
): SecretSanta {
// ...
}
```
上面示例中,模块只输出了函数`getGift()`,没有输出参数`SecretName`和返回值`SecretSanta`。这时就可以通过`Parameters<T>`和`ReturnType<T>`拿到这两个接口类型。
```typescript
type ParaT = Parameters<typeof getGift>[0]; // SecretName
type ReturnT = ReturnType<typeof getGift>; // SecretSanta
```
`Parameters<Type>`的实现如下。
```typescript
type Parameters<T extends (...args: any) => any> =
T extends (...args: infer P)
=> any ? P : never
```
## `Partial<Type>`
`Partial<Type>`返回一个新类型,将参数类型`Type`的所有属性变为可选属性。
```typescript
interface A {
x: number;
y: number;
}
type T = Partial<A>; // { x?: number; y?: number; }
```
`Partial<Type>`的实现如下。
```typescript
type Partial<T> = {
[P in keyof T]?: T[P];
};
```
## `Pick<Type, Keys>`
`Pick<Type, Keys>`返回一个新的对象类型,第一个参数`Type`是一个对象类型,第二个参数`Keys`是`Type`里面被选定的键名。
```typescript
interface A {
x: number;
y: number;
}
type T1 = Pick<A, 'x'>; // { x: number }
type T2 = Pick<A, 'y'>; // { y: number }
type T3 = Pick<A, 'x'|'y'>; // { x: number; y: number }
```
上面示例中,`Pick<Type, Keys>`会从对象类型`A`里面挑出指定的键名,组成一个新的对象类型。
指定的键名`Keys`必须是对象键名`Type`里面已经存在的键名,否则会报错。
```typescript
interface A {
x: number;
y: number;
}
type T = Pick<A, 'z'>; // 报错
```
上面示例中,对象类型`A`不存在键名`z`,所以报错了。
`Pick<Type, Keys>`的实现如下。
```typescript
type Pick<T, K extends keyof T> = {
[P in K]: T[P];
};
```
## `Readonly<Type>`
`Readonly<Type>`返回一个新类型,将参数类型`Type`的所有属性变为只读属性。
```typescript
interface A {
x: number;
y?: number;
}
// { readonly x: number; readonly y?: number; }
type T = Readonly<A>;
```
上面示例中,`y`是可选属性,`Readonly<Type>`不会改变这一点,只会让`y`变成只读。
`Readonly<Type>`的实现如下。
```typescript
type Readonly<T> = {
readonly [P in keyof T]: T[P];
};
```
我们可以自定义类型工具`Mutable<Type>`,将参数类型的所有属性变成可变属性。
```typescript
type Mutable<T> = {
-readonly [P in keyof T]: T[P];
};
```
上面代码中,`-readonly`表示去除属性的只读标志。
相应地,`+readonly`就表示增加只读标志,等同于`readonly`。因此,`ReadOnly<Type>`的实现也可以写成下面这样。
```typescript
type Readonly<T> = {
+readonly [P in keyof T]: T[P];
};
```
`Readonly<Type>`可以与`Partial<Type>`结合使用,将所有属性变成只读的可选属性。
```typescript
interface Person {
name: string;
age: number;
}
const worker: Readonly<Partial<Person>>
= { name: '张三' };
worker.name = '李四'; // 报错
```
## `Record<Keys, Type>`
`Record<Keys, Type>`返回一个对象类型,参数`Keys`用作键名,参数`Type`用作键值类型。
```typescript
// { a: number }
type T = Record<'a', number>;
```
上面示例中,`Record<Keys, Type>`的第一个参数`a`,用作对象的键名,第二个参数`number`是`a`的键值类型。
参数`Keys`可以是联合类型,这时会依次展开为多个键。
```typescript
// { a: number, b: number }
type T = Record<'a'|'b', number>;
```
上面示例中,第一个参数是联合类型`'a'|'b'`,展开成两个键名`a`和`b`。
如果参数`Type`是联合类型,就表明键值是联合类型。
```typescript
// { a: number|string }
type T = Record<'a', number|string>;
```
参数`Keys`的类型必须兼容`string|number|symbol`,否则不能用作键名,会报错。
`Record<Keys, Type>`的实现如下。
```typescript
type Record<K extends string|number|symbol, T>
= { [P in K]: T; }
```
## `Required<Type>`
`Required<Type>`返回一个新类型,将参数类型`Type`的所有属性变为必选属性。它与`Partial<Type>`的作用正好相反。
```typescript
interface A {
x?: number;
y: number;
}
type T = Required<A>; // { x: number; y: number; }
```
`Required<Type>`的实现如下。
```typescript
type Required<T> = {
[P in keyof T]-?: T[P];
};
```
上面代码中,符号`-?`表示去除可选属性的“问号”,使其变成必选属性。
相对应地,符号`+?`表示增加可选属性的“问号”,等同于`?`。因此,前面的`Partial<Type>`的定义也可以写成下面这样。
```typescript
type Partial<T> = {
[P in keyof T]+?: T[P];
};
```
## `ReadonlyArray<Type>`
`ReadonlyArray<Type>`用来生成一个只读数组类型,类型参数`Type`表示数组成员的类型。
```typescript
const values: ReadonlyArray<string>
= ['a', 'b', 'c'];
values[0] = 'x'; // 报错
values.push('x'); // 报错
values.pop(); // 报错
values.splice(1, 1); // 报错
```
上面示例中,变量`values`的类型是一个只读数组,所以修改成员会报错,并且那些会修改源数组的方法`push()`、`pop()`、`splice()`等都不存在。
`ReadonlyArray<Type>`的实现如下。
```typescript
interface ReadonlyArray<T> {
readonly length: number;
readonly [n: number]: T;
// ...
}
```
## `ReturnType<Type>`
`ReturnType<Type>`提取函数类型`Type`的返回值类型,作为一个新类型返回。
```typescript
type T1 = ReturnType<() => string>; // string
type T2 = ReturnType<() => {
a: string; b: number
}>; // { a: string; b: number }
type T3 = ReturnType<(s:string) => void>; // void
```
如果参数类型是泛型函数,返回值取决于泛型类型。如果泛型不带有限制条件,就会返回`unknown`。
```typescript
type T1 = ReturnType<<T>() => T>; // unknown
type T2 = ReturnType<
<T extends U, U extends number[]>() => T
>; // number[]
```
如果类型不是函数,会报错。
```typescript
type T1 = ReturnType<boolean>; // 报错
type T2 = ReturnType<Function>; // 报错
```
`any`和`never`是两个特殊值,分别返回`any`和`never`。
```typescript
type T1 = ReturnType<any>; // any
type T2 = ReturnType<never>; // never
```
`ReturnType<Type>`的实现如下。
```typescript
type ReturnType<
T extends (...args: any) => any
> =
T extends (...args: any) => infer R ? R : any;
```
## `ThisParameterType<Type>`
`ThisParameterType<Type>`提取函数类型中`this`参数的类型。
```typescript
function toHex(this: Number) {
return this.toString(16);
}
type T = ThisParameterType<typeof toHex>; // number
```
如果函数没有`this`参数,则返回`unknown`。
`ThisParameterType<Type>`的实现如下。
```typescript
type ThisParameterType<T> =
T extends (
this: infer U,
...args: never
) => any ? U : unknown
```
## `ThisType<Type>`
`ThisType<Type>`不返回类型,只用来跟其他类型组成交叉类型,用来提示 TypeScript 其他类型里面的`this`的类型。
```typescript
interface HelperThisValue {
logError: (error:string) => void;
}
let helperFunctions:
{ [name: string]: Function } &
ThisType<HelperThisValue>
= {
hello: function() {
this.logError("Error: Something wrong!"); // 正确
this.update(); // 报错
}
}
```
上面示例中,变量`helperFunctions`的类型是一个正常的对象类型与`ThisType<HelperThisValue>`组成的交叉类型。
这里的`ThisType`的作用是提示 TypeScript变量`helperFunctions`的`this`应该满足`HelperThisValue`的条件。所以,`this.logError()`可以正确调用,而`this.update()`会报错,因为`HelperThisValue`里面没有这个方法。
注意,使用这个类型工具时,必须打开`noImplicitThis`设置。
下面是另一个例子。
```typescript
let obj: ThisType<{ x: number }> &
{ getX: () => number };
obj = {
getX() {
return this.x + this.y; // 报错
},
};
```
上面示例中,`getX()`里面的`this.y`会报错,因为根据`ThisType<{ x: number }>`,这个对象的`this`不包含属性`y`。
`ThisType<Type>`的实现就是一个空接口。
```typescript
interface ThisType<T> { }
```
## 字符串类型工具
TypeScript 内置了四个字符串类型工具,专门用来操作字符串类型。这四个工具类型都定义在 TypeScript 自带的`.d.ts`文件里面。
它们的实现都是在底层调用 JavaScript 引擎提供 JavaScript 字符操作方法。
### `Uppercase<StringType>`
`Uppercase<StringType>`将字符串类型的每个字符转为大写。
```typescript
type A = 'hello';
// "HELLO"
type B = Uppercase<A>;
```
上面示例中,`Uppercase<T>`将 hello 转为 HELLO。
### `Lowercase<StringType>`
`Lowercase<StringType>`将字符串的每个字符转为小写。
```typescript
type A = 'HELLO';
// "hello"
type B = Lowercase<A>;
```
上面示例中,`Lowercase<T>`将 HELLO 转为 hello。
### `Capitalize<StringType>`
`Capitalize<StringType>`将字符串的第一个字符转为大写。
```typescript
type A = 'hello';
// "Hello"
type B = Capitalize<A>;
```
上面示例中,`Capitalize<T>`将 hello 转为 Hello。
### `Uncapitalize<StringType>`
`Uncapitalize<StringType>` 将字符串的第一个字符转为小写。
```typescript
type A = 'HELLO';
// "hELLO"
type B = Uncapitalize<A>;
```
上面示例中,`Uncapitalize<T>`将 HELLO 转为 hELLO。
## 参考链接
- [What is TypeScript's ThisType used for?](https://stackoverflow.com/questions/55029032/what-is-typescripts-thistype-used-for)