├── .github └── workflows │ └── wangdoc.yml ├── .gitignore ├── README.md ├── chapters.yml ├── docs ├── any.md ├── array.md ├── assert.md ├── basic.md ├── class.md ├── comment.md ├── d.ts.md ├── declare.md ├── decorator-legacy.md ├── decorator.md ├── enum.md ├── es6.md ├── function.md ├── generics.md ├── interface.md ├── intro.md ├── mapping.md ├── module.md ├── namespace.md ├── narrowing.md ├── npm.md ├── object.md ├── operator.md ├── react.md ├── symbol.md ├── tsc.md ├── tsconfig.json.md ├── tuple.md ├── type-operations.md ├── types.md └── utility.md ├── loppo.yml └── package.json /.github/workflows/wangdoc.yml: -------------------------------------------------------------------------------- 1 | name: TypeScript tutorial CI 2 | on: 3 | push: 4 | branches: 5 | - main 6 | 7 | jobs: 8 | page-generator: 9 | name: Generating pages 10 | runs-on: ubuntu-latest 11 | steps: 12 | - name: Checkout 13 | uses: actions/checkout@v4 14 | with: 15 | persist-credentials: false 16 | - name: Setup Node.js 17 | uses: actions/setup-node@v4 18 | with: 19 | node-version: 'latest' 20 | - name: Install dependencies 21 | run: npm install 22 | - name: Build pages 23 | run: npm run build 24 | - name: Deploy to website 25 | uses: JamesIves/github-pages-deploy-action@v4 26 | with: 27 | git-config-name: wangdoc-bot 28 | git-config-email: yifeng.ruan@gmail.com 29 | repository-name: wangdoc/website 30 | token: ${{ secrets.WANGDOC_BOT_TOKEN }} 31 | branch: master # The branch the action should deploy to. 32 | folder: dist # The folder the action should deploy. 33 | target-folder: dist/typescript 34 | clean: true # Automatically remove deleted files from the deploy branch 35 | commit-message: update from TypeScript tutorial 36 | 37 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | dist/ 3 | package-lock.json 4 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | TypeScript 开源教程,介绍基本概念和用法,面向初学者。 2 | 3 | ![](https://cdn.beekka.com/blogimg/asset/202308/bg2023080705.webp) 4 | 5 | -------------------------------------------------------------------------------- /chapters.yml: -------------------------------------------------------------------------------- 1 | - intro.md: 简介 2 | - basic.md: 基本用法 3 | - any.md: any 类型 4 | - types.md: 类型系统 5 | - array.md: 数组 6 | - tuple.md: 元组 7 | - symbol.md: symbol 类型 8 | - function.md: 函数 9 | - object.md: 对象 10 | - interface.md: interface 11 | - class.md: 类 12 | - generics.md: 泛型 13 | - enum.md: Enum 类型 14 | - assert.md: 类型断言 15 | - module.md: 模块 16 | - namespace.md: namespace 17 | - decorator.md: 装饰器 18 | - decorator-legacy.md: 装饰器(旧语法) 19 | - declare.md: declare 关键字 20 | - d.ts.md: d.ts 类型声明文件 21 | - operator.md: 类型运算符 22 | - mapping.md: 类型映射 23 | - utility.md: 类型工具 24 | - comment.md: 注释指令 25 | - tsconfig.json.md: tsconfig.json 文件 26 | - tsc.md: tsc 命令 27 | 28 | -------------------------------------------------------------------------------- /docs/any.md: -------------------------------------------------------------------------------- 1 | # any 类型,unknown 类型,never 类型 2 | 3 | 本章介绍 TypeScript 的三种特殊类型,它们可以作为学习 TypeScript 类型系统的起点。 4 | 5 | ## any 类型 6 | 7 | ### 基本含义 8 | 9 | any 类型表示没有任何限制,该类型的变量可以赋予任意类型的值。 10 | 11 | ```typescript 12 | let x:any; 13 | 14 | x = 1; // 正确 15 | x = 'foo'; // 正确 16 | x = true; // 正确 17 | ``` 18 | 19 | 上面示例中,变量`x`的类型是`any`,就可以被赋值为任意类型的值。 20 | 21 | 变量类型一旦设为`any`,TypeScript 实际上会关闭这个变量的类型检查。即使有明显的类型错误,只要句法正确,都不会报错。 22 | 23 | ```typescript 24 | let x:any = 'hello'; 25 | 26 | x(1) // 不报错 27 | x.foo = 100; // 不报错 28 | ``` 29 | 30 | 上面示例中,变量`x`的值是一个字符串,但是把它当作函数调用,或者当作对象读取任意属性,TypeScript 编译时都不报错。原因就是`x`的类型是`any`,TypeScript 不对其进行类型检查。 31 | 32 | 由于这个原因,应该尽量避免使用`any`类型,否则就失去了使用 TypeScript 的意义。 33 | 34 | 实际开发中,`any`类型主要适用以下两个场合。 35 | 36 | (1)出于特殊原因,需要关闭某些变量的类型检查,就可以把该变量的类型设为`any`。 37 | 38 | (2)为了适配以前老的 JavaScript 项目,让代码快速迁移到 TypeScript,可以把变量类型设为`any`。有些年代很久的大型 JavaScript 项目,尤其是别人的代码,很难为每一行适配正确的类型,这时你为那些类型复杂的变量加上`any`,TypeScript 编译时就不会报错。 39 | 40 | 总之,TypeScript 认为,只要开发者使用了`any`类型,就表示开发者想要自己来处理这些代码,所以就不对`any`类型进行任何限制,怎么使用都可以。 41 | 42 | 从集合论的角度看,`any`类型可以看成是所有其他类型的全集,包含了一切可能的类型。TypeScript 将这种类型称为“顶层类型”(top type),意为涵盖了所有下层。 43 | 44 | ### 类型推断问题 45 | 46 | 对于开发者没有指定类型、TypeScript 必须自己推断类型的那些变量,如果无法推断出类型,TypeScript 就会认为该变量的类型是`any`。 47 | 48 | ```typescript 49 | function add(x, y) { 50 | return x + y; 51 | } 52 | 53 | add(1, [1, 2, 3]) // 不报错 54 | ``` 55 | 56 | 上面示例中,函数`add()`的参数变量`x`和`y`,都没有足够的信息,TypeScript 无法推断出它们的类型,就会认为这两个变量和函数返回值的类型都是`any`。以至于后面就不再对函数`add()`进行类型检查了,怎么用都可以。 57 | 58 | 这显然是很糟糕的情况,所以对于那些类型不明显的变量,一定要显式声明类型,防止被推断为`any`。 59 | 60 | TypeScript 提供了一个编译选项`noImplicitAny`,打开该选项,只要推断出`any`类型就会报错。 61 | 62 | ```bash 63 | $ tsc --noImplicitAny app.ts 64 | ``` 65 | 66 | 上面命令使用了`noImplicitAny`编译选项进行编译,这时上面的函数`add()`就会报错。 67 | 68 | 这里有一个特殊情况,即使打开了`noImplicitAny`,使用`let`和`var`命令声明变量,但不赋值也不指定类型,是不会报错的。 69 | 70 | ```typescript 71 | var x; // 不报错 72 | let y; // 不报错 73 | ``` 74 | 75 | 上面示例中,变量`x`和`y`声明时没有赋值,也没有指定类型,TypeScript 会推断它们的类型为`any`。这时即使打开了`noImplicitAny`,也不会报错。 76 | 77 | ```typescript 78 | let x; 79 | 80 | x = 123; 81 | x = { foo: 'hello' }; 82 | ``` 83 | 84 | 上面示例中,变量`x`的类型推断为`any`,但是不报错,可以顺利通过编译。 85 | 86 | 由于这个原因,建议使用`let`和`var`声明变量时,如果不赋值,就一定要显式声明类型,否则可能存在安全隐患。 87 | 88 | `const`命令没有这个问题,因为 JavaScript 语言规定`const`声明变量时,必须同时进行初始化(赋值)。 89 | 90 | ```typescript 91 | const x; // 报错 92 | ``` 93 | 94 | 上面示例中,`const`命令声明的`x`是不能改变值的,声明时必须同时赋值,否则报错,所以它不存在类型推断为`any`的问题。 95 | 96 | ### 污染问题 97 | 98 | `any`类型除了关闭类型检查,还有一个很大的问题,就是它会“污染”其他变量。它可以赋值给其他任何类型的变量(因为没有类型检查),导致其他变量出错。 99 | 100 | ```typescript 101 | let x:any = 'hello'; 102 | let y:number; 103 | 104 | y = x; // 不报错 105 | 106 | y * 123 // 不报错 107 | y.toFixed() // 不报错 108 | ``` 109 | 110 | 上面示例中,变量`x`的类型是`any`,实际的值是一个字符串。变量`y`的类型是`number`,表示这是一个数值变量,但是它被赋值为`x`,这时并不会报错。然后,变量`y`继续进行各种数值运算,TypeScript 也检查不出错误,问题就这样留到运行时才会暴露。 111 | 112 | 污染其他具有正确类型的变量,把错误留到运行时,这就是不宜使用`any`类型的另一个主要原因。 113 | 114 | ## unknown 类型 115 | 116 | 为了解决`any`类型“污染”其他变量的问题,TypeScript 3.0 引入了[`unknown`类型](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-0.html#new-unknown-top-type)。它与`any`含义相同,表示类型不确定,可能是任意类型,但是它的使用有一些限制,不像`any`那样自由,可以视为严格版的`any`。 117 | 118 | `unknown`跟`any`的相似之处,在于所有类型的值都可以分配给`unknown`类型。 119 | 120 | ```typescript 121 | let x:unknown; 122 | 123 | x = true; // 正确 124 | x = 42; // 正确 125 | x = 'Hello World'; // 正确 126 | ``` 127 | 128 | 上面示例中,变量`x`的类型是`unknown`,可以赋值为各种类型的值。这与`any`的行为一致。 129 | 130 | `unknown`类型跟`any`类型的不同之处在于,它不能直接使用。主要有以下几个限制。 131 | 132 | 首先,`unknown`类型的变量,不能直接赋值给其他类型的变量(除了`any`类型和`unknown`类型)。 133 | 134 | ```typescript 135 | let v:unknown = 123; 136 | 137 | let v1:boolean = v; // 报错 138 | let v2:number = v; // 报错 139 | ``` 140 | 141 | 上面示例中,变量`v`是`unknown`类型,赋值给`any`和`unknown`以外类型的变量都会报错,这就避免了污染问题,从而克服了`any`类型的一大缺点。 142 | 143 | 其次,不能直接调用`unknown`类型变量的方法和属性。 144 | 145 | ```typescript 146 | let v1:unknown = { foo: 123 }; 147 | v1.foo // 报错 148 | 149 | let v2:unknown = 'hello'; 150 | v2.trim() // 报错 151 | 152 | let v3:unknown = (n = 0) => n + 1; 153 | v3() // 报错 154 | ``` 155 | 156 | 上面示例中,直接调用`unknown`类型变量的属性和方法,或者直接当作函数执行,都会报错。 157 | 158 | 再次,`unknown`类型变量能够进行的运算是有限的,只能进行比较运算(运算符`==`、`===`、`!=`、`!==`、`||`、`&&`、`?`)、取反运算(运算符`!`)、`typeof`运算符和`instanceof`运算符这几种,其他运算都会报错。 159 | 160 | ```typescript 161 | let a:unknown = 1; 162 | 163 | a + 1 // 报错 164 | a === 1 // 正确 165 | ``` 166 | 167 | 上面示例中,`unknown`类型的变量`a`进行加法运算会报错,因为这是不允许的运算。但是,进行比较运算就是可以的。 168 | 169 | 那么,怎么才能使用`unknown`类型变量呢? 170 | 171 | 答案是只有经过“类型缩小”,`unknown`类型变量才可以使用。所谓“类型缩小”,就是缩小`unknown`变量的类型范围,确保不会出错。 172 | 173 | ```typescript 174 | let a:unknown = 1; 175 | 176 | if (typeof a === 'number') { 177 | let r = a + 10; // 正确 178 | } 179 | ``` 180 | 181 | 上面示例中,`unknown`类型的变量`a`经过`typeof`运算以后,能够确定实际类型是`number`,就能用于加法运算了。这就是“类型缩小”,即将一个不确定的类型缩小为更明确的类型。 182 | 183 | 下面是另一个例子。 184 | 185 | ```typescript 186 | let s:unknown = 'hello'; 187 | 188 | if (typeof s === 'string') { 189 | s.length; // 正确 190 | } 191 | ``` 192 | 193 | 上面示例中,确定变量`s`的类型为字符串以后,才能调用它的`length`属性。 194 | 195 | 这样设计的目的是,只有明确`unknown`变量的实际类型,才允许使用它,防止像`any`那样可以随意乱用,“污染”其他变量。类型缩小以后再使用,就不会报错。 196 | 197 | 总之,`unknown`可以看作是更安全的`any`。一般来说,凡是需要设为`any`类型的地方,通常都应该优先考虑设为`unknown`类型。 198 | 199 | 在集合论上,`unknown`也可以视为所有其他类型(除了`any`)的全集,所以它和`any`一样,也属于 TypeScript 的顶层类型。 200 | 201 | ## never 类型 202 | 203 | 为了保持与集合论的对应关系,以及类型运算的完整性,TypeScript 还引入了“空类型”的概念,即该类型为空,不包含任何值。 204 | 205 | 由于不存在任何属于“空类型”的值,所以该类型被称为`never`,即不可能有这样的值。 206 | 207 | ```typescript 208 | let x:never; 209 | ``` 210 | 211 | 上面示例中,变量`x`的类型是`never`,就不可能赋给它任何值,否则都会报错。 212 | 213 | `never`类型的使用场景,主要是在一些类型运算之中,保证类型运算的完整性,详见后面章节。另外,不可能返回值的函数,返回值的类型就可以写成`never`,详见《函数》一章。 214 | 215 | 如果一个变量可能有多种类型(即联合类型),通常需要使用分支处理每一种类型。这时,处理所有可能的类型之后,剩余的情况就属于`never`类型。 216 | 217 | ```typescript 218 | function fn(x:string|number) { 219 | if (typeof x === 'string') { 220 | // ... 221 | } else if (typeof x === 'number') { 222 | // ... 223 | } else { 224 | x; // never 类型 225 | } 226 | } 227 | ``` 228 | 229 | 上面示例中,参数变量`x`可能是字符串,也可能是数值,判断了这两种情况后,剩下的最后那个`else`分支里面,`x`就是`never`类型了。 230 | 231 | `never`类型的一个重要特点是,可以赋值给任意其他类型。 232 | 233 | ```typescript 234 | function f():never { 235 | throw new Error('Error'); 236 | } 237 | 238 | let v1:number = f(); // 不报错 239 | let v2:string = f(); // 不报错 240 | let v3:boolean = f(); // 不报错 241 | ``` 242 | 243 | 上面示例中,函数`f()`会抛出错误,所以返回值类型可以写成`never`,即不可能返回任何值。各种其他类型的变量都可以赋值为`f()`的运行结果(`never`类型)。 244 | 245 | 为什么`never`类型可以赋值给任意其他类型呢?这也跟集合论有关,空集是任何集合的子集。TypeScript 就相应规定,任何类型都包含了`never`类型。因此,`never`类型是任何其他类型所共有的,TypeScript 把这种情况称为“底层类型”(bottom type)。 246 | 247 | 总之,TypeScript 有两个“顶层类型”(`any`和`unknown`),但是“底层类型”只有`never`唯一一个。 248 | 249 | -------------------------------------------------------------------------------- /docs/array.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的数组类型 2 | 3 | JavaScript 数组在 TypeScript 里面分成两种类型,分别是数组(array)和元组(tuple)。 4 | 5 | 本章介绍数组,下一章介绍元组。 6 | 7 | ## 简介 8 | 9 | TypeScript 数组有一个根本特征:所有成员的类型必须相同,但是成员数量是不确定的,可以是无限数量的成员,也可以是零成员。 10 | 11 | 数组的类型有两种写法。第一种写法是在数组成员的类型后面,加上一对方括号。 12 | 13 | ```typescript 14 | let arr:number[] = [1, 2, 3]; 15 | ``` 16 | 17 | 上面示例中,数组`arr`的类型是`number[]`,其中`number`表示数组成员类型是`number`。 18 | 19 | 如果数组成员的类型比较复杂,可以写在圆括号里面。 20 | 21 | ```typescript 22 | let arr:(number|string)[]; 23 | ``` 24 | 25 | 上面示例中,数组`arr`的成员类型是`number|string`。 26 | 27 | 这个例子里面的圆括号是必须的,否则因为竖杠`|`的优先级低于`[]`,TypeScript 会把`number|string[]`理解成`number`和`string[]`的联合类型。 28 | 29 | 如果数组成员可以是任意类型,写成`any[]`。当然,这种写法是应该避免的。 30 | 31 | ```typescript 32 | let arr:any[]; 33 | ``` 34 | 35 | 数组类型的第二种写法是使用 TypeScript 内置的 Array 接口。 36 | 37 | ```typescript 38 | let arr:Array = [1, 2, 3]; 39 | ``` 40 | 41 | 上面示例中,数组`arr`的类型是`Array`,其中`number`表示成员类型是`number`。 42 | 43 | 这种写法对于成员类型比较复杂的数组,代码可读性会稍微好一些。 44 | 45 | ```typescript 46 | let arr:Array; 47 | ``` 48 | 49 | 这种写法本质上属于泛型,这里只要知道怎么写就可以了,详细解释参见《泛型》一章。另外,数组类型还有第三种写法,因为很少用到,本章就省略了,详见《interface 接口》一章。 50 | 51 | 数组类型声明了以后,成员数量是不限制的,任意数量的成员都可以,也可以是空数组。 52 | 53 | ```typescript 54 | let arr:number[]; 55 | arr = []; 56 | arr = [1]; 57 | arr = [1, 2]; 58 | arr = [1, 2, 3]; 59 | ``` 60 | 61 | 上面示例中,数组`arr`无论有多少个成员,都是正确的。 62 | 63 | 这种规定的隐藏含义就是,数组的成员是可以动态变化的。 64 | 65 | ```typescript 66 | let arr:number[] = [1, 2, 3]; 67 | 68 | arr[3] = 4; 69 | arr.length = 2; 70 | 71 | arr // [1, 2] 72 | ``` 73 | 74 | 上面示例中,数组增加成员或减少成员,都是可以的。 75 | 76 | 正是由于成员数量可以动态变化,所以 TypeScript 不会对数组边界进行检查,越界访问数组并不会报错。 77 | 78 | ```typescript 79 | let arr:number[] = [1, 2, 3]; 80 | let foo = arr[3]; // 正确 81 | ``` 82 | 83 | 上面示例中,变量`foo`的值是一个不存在的数组成员,TypeScript 并不会报错。 84 | 85 | TypeScript 允许使用方括号读取数组成员的类型。 86 | 87 | ```typescript 88 | type Names = string[]; 89 | type Name = Names[0]; // string 90 | ``` 91 | 92 | 上面示例中,类型`Names`是字符串数组,那么`Names[0]`返回的类型就是`string`。 93 | 94 | 由于数组成员的索引类型都是`number`,所以读取成员类型也可以写成下面这样。 95 | 96 | ```typescript 97 | type Names = string[]; 98 | type Name = Names[number]; // string 99 | ``` 100 | 101 | 上面示例中,`Names[number]`表示数组`Names`所有数值索引的成员类型,所以返回`string`。 102 | 103 | ## 数组的类型推断 104 | 105 | 如果数组变量没有声明类型,TypeScript 就会推断数组成员的类型。这时,推断行为会因为值的不同,而有所不同。 106 | 107 | 如果变量的初始值是空数组,那么 TypeScript 会推断数组类型是`any[]`。 108 | 109 | ```typescript 110 | // 推断为 any[] 111 | const arr = []; 112 | ``` 113 | 114 | 后面,为这个数组赋值时,TypeScript 会自动更新类型推断。 115 | 116 | ```typescript 117 | const arr = []; 118 | arr // 推断为 any[] 119 | 120 | arr.push(123); 121 | arr // 推断类型为 number[] 122 | 123 | arr.push('abc'); 124 | arr // 推断类型为 (string|number)[] 125 | ``` 126 | 127 | 上面示例中,数组变量`arr`的初始值是空数组,然后随着新成员的加入,TypeScript 会自动修改推断的数组类型。 128 | 129 | 但是,类型推断的自动更新只发生初始值为空数组的情况。如果初始值不是空数组,类型推断就不会更新。 130 | 131 | ```typescript 132 | // 推断类型为 number[] 133 | const arr = [123]; 134 | 135 | arr.push('abc'); // 报错 136 | ``` 137 | 138 | 上面示例中,数组变量`arr`的初始值是`[123]`,TypeScript 就推断成员类型为`number`。新成员如果不是这个类型,TypeScript 就会报错,而不会更新类型推断。 139 | 140 | ## 只读数组,const 断言 141 | 142 | JavaScript 规定,`const`命令声明的数组变量是可以改变成员的。 143 | 144 | ```typescript 145 | const arr = [0, 1]; 146 | arr[0] = 2; 147 | ``` 148 | 149 | 上面示例中,修改`const`命令声明的数组的成员是允许的。 150 | 151 | 但是,很多时候确实有声明为只读数组的需求,即不允许变动数组成员。 152 | 153 | TypeScript 允许声明只读数组,方法是在数组类型前面加上`readonly`关键字。 154 | 155 | ```typescript 156 | const arr:readonly number[] = [0, 1]; 157 | 158 | arr[1] = 2; // 报错 159 | arr.push(3); // 报错 160 | delete arr[0]; // 报错 161 | ``` 162 | 163 | 上面示例中,`arr`是一个只读数组,删除、修改、新增数组成员都会报错。 164 | 165 | TypeScript 将`readonly number[]`与`number[]`视为两种不一样的类型,后者是前者的子类型。 166 | 167 | 这是因为只读数组没有`pop()`、`push()`之类会改变原数组的方法,所以`number[]`的方法数量要多于`readonly number[]`,这意味着`number[]`其实是`readonly number[]`的子类型。 168 | 169 | 我们知道,子类型继承了父类型的所有特征,并加上了自己的特征,所以子类型`number[]`可以用于所有使用父类型的场合,反过来就不行。 170 | 171 | ```typescript 172 | let a1:number[] = [0, 1]; 173 | let a2:readonly number[] = a1; // 正确 174 | 175 | a1 = a2; // 报错 176 | ``` 177 | 178 | 上面示例中,子类型`number[]`可以赋值给父类型`readonly number[]`,但是反过来就会报错。 179 | 180 | 由于只读数组是数组的父类型,所以它不能代替数组。这一点很容易产生令人困惑的报错。 181 | 182 | ```typescript 183 | function getSum(s:number[]) { 184 | // ... 185 | } 186 | 187 | const arr:readonly number[] = [1, 2, 3]; 188 | 189 | getSum(arr) // 报错 190 | ``` 191 | 192 | 上面示例中,函数`getSum()`的参数`s`是一个数组,传入只读数组就会报错。原因就是只读数组是数组的父类型,父类型不能替代子类型。这个问题的解决方法是使用类型断言`getSum(arr as number[])`,详见《类型断言》一章。 193 | 194 | 注意,`readonly`关键字不能与数组的泛型写法一起使用。 195 | 196 | ```typescript 197 | // 报错 198 | const arr:readonly Array = [0, 1]; 199 | ``` 200 | 201 | 上面示例中,`readonly`与数组的泛型写法一起使用,就会报错。 202 | 203 | 实际上,TypeScript 提供了两个专门的泛型,用来生成只读数组的类型。 204 | 205 | ```typescript 206 | const a1:ReadonlyArray = [0, 1]; 207 | 208 | const a2:Readonly = [0, 1]; 209 | ``` 210 | 211 | 上面示例中,泛型`ReadonlyArray`和`Readonly`都可以用来生成只读数组类型。两者尖括号里面的写法不一样,`Readonly`的尖括号里面是整个数组(`number[]`),而`ReadonlyArray`的尖括号里面是数组成员(`number`)。 212 | 213 | 只读数组还有一种声明方法,就是使用“const 断言”。 214 | 215 | ```typescript 216 | const arr = [0, 1] as const; 217 | 218 | arr[0] = [2]; // 报错 219 | ``` 220 | 221 | 上面示例中,`as const`告诉 TypeScript,推断类型时要把变量`arr`推断为只读数组,从而使得数组成员无法改变。 222 | 223 | ## 多维数组 224 | 225 | TypeScript 使用`T[][]`的形式,表示二维数组,`T`是最底层数组成员的类型。 226 | 227 | ```typescript 228 | var multi:number[][] = 229 | [[1,2,3], [23,24,25]]; 230 | ``` 231 | 232 | 上面示例中,变量`multi`的类型是`number[][]`,表示它是一个二维数组,最底层的数组成员类型是`number`。 233 | 234 | -------------------------------------------------------------------------------- /docs/assert.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的类型断言 2 | 3 | ## 简介 4 | 5 | 对于没有类型声明的值,TypeScript 会进行类型推断,很多时候得到的结果,未必是开发者想要的。 6 | 7 | ```typescript 8 | type T = 'a'|'b'|'c'; 9 | let foo = 'a'; 10 | 11 | let bar:T = foo; // 报错 12 | ``` 13 | 14 | 上面示例中,最后一行报错,原因是 TypeScript 推断变量`foo`的类型是`string`,而变量`bar`的类型是`'a'|'b'|'c'`,前者是后者的父类型。父类型不能赋值给子类型,所以就报错了。 15 | 16 | TypeScript 提供了“类型断言”这样一种手段,允许开发者在代码中“断言”某个值的类型,告诉编译器此处的值是什么类型。TypeScript 一旦发现存在类型断言,就不再对该值进行类型推断,而是直接采用断言给出的类型。 17 | 18 | 这种做法的实质是,允许开发者在某个位置“绕过”编译器的类型推断,让本来通不过类型检查的代码能够通过,避免编译器报错。这样虽然削弱了 TypeScript 类型系统的严格性,但是为开发者带来了方便,毕竟开发者比编译器更了解自己的代码。 19 | 20 | 回到上面的例子,解决方法就是进行类型断言,在赋值时断言变量`foo`的类型。 21 | 22 | ```typescript 23 | type T = 'a'|'b'|'c'; 24 | 25 | let foo = 'a'; 26 | let bar:T = foo as T; // 正确 27 | ``` 28 | 29 | 上面示例中,最后一行的`foo as T`表示告诉编译器,变量`foo`的类型断言为`T`,所以这一行不再需要类型推断了,编译器直接把`foo`的类型当作`T`,就不会报错了。 30 | 31 | 总之,类型断言并不是真的改变一个值的类型,而是提示编译器,应该如何处理这个值。 32 | 33 | 类型断言有两种语法。 34 | 35 | ```typescript 36 | // 语法一:<类型>值 37 | value 38 | 39 | // 语法二:值 as 类型 40 | value as Type 41 | ``` 42 | 43 | 上面两种语法是等价的,`value`表示值,`Type`表示类型。早期只有语法一,后来因为 TypeScript 开始支持 React 的 JSX 语法(尖括号表示 HTML 元素),为了避免两者冲突,就引入了语法二。目前,推荐使用语法二。 44 | 45 | ```typescript 46 | // 语法一 47 | let bar:T = foo; 48 | 49 | // 语法二 50 | let bar:T = foo as T; 51 | ``` 52 | 53 | 上面示例是两种类型断言的语法,其中的语法一因为跟 JSX 语法冲突,使用时必须关闭 TypeScript 的 React 支持,否则会无法识别。由于这个原因,现在一般都使用语法二。 54 | 55 | 下面看一个例子。《对象》一章提到过,对象类型有严格字面量检查,如果存在额外的属性会报错。 56 | 57 | ```typescript 58 | // 报错 59 | const p:{ x: number } = { x: 0, y: 0 }; 60 | ``` 61 | 62 | 上面示例中,等号右侧是一个对象字面量,多出了属性`y`,导致报错。解决方法就是使用类型断言,可以用两种不同的断言。 63 | 64 | ```typescript 65 | // 正确 66 | const p0:{ x: number } = 67 | { x: 0, y: 0 } as { x: number }; 68 | 69 | // 正确 70 | const p1:{ x: number } = 71 | { x: 0, y: 0 } as { x: number; y: number }; 72 | ``` 73 | 74 | 上面示例中,两种类型断言都是正确的。第一种断言将类型改成与等号左边一致,第二种断言使得等号右边的类型是左边类型的子类型,子类型可以赋值给父类型,同时因为存在类型断言,就没有严格字面量检查了,所以不报错。 75 | 76 | 下面是一个网页编程的实际例子。 77 | 78 | ```typescript 79 | const username = document.getElementById('username'); 80 | 81 | if (username) { 82 | (username as HTMLInputElement).value; // 正确 83 | } 84 | ``` 85 | 86 | 上面示例中,变量`username`的类型是`HTMLElement | null`,排除了`null`的情况以后,HTMLElement 类型是没有`value`属性的。如果`username`是一个输入框,那么就可以通过类型断言,将它的类型改成`HTMLInputElement`,就可以读取`value`属性。 87 | 88 | 注意,上例的类型断言的圆括号是必需的,否则`username`会被断言成`HTMLInputElement.value`,从而报错。 89 | 90 | 类型断言不应滥用,因为它改变了 TypeScript 的类型检查,很可能埋下错误的隐患。 91 | 92 | ```typescript 93 | const data:object = { 94 | a: 1, 95 | b: 2, 96 | c: 3 97 | }; 98 | 99 | data.length; // 报错 100 | 101 | (data as Array).length; // 正确 102 | ``` 103 | 104 | 上面示例中,变量`data`是一个对象,没有`length`属性。但是通过类型断言,可以将它的类型断言为数组,这样使用`length`属性就能通过类型检查。但是,编译后的代码在运行时依然会报错,所以类型断言可以让错误的代码通过编译。 105 | 106 | 类型断言的一大用处是,指定 unknown 类型的变量的具体类型。 107 | 108 | ```typescript 109 | const value:unknown = 'Hello World'; 110 | 111 | const s1:string = value; // 报错 112 | const s2:string = value as string; // 正确 113 | ``` 114 | 115 | 上面示例中,unknown 类型的变量`value`不能直接赋值给其他类型的变量,但是可以将它断言为其他类型,这样就可以赋值给别的变量了。 116 | 117 | ## 类型断言的条件 118 | 119 | 类型断言并不意味着,可以把某个值断言为任意类型。 120 | 121 | ```typescript 122 | const n = 1; 123 | const m:string = n as string; // 报错 124 | ``` 125 | 126 | 上面示例中,变量`n`是数值,无法把它断言成字符串,TypeScript 会报错。 127 | 128 | 类型断言的使用前提是,值的实际类型与断言的类型必须满足一个条件。 129 | 130 | ```typescript 131 | expr as T 132 | ``` 133 | 134 | 上面代码中,`expr`是实际的值,`T`是类型断言,它们必须满足下面的条件:`expr`是`T`的子类型,或者`T`是`expr`的子类型。 135 | 136 | 也就是说,类型断言要求实际的类型与断言的类型兼容,实际类型可以断言为一个更加宽泛的类型(父类型),也可以断言为一个更加精确的类型(子类型),但不能断言为一个完全无关的类型。 137 | 138 | 但是,如果真的要断言成一个完全无关的类型,也是可以做到的。那就是连续进行两次类型断言,先断言成 unknown 类型或 any 类型,然后再断言为目标类型。因为`any`类型和`unknown`类型是所有其他类型的父类型,所以可以作为两种完全无关的类型的中介。 139 | 140 | ```typescript 141 | // 或者写成 expr 142 | expr as unknown as T 143 | ``` 144 | 145 | 上面代码中,`expr`连续进行了两次类型断言,第一次断言为`unknown`类型,第二次断言为`T`类型。这样的话,`expr`就可以断言成任意类型`T`,而不报错。 146 | 147 | 下面是本小节开头那个例子的改写。 148 | 149 | ```typescript 150 | const n = 1; 151 | const m:string = n as unknown as string; // 正确 152 | ``` 153 | 154 | 上面示例中,通过两次类型断言,变量`n`的类型就从数值,变成了完全无关的字符串,从而赋值时不会报错。 155 | 156 | ## as const 断言 157 | 158 | 如果没有声明变量类型,let 命令声明的变量,会被类型推断为 TypeScript 内置的基本类型之一;const 命令声明的变量,则被推断为值类型常量。 159 | 160 | ```typescript 161 | // 类型推断为基本类型 string 162 | let s1 = 'JavaScript'; 163 | 164 | // 类型推断为字符串 “JavaScript” 165 | const s2 = 'JavaScript'; 166 | ``` 167 | 168 | 上面示例中,变量`s1`的类型被推断为`string`,变量`s2`的类型推断为值类型`JavaScript`。后者是前者的子类型,相当于 const 命令有更强的限定作用,可以缩小变量的类型范围。 169 | 170 | 有些时候,let 变量会出现一些意想不到的报错,变更成 const 变量就能消除报错。 171 | 172 | ```typescript 173 | let s = 'JavaScript'; 174 | 175 | type Lang = 176 | |'JavaScript' 177 | |'TypeScript' 178 | |'Python'; 179 | 180 | function setLang(language:Lang) { 181 | /* ... */ 182 | } 183 | 184 | setLang(s); // 报错 185 | ``` 186 | 187 | 上面示例中,最后一行报错,原因是函数`setLang()`的参数`language`类型是`Lang`,这是一个联合类型。但是,传入的字符串`s`的类型被推断为`string`,属于`Lang`的父类型。父类型不能替代子类型,导致报错。 188 | 189 | 一种解决方法就是把 let 命令改成 const 命令。 190 | 191 | ```typescript 192 | const s = 'JavaScript'; 193 | ``` 194 | 195 | 这样的话,变量`s`的类型就是值类型`JavaScript`,它是联合类型`Lang`的子类型,传入函数`setLang()`就不会报错。 196 | 197 | 另一种解决方法是使用类型断言。TypeScript 提供了一种特殊的类型断言`as const`,用于告诉编译器,推断类型时,可以将这个值推断为常量,即把 let 变量断言为 const 变量,从而把内置的基本类型变更为值类型。 198 | 199 | ```typescript 200 | let s = 'JavaScript' as const; 201 | setLang(s); // 正确 202 | ``` 203 | 204 | 上面示例中,变量`s`虽然是用 let 命令声明的,但是使用了`as const`断言以后,就等同于是用 const 命令声明的,变量`s`的类型会被推断为值类型`JavaScript`。 205 | 206 | 使用了`as const`断言以后,let 变量就不能再改变值了。 207 | 208 | ```typescript 209 | let s = 'JavaScript' as const; 210 | s = 'Python'; // 报错 211 | ``` 212 | 213 | 上面示例中,let 命令声明的变量`s`,使用`as const`断言以后,就不能改变值了,否则报错。 214 | 215 | 注意,`as const`断言只能用于字面量,不能用于变量。 216 | 217 | ```typescript 218 | let s = 'JavaScript'; 219 | setLang(s as const); // 报错 220 | ``` 221 | 222 | 上面示例中,`as const`断言用于变量`s`,就报错了。下面的写法可以更清晰地看出这一点。 223 | 224 | ```typescript 225 | let s1 = 'JavaScript'; 226 | let s2 = s1 as const; // 报错 227 | ``` 228 | 229 | 另外,`as const`也不能用于表达式。 230 | 231 | ```typescript 232 | let s = ('Java' + 'Script') as const; // 报错 233 | ``` 234 | 235 | 上面示例中,`as const`用于表达式,导致报错。 236 | 237 | `as const`也可以写成前置的形式。 238 | 239 | ```typescript 240 | // 后置形式 241 | expr as const 242 | 243 | // 前置形式 244 | expr 245 | ``` 246 | 247 | `as const`断言可以用于整个对象,也可以用于对象的单个属性,这时它的类型缩小效果是不一样的。 248 | 249 | ```typescript 250 | const v1 = { 251 | x: 1, 252 | y: 2, 253 | }; // 类型是 { x: number; y: number; } 254 | 255 | const v2 = { 256 | x: 1 as const, 257 | y: 2, 258 | }; // 类型是 { x: 1; y: number; } 259 | 260 | const v3 = { 261 | x: 1, 262 | y: 2, 263 | } as const; // 类型是 { readonly x: 1; readonly y: 2; } 264 | ``` 265 | 266 | 上面示例中,第二种写法是对属性`x`缩小类型,第三种写法是对整个对象缩小类型。 267 | 268 | 总之,`as const`会将字面量的类型断言为不可变类型,缩小成 TypeScript 允许的最小类型。 269 | 270 | 下面是数组的例子。 271 | 272 | ```typescript 273 | // a1 的类型推断为 number[] 274 | const a1 = [1, 2, 3]; 275 | 276 | // a2 的类型推断为 readonly [1, 2, 3] 277 | const a2 = [1, 2, 3] as const; 278 | ``` 279 | 280 | 上面示例中,数组字面量使用`as const`断言后,类型推断就变成了只读元组。 281 | 282 | 由于`as const`会将数组变成只读元组,所以很适合用于函数的 rest 参数。 283 | 284 | ```typescript 285 | function add(x:number, y:number) { 286 | return x + y; 287 | } 288 | 289 | const nums = [1, 2]; 290 | const total = add(...nums); // 报错 291 | ``` 292 | 293 | 上面示例中,变量`nums`的类型推断为`number[]`,导致使用扩展运算符`...`传入函数`add()`会报错,因为`add()`只能接受两个参数,而`...nums`并不能保证参数的个数。 294 | 295 | 事实上,对于固定参数个数的函数,如果传入的参数包含扩展运算符,那么扩展运算符只能用于元组。只有当函数定义使用了 rest 参数,扩展运算符才能用于数组。 296 | 297 | 解决方法就是使用`as const`断言,将数组变成元组。 298 | 299 | ```typescript 300 | const nums = [1, 2] as const; 301 | const total = add(...nums); // 正确 302 | ``` 303 | 304 | 上面示例中,使用`as const`断言后,变量`nums`的类型会被推断为`readonly [1, 2]`,使用扩展运算符展开后,正好符合函数`add()`的参数类型。 305 | 306 | Enum 成员也可以使用`as const`断言。 307 | 308 | ```typescript 309 | enum Foo { 310 | X, 311 | Y, 312 | } 313 | let e1 = Foo.X; // Foo 314 | let e2 = Foo.X as const; // Foo.X 315 | ``` 316 | 317 | 上面示例中,如果不使用`as const`断言,变量`e1`的类型被推断为整个 Enum 类型;使用了`as const`断言以后,变量`e2`的类型被推断为 Enum 的某个成员,这意味着它不能变更为其他成员。 318 | 319 | ## 非空断言 320 | 321 | 对于那些可能为空的变量(即可能等于`undefined`或`null`),TypeScript 提供了非空断言,保证这些变量不会为空,写法是在变量名后面加上感叹号`!`。 322 | 323 | ```typescript 324 | function f(x?:number|null) { 325 | validateNumber(x); // 自定义函数,确保 x 是数值 326 | console.log(x!.toFixed()); 327 | } 328 | 329 | function validateNumber(e?:number|null) { 330 | if (typeof e !== 'number') 331 | throw new Error('Not a number'); 332 | } 333 | ``` 334 | 335 | 上面示例中,函数`f()`的参数`x`的类型是`number|null`,即可能为空。如果为空,就不存在`x.toFixed()`方法,这样写会报错。但是,开发者可以确认,经过`validateNumber()`的前置检验,变量`x`肯定不会为空,这时就可以使用非空断言,为函数体内部的变量`x`加上后缀`!`,`x!.toFixed()`编译就不会报错了。 336 | 337 | 非空断言在实际编程中很有用,有时可以省去一些额外的判断。 338 | 339 | ```typescript 340 | const root = document.getElementById('root'); 341 | 342 | // 报错 343 | root.addEventListener('click', e => { 344 | /* ... */ 345 | }); 346 | ``` 347 | 348 | 上面示例中,`getElementById()`有可能返回空值`null`,即变量`root`可能为空,这时对它调用`addEventListener()`方法就会报错,通不过编译。但是,开发者如果可以确认`root`元素肯定会在网页中存在,这时就可以使用非空断言。 349 | 350 | ```typescript 351 | const root = document.getElementById('root')!; 352 | ``` 353 | 354 | 上面示例中,`getElementById()`方法加上后缀`!`,表示这个方法肯定返回非空结果。 355 | 356 | 不过,非空断言会造成安全隐患,只有在确定一个表达式的值不为空时才能使用。比较保险的做法还是手动检查一下是否为空。 357 | 358 | ```typescript 359 | const root = document.getElementById('root'); 360 | 361 | if (root === null) { 362 | throw new Error('Unable to find DOM element #root'); 363 | } 364 | 365 | root.addEventListener('click', e => { 366 | /* ... */ 367 | }); 368 | ``` 369 | 370 | 上面示例中,如果`root`为空会抛错,比非空断言更保险一点。 371 | 372 | 非空断言还可以用于赋值断言。TypeScript 有一个编译设置,要求类的属性必须初始化(即有初始值),如果不对属性赋值就会报错。 373 | 374 | ```typescript 375 | class Point { 376 | x:number; // 报错 377 | y:number; // 报错 378 | 379 | constructor(x:number, y:number) { 380 | // ... 381 | } 382 | } 383 | ``` 384 | 385 | 上面示例中,属性`x`和`y`会报错,因为 TypeScript 认为它们没有初始化。 386 | 387 | 这时就可以使用非空断言,表示这两个属性肯定会有值,这样就不会报错了。 388 | 389 | ```typescript 390 | class Point { 391 | x!:number; // 正确 392 | y!:number; // 正确 393 | 394 | constructor(x:number, y:number) { 395 | // ... 396 | } 397 | } 398 | ``` 399 | 400 | 另外,非空断言只有在打开编译选项`strictNullChecks`时才有意义。如果不打开这个选项,编译器就不会检查某个变量是否可能为`undefined`或`null`。 401 | 402 | ## 断言函数 403 | 404 | 断言函数是一种特殊函数,用于保证函数参数符合某种类型。如果函数参数达不到要求,就会抛出错误,中断程序执行;如果达到要求,就不进行任何操作,让代码按照正常流程运行。 405 | 406 | ```typescript 407 | function isString(value:unknown):void { 408 | if (typeof value !== 'string') 409 | throw new Error('Not a string'); 410 | } 411 | ``` 412 | 413 | 上面示例中,函数`isString()`就是一个断言函数,用来保证参数`value`是一个字符串,否则就会抛出错误,中断程序的执行。 414 | 415 | 下面是它的用法。 416 | 417 | ```typescript 418 | function toUpper(x: string|number) { 419 | isString(x); 420 | return x.toUpperCase(); 421 | } 422 | ``` 423 | 424 | 上面示例中,函数`toUpper()`的参数`x`,可能是字符串,也可能是数值。但是,函数体的最后一行调用`toUpperCase()`方法,必须保证`x`是字符串,否则报错。所以,这一行前面调用断言函数`isString()`,调用以后 TypeScript 就能确定,变量`x`一定是字符串,不是数值,也就不报错了。 425 | 426 | 传统的断言函数`isString()`的写法有一个缺点,它的参数类型是`unknown`,返回值类型是`void`(即没有返回值)。单单从这样的类型声明,很难看出`isString()`是一个断言函数。 427 | 428 | 为了更清晰地表达断言函数,TypeScript 3.7 引入了新的类型写法。 429 | 430 | ```typescript 431 | function isString(value:unknown):asserts value is string { 432 | if (typeof value !== 'string') 433 | throw new Error('Not a string'); 434 | } 435 | ``` 436 | 437 | 上面示例中,函数`isString()`的返回值类型写成`asserts value is string`,其中`asserts`和`is`都是关键词,`value`是函数的参数名,`string`是函数参数的预期类型。它的意思是,该函数用来断言参数`value`的类型是`string`,如果达不到要求,程序就会在这里中断。 438 | 439 | 使用了断言函数的新写法以后,TypeScript 就会自动识别,只要执行了该函数,对应的变量都为断言的类型。 440 | 441 | 注意,函数返回值的断言写法,只是用来更清晰地表达函数意图,真正的检查是需要开发者自己部署的。而且,如果内部的检查与断言不一致,TypeScript 也不会报错。 442 | 443 | ```typescript 444 | function isString(value:unknown):asserts value is string { 445 | if (typeof value !== 'number') 446 | throw new Error('Not a number'); 447 | } 448 | ``` 449 | 450 | 上面示例中,函数的断言是参数`value`类型为字符串,但是实际上,内部检查的却是它是否为数值,如果不是就抛错。这段代码能够正常通过编译,表示 TypeScript 并不会检查断言与实际的类型检查是否一致。 451 | 452 | 另外,断言函数的`asserts`语句等同于`void`类型,所以如果返回除了`undefined`和`null`以外的值,都会报错。 453 | 454 | ```typescript 455 | function isString(value:unknown):asserts value is string { 456 | if (typeof value !== 'string') 457 | throw new Error('Not a string'); 458 | return true; // 报错 459 | } 460 | ``` 461 | 462 | 上面示例中,断言函数返回了`true`,导致报错。 463 | 464 | 下面是另一个例子。 465 | 466 | ```typescript 467 | type AccessLevel = 'r' | 'w' | 'rw'; 468 | 469 | function allowsReadAccess( 470 | level:AccessLevel 471 | ):asserts level is 'r' | 'rw' { 472 | if (!level.includes('r')) 473 | throw new Error('Read not allowed'); 474 | } 475 | ``` 476 | 477 | 上面示例中,函数`allowsReadAccess()`用来断言参数`level`一定等于`r`或`rw`。 478 | 479 | 如果要断言参数非空,可以使用工具类型`NonNullable`。 480 | 481 | ```typescript 482 | function assertIsDefined( 483 | value:T 484 | ):asserts value is NonNullable { 485 | if (value === undefined || value === null) { 486 | throw new Error(`${value} is not defined`) 487 | } 488 | } 489 | ``` 490 | 491 | 上面示例中,工具类型`NonNullable`对应类型`T`去除空类型后的剩余类型。 492 | 493 | 如果要将断言函数用于函数表达式,可以采用下面的写法。根据 TypeScript 的[要求](https://github.com/microsoft/TypeScript/pull/33622#issuecomment-575301357),这时函数表达式所赋予的变量,必须有明确的类型声明。 494 | 495 | ```typescript 496 | type AssertIsNumber = 497 | (value:unknown) => asserts value is number; 498 | 499 | const assertIsNumber:AssertIsNumber = (value) => { 500 | if (typeof value !== 'number') 501 | throw Error('Not a number'); 502 | }; 503 | ``` 504 | 505 | 注意,断言函数与类型保护函数(type guard)是两种不同的函数。它们的区别是,断言函数不返回值,而类型保护函数总是返回一个布尔值。 506 | 507 | ```typescript 508 | function isString( 509 | value:unknown 510 | ):value is string { 511 | return typeof value === 'string'; 512 | } 513 | ``` 514 | 515 | 上面示例就是一个类型保护函数`isString()`,作用是检查参数`value`是否为字符串。如果是的,返回`true`,否则返回`false`。该函数的返回值类型是`value is string`,其中的`is`是一个类型运算符,如果左侧的值符合右侧的类型,则返回`true`,否则返回`false`。 516 | 517 | 如果要断言某个参数保证为真(即不等于`false`、`undefined`和`null`),TypeScript 提供了断言函数的一种简写形式。 518 | 519 | ```typescript 520 | function assert(x:unknown):asserts x { 521 | // ... 522 | } 523 | ``` 524 | 525 | 上面示例中,函数`assert()`的断言部分,`asserts x`省略了谓语和宾语,表示参数`x`保证为真(`true`)。 526 | 527 | 同样的,参数为真的实际检查需要开发者自己实现。 528 | 529 | ```typescript 530 | function assert(x:unknown):asserts x { 531 | if (!x) { 532 | throw new Error(`${x} should be a truthy value.`); 533 | } 534 | } 535 | ``` 536 | 537 | 这种断言函数的简写形式,通常用来检查某个操作是否成功。 538 | 539 | ```typescript 540 | type Person = { 541 | name: string; 542 | email?: string; 543 | }; 544 | 545 | function loadPerson(): Person | null { 546 | return null; 547 | } 548 | 549 | let person = loadPerson(); 550 | 551 | function assert( 552 | condition: unknown, 553 | message: string 554 | ):asserts condition { 555 | if (!condition) throw new Error(message); 556 | } 557 | 558 | // Error: Person is not defined 559 | assert(person, 'Person is not defined'); 560 | console.log(person.name); 561 | ``` 562 | 563 | 上面示例中,只有`loadPerson()`返回结果为真(即操作成功),`assert()`才不会报错。 564 | 565 | ## 参考链接 566 | 567 | - [Const Assertions in Literal Expressions in TypeScript](https://mariusschulz.com/blog/const-assertions-in-literal-expressions-in-typescript), Marius Schulz 568 | - [Assertion Functions in TypeScript](https://mariusschulz.com/blog/assertion-functions-in-typescript), Marius Schulz 569 | - [Assertion functions in TypeScript](https://blog.logrocket.com/assertion-functions-typescript/), Matteo Di Pirro 570 | 571 | -------------------------------------------------------------------------------- /docs/basic.md: -------------------------------------------------------------------------------- 1 | # TypeScript 基本用法 2 | 3 | 本章介绍 TypeScript 的一些最基本的语法和用法。 4 | 5 | ## 类型声明 6 | 7 | TypeScript 代码最明显的特征,就是为 JavaScript 变量加上了类型声明。 8 | 9 | ```typescript 10 | let foo:string; 11 | ``` 12 | 13 | 上面示例中,变量`foo`的后面使用冒号,声明了它的类型为`string`。 14 | 15 | 类型声明的写法,一律为在标识符后面添加“冒号 + 类型”。函数参数和返回值,也是这样来声明类型。 16 | 17 | ```typescript 18 | function toString(num:number):string { 19 | return String(num); 20 | } 21 | ``` 22 | 23 | 上面示例中,函数`toString()`的参数`num`的类型是`number`。参数列表的圆括号后面,声明了返回值的类型是`string`。更详细的介绍,参见《函数》一章。 24 | 25 | 注意,变量的值应该与声明的类型一致,如果不一致,TypeScript 就会报错。 26 | 27 | ```typescript 28 | // 报错 29 | let foo:string = 123; 30 | ``` 31 | 32 | 上面示例中,变量`foo`的类型是字符串,但是赋值为数值`123`,TypeScript 就报错了。 33 | 34 | 另外,TypeScript 规定,变量只有赋值后才能使用,否则就会报错。 35 | 36 | ```typescript 37 | let x:number; 38 | console.log(x) // 报错 39 | ``` 40 | 41 | 上面示例中,变量`x`没有赋值就被读取,导致报错。而 JavaScript 允许这种行为,不会报错,没有赋值的变量会返回`undefined`。 42 | 43 | ## 类型推断 44 | 45 | 类型声明并不是必需的,如果没有,TypeScript 会自己推断类型。 46 | 47 | ```typescript 48 | let foo = 123; 49 | ``` 50 | 51 | 上面示例中,变量`foo`并没有类型声明,TypeScript 就会推断它的类型。由于它被赋值为一个数值,因此 TypeScript 推断它的类型为`number`。 52 | 53 | 后面,如果变量`foo`更改为其他类型的值,跟推断的类型不一致,TypeScript 就会报错。 54 | 55 | ```typescript 56 | let foo = 123; 57 | foo = 'hello'; // 报错 58 | ``` 59 | 60 | 上面示例中,变量`foo`的类型推断为`number`,后面赋值为字符串,TypeScript 就报错了。 61 | 62 | TypeScript 也可以推断函数的返回值。 63 | 64 | ```typescript 65 | function toString(num:number) { 66 | return String(num); 67 | } 68 | ``` 69 | 70 | 上面示例中,函数`toString()`没有声明返回值的类型,但是 TypeScript 推断返回的是字符串。正是因为 TypeScript 的类型推断,所以函数返回值的类型通常是省略不写的。 71 | 72 | 从这里可以看到,TypeScript 的设计思想是,类型声明是可选的,你可以加,也可以不加。即使不加类型声明,依然是有效的 TypeScript 代码,只是这时不能保证 TypeScript 会正确推断出类型。由于这个原因,所有 JavaScript 代码都是合法的 TypeScript 代码。 73 | 74 | 这样设计还有一个好处,将以前的 JavaScript 项目改为 TypeScript 项目时,你可以逐步地为老代码添加类型,即使有些代码没有添加,也不会无法运行。 75 | 76 | ## TypeScript 的编译 77 | 78 | JavaScript 的运行环境(浏览器和 Node.js)不认识 TypeScript 代码。所以,TypeScript 项目要想运行,必须先转为 JavaScript 代码,这个代码转换的过程就叫做“编译”(compile)。 79 | 80 | TypeScript 官方没有做运行环境,只提供编译器。编译时,会将类型声明和类型相关的代码全部删除,只留下能运行的 JavaScript 代码,并且不会改变 JavaScript 的运行结果。 81 | 82 | 因此,TypeScript 的类型检查只是编译时的类型检查,而不是运行时的类型检查。一旦代码编译为 JavaScript,运行时就不再检查类型了。 83 | 84 | ## 值与类型 85 | 86 | 学习 TypeScript 需要分清楚“值”(value)和“类型”(type)。 87 | 88 | “类型”是针对“值”的,可以视为是后者的一个元属性。每一个值在 TypeScript 里面都是有类型的。比如,`3`是一个值,它的类型是`number`。 89 | 90 | TypeScript 代码只涉及类型,不涉及值。所有跟“值”相关的处理,都由 JavaScript 完成。 91 | 92 | 这一点务必牢记。TypeScript 项目里面,其实存在两种代码,一种是底层的“值代码”,另一种是上层的“类型代码”。前者使用 JavaScript 语法,后者使用 TypeScript 的类型语法。 93 | 94 | 它们是可以分离的,TypeScript 的编译过程,实际上就是把“类型代码”全部拿掉,只保留“值代码”。 95 | 96 | 编写 TypeScript 项目时,不要混淆哪些是值代码,哪些是类型代码。 97 | 98 | ## TypeScript Playground 99 | 100 | 最简单的 TypeScript 使用方法,就是使用官网的在线编译页面,叫做 [TypeScript Playground](http://www.typescriptlang.org/play/)。 101 | 102 | 只要打开这个网页,把 TypeScript 代码贴进文本框,它就会在当前页面自动编译出 JavaScript 代码,还可以在浏览器执行编译产物。如果编译报错,它也会给出详细的报错信息。 103 | 104 | 这个页面还具有支持完整的 IDE 支持,可以自动语法提示。此外,它支持把代码片段和编译器设置保存成 URL,分享给他人。 105 | 106 | 本书的示例都建议放到这个页面,进行查看和编译。 107 | 108 | ## tsc 编译器 109 | 110 | TypeScript 官方提供的编译器叫做 tsc,可以将 TypeScript 脚本编译成 JavaScript 脚本。本机想要编译 TypeScript 代码,必须安装 tsc。 111 | 112 | 根据约定,TypeScript 脚本文件使用`.ts`后缀名,JavaScript 脚本文件使用`.js`后缀名。tsc 的作用就是把`.ts`脚本转变成`.js`脚本。 113 | 114 | ### 安装 115 | 116 | tsc 是一个 npm 模块,使用下面的命令安装(必须先安装 npm)。 117 | 118 | ```bash 119 | $ npm install -g typescript 120 | ``` 121 | 122 | 上面命令是全局安装 tsc,也可以在项目中将 tsc 安装为一个依赖模块。 123 | 124 | 安装完成后,检查一下是否安装成功。 125 | 126 | ```bash 127 | # 或者 tsc --version 128 | $ tsc -v 129 | Version 5.1.6 130 | ``` 131 | 132 | 上面命令中,`-v`或`--version`参数可以输出当前安装的 tsc 版本。 133 | 134 | ### 帮助信息 135 | 136 | `-h`或`--help`参数输出帮助信息。 137 | 138 | ```bash 139 | $ tsc -h 140 | ``` 141 | 142 | 默认情况下,“--help”参数仅显示基本的可用选项。我们可以使用“--all”参数,查看完整的帮助信息。 143 | 144 | ```bash 145 | $ tsc --all 146 | ``` 147 | 148 | ### 编译脚本 149 | 150 | 安装 tsc 之后,就可以编译 TypeScript 脚本了。 151 | 152 | `tsc`命令后面,加上 TypeScript 脚本文件,就可以将其编译成 JavaScript 脚本。 153 | 154 | ```bash 155 | $ tsc app.ts 156 | ``` 157 | 158 | 上面命令会在当前目录下,生成一个`app.js`脚本文件,这个脚本就完全是编译后生成的 JavaScript 代码。 159 | 160 | `tsc`命令也可以一次编译多个 TypeScript 脚本。 161 | 162 | ```bash 163 | $ tsc file1.ts file2.ts file3.ts 164 | ``` 165 | 166 | 上面命令会在当前目录生成三个 JavaScript 脚本文件`file1.js`、`file2.js`、`file3.js`。 167 | 168 | tsc 有很多参数,可以调整编译行为。 169 | 170 | **(1)--outFile** 171 | 172 | 如果想将多个 TypeScript 脚本编译成一个 JavaScript 文件,使用`--outFile`参数。 173 | 174 | ```bash 175 | $ tsc file1.ts file2.ts --outFile app.js 176 | ``` 177 | 178 | 上面命令将`file1.ts`和`file2.ts`两个脚本编译成一个 JavaScript 文件`app.js`。 179 | 180 | **(2)--outDir** 181 | 182 | 编译结果默认都保存在当前目录,`--outDir`参数可以指定保存到其他目录。 183 | 184 | ```bash 185 | $ tsc app.ts --outDir dist 186 | ``` 187 | 188 | 上面命令会在`dist`子目录下生成`app.js`。 189 | 190 | **(3)--target** 191 | 192 | 为了保证编译结果能在各种 JavaScript 引擎运行,tsc 默认会将 TypeScript 代码编译成很低版本的 JavaScript,即3.0版本(以`es3`表示)。这通常不是我们想要的结果。 193 | 194 | 这时可以使用`--target`参数,指定编译后的 JavaScript 版本。建议使用`es2015`,或者更新版本。 195 | 196 | ```bash 197 | $ tsc --target es2015 app.ts 198 | ``` 199 | 200 | ### 编译错误的处理 201 | 202 | 编译过程中,如果没有报错,`tsc`命令不会有任何显示。所以,如果你没有看到任何提示,就表示编译成功了。 203 | 204 | 如果编译报错,`tsc`命令就会显示报错信息,但是这种情况下,依然会编译生成 JavaScript 脚本。 205 | 206 | 举例来说,下面是一个错误的 TypeScript 脚本`app.ts`。 207 | 208 | ```typescript 209 | // app.ts 210 | let foo:number = 123; 211 | foo = 'abc'; // 报错 212 | ``` 213 | 214 | 上面示例中,变量`foo`是数值类型,赋值为字符串,`tsc`命令编译这个脚本就会报错。 215 | 216 | ```bash 217 | $ tsc app.ts 218 | 219 | app.ts:2:1 - error TS2322: Type 'string' is not assignable to type 'number'. 220 | 221 | 2 foo = 'abc'; 222 | ~~~ 223 | 224 | Found 1 error in app.ts:2 225 | ``` 226 | 227 | 上面示例中,`tsc`命令输出报错信息,表示变量`foo`被错误地赋值为字符串。 228 | 229 | 这种情况下,编译产物`app.js`还是会照样生成,下面就是编译后的结果。 230 | 231 | ```javascript 232 | // app.js 233 | var foo = 123; 234 | foo = 'abc'; 235 | ``` 236 | 237 | 可以看到,尽管有错,tsc 依然原样将 TypeScript 编译成 JavaScript 脚本。 238 | 239 | 这是因为 TypeScript 团队认为,编译器的作用只是给出编译错误,至于怎么处理这些错误,那就是开发者自己的判断了。开发者更了解自己的代码,所以不管怎样,编译产物都会生成,让开发者决定下一步怎么处理。 240 | 241 | 如果希望一旦报错就停止编译,不生成编译产物,可以使用`--noEmitOnError`参数。 242 | 243 | ```bash 244 | $ tsc --noEmitOnError app.ts 245 | ``` 246 | 247 | 上面命令在报错后,就不会生成`app.js`。 248 | 249 | tsc 还有一个`--noEmit`参数,只检查类型是否正确,不生成 JavaScript 文件。 250 | 251 | ```bash 252 | $ tsc --noEmit app.ts 253 | ``` 254 | 255 | 上面命令只检查是否有编译错误,不会生成`app.js`。 256 | 257 | tsc 命令的更多参数,详见《tsc 编译器》一章。 258 | 259 | ### tsconfig.json 260 | 261 | TypeScript 允许将`tsc`的编译参数,写在配置文件`tsconfig.json`。只要当前目录有这个文件,`tsc`就会自动读取,所以运行时可以不写参数。 262 | 263 | ```bash 264 | $ tsc file1.ts file2.ts --outFile dist/app.js 265 | ``` 266 | 267 | 上面这个命令写成`tsconfig.json`,就是下面这样。 268 | 269 | ```json 270 | { 271 | "files": ["file1.ts", "file2.ts"], 272 | "compilerOptions": { 273 | "outFile": "dist/app.js" 274 | } 275 | } 276 | ``` 277 | 278 | 有了这个配置文件,编译时直接调用`tsc`命令就可以了。 279 | 280 | ```bash 281 | $ tsc 282 | ``` 283 | 284 | `tsconfig.json`的详细介绍,参见《tsconfig.json 配置文件》一章。 285 | 286 | ## ts-node 模块 287 | 288 | [ts-node](https://github.com/TypeStrong/ts-node) 是一个非官方的 npm 模块,可以直接运行 TypeScript 代码。 289 | 290 | 使用时,可以先全局安装它。 291 | 292 | ```bash 293 | $ npm install -g ts-node 294 | ``` 295 | 296 | 安装后,就可以直接运行 TypeScript 脚本。 297 | 298 | ```bash 299 | $ ts-node script.ts 300 | ``` 301 | 302 | 上面命令运行了 TypeScript 脚本`script.ts`,给出运行结果。 303 | 304 | 如果不安装 ts-node,也可以通过 npx 调用它来运行 TypeScript 脚本。 305 | 306 | ```bash 307 | $ npx ts-node script.ts 308 | ``` 309 | 310 | 上面命令中,`npx`会在线调用 ts-node,从而在不安装的情况下,运行`script.ts`。 311 | 312 | 如果执行 ts-node 命令不带有任何参数,它会提供一个 TypeScript 的命令行 REPL 运行环境,你可以在这个环境中输入 TypeScript 代码,逐行执行。 313 | 314 | ```bash 315 | $ ts-node 316 | > 317 | ``` 318 | 319 | 上面示例中,单独运行`ts-node`命令,会给出一个大于号,这就是 TypeScript 的 REPL 运行环境,可以逐行输入代码运行。 320 | 321 | ```bash 322 | $ ts-node 323 | > const twice = (x:string) => x + x; 324 | > twice('abc') 325 | 'abcabc' 326 | > 327 | ``` 328 | 329 | 上面示例中,在 TypeScript 命令行 REPL 环境中,先输入一个函数`twice`,然后调用该函数,就会得到结果。 330 | 331 | 要退出这个 REPL 环境,可以按下 Ctrl + d,或者输入`.exit`。 332 | 333 | 如果只是想简单运行 TypeScript 代码看看结果,ts-node 不失为一个便捷的方法。 334 | 335 | -------------------------------------------------------------------------------- /docs/comment.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的注释指令 2 | 3 | TypeScript 接受一些注释指令。 4 | 5 | 所谓“注释指令”,指的是采用 JS 双斜杠注释的形式,向编译器发出的命令。 6 | 7 | ## `// @ts-nocheck` 8 | 9 | `// @ts-nocheck`告诉编译器不对当前脚本进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。 10 | 11 | ```javascript 12 | // @ts-nocheck 13 | 14 | const element = document.getElementById(123); 15 | ``` 16 | 17 | 上面示例中,`document.getElementById(123)`存在类型错误,但是编译器不对该脚本进行类型检查,所以不会报错。 18 | 19 | ## `// @ts-check` 20 | 21 | 如果一个 JavaScript 脚本顶部添加了`// @ts-check`,那么编译器将对该脚本进行类型检查,不论是否启用了`checkJs`编译选项。 22 | 23 | ```javascript 24 | // @ts-check 25 | let isChecked = true; 26 | 27 | console.log(isChceked); // 报错 28 | ``` 29 | 30 | 上面示例是一个 JavaScript 脚本,`// @ts-check`告诉 TypeScript 编译器对其进行类型检查,所以最后一行会报错,提示拼写错误。 31 | 32 | ## `// @ts-ignore` 33 | 34 | `// @ts-ignore`告诉编译器不对下一行代码进行类型检查,可以用于 TypeScript 脚本,也可以用于 JavaScript 脚本。 35 | 36 | ```typescript 37 | let x:number; 38 | 39 | x = 0; 40 | 41 | // @ts-ignore 42 | x = false; // 不报错 43 | ``` 44 | 45 | 上面示例中,最后一行是类型错误,变量`x`的类型是`number`,不能等于布尔值。但是因为前面加上了`// @ts-ignore`,编译器会跳过这一行的类型检查,所以不会报错。 46 | 47 | ## `// @ts-expect-error` 48 | 49 | `// @ts-expect-error`主要用在测试用例,当下一行有类型错误时,它会压制 TypeScript 的报错信息(即不显示报错信息),把错误留给代码自己处理。 50 | 51 | ```typescript 52 | function doStuff(abc: string, xyz: string) { 53 | assert(typeof abc === "string"); 54 | assert(typeof xyz === "string"); 55 | // do some stuff 56 | } 57 | 58 | expect(() => { 59 | // @ts-expect-error 60 | doStuff(123, 456); 61 | }).toThrow(); 62 | ``` 63 | 64 | 上面示例是一个测试用例,倒数第二行的`doStuff(123, 456)`的参数类型与定义不一致,TypeScript 引擎会报错。但是,测试用例本身测试的就是这个错误,已经有专门的处理代码,所以这里可以使用`// @ts-expect-error`,不显示引擎的报错信息。 65 | 66 | 如果下一行没有类型错误,`// @ts-expect-error`则会显示一行提示。 67 | 68 | ```typescript 69 | // @ts-expect-error 70 | console.log(1 + 1); 71 | // 输出 Unused '@ts-expect-error' directive. 72 | ``` 73 | 74 | 上面示例中,第二行是正确代码,这时系统会给出一个提示,表示`@ts-expect-error`没有用到。 75 | 76 | ## JSDoc 77 | 78 | TypeScript 直接处理 JS 文件时,如果无法推断出类型,会使用 JS 脚本里面的 JSDoc 注释。 79 | 80 | 使用 JSDoc 时,有两个基本要求。 81 | 82 | (1)JSDoc 注释必须以`/**`开始,其中星号(`*`)的数量必须为两个。若使用其他形式的多行注释,则 JSDoc 会忽略该条注释。 83 | 84 | (2)JSDoc 注释必须与它描述的代码处于相邻的位置,并且注释在上,代码在下。 85 | 86 | 下面是 JSDoc 的一个简单例子。 87 | 88 | ```javascript 89 | /** 90 | * @param {string} somebody 91 | */ 92 | function sayHello(somebody) { 93 | console.log('Hello ' + somebody); 94 | } 95 | ``` 96 | 97 | 上面示例中,注释里面的`@param`是一个 JSDoc 声明,表示下面的函数`sayHello()`的参数`somebody`类型为`string`。 98 | 99 | TypeScript 编译器支持大部分的 JSDoc 声明,下面介绍其中的一些。 100 | 101 | ### @typedef 102 | 103 | `@typedef`命令创建自定义类型,等同于 TypeScript 里面的类型别名。 104 | 105 | ```javascript 106 | /** 107 | * @typedef {(number | string)} NumberLike 108 | */ 109 | ``` 110 | 111 | 上面示例中,定义了一个名为`NumberLike`的新类型,它是由`number`和`string`构成的联合类型,等同于 TypeScript 的如下语句。 112 | 113 | ```typescript 114 | type NumberLike = string | number; 115 | ``` 116 | 117 | ### @type 118 | 119 | `@type`命令定义变量的类型。 120 | 121 | ```javascript 122 | /** 123 | * @type {string} 124 | */ 125 | let a; 126 | ``` 127 | 128 | 上面示例中,`@type`定义了变量`a`的类型为`string`。 129 | 130 | 在`@type`命令中可以使用由`@typedef`命令创建的类型。 131 | 132 | ```javascript 133 | /** 134 | * @typedef {(number | string)} NumberLike 135 | */ 136 | 137 | /** 138 | * @type {NumberLike} 139 | */ 140 | let a = 0; 141 | ``` 142 | 143 | 在`@type`命令中允许使用 TypeScript 类型及其语法。 144 | 145 | ```javascript 146 | /**@type {true | false} */ 147 | let a; 148 | 149 | /** @type {number[]} */ 150 | let b; 151 | 152 | /** @type {Array} */ 153 | let c; 154 | 155 | /** @type {{ readonly x: number, y?: string }} */ 156 | let d; 157 | 158 | /** @type {(s: string, b: boolean) => number} */ 159 | let e; 160 | ``` 161 | 162 | ### @param 163 | 164 | `@param`命令用于定义函数参数的类型。 165 | 166 | ```javascript 167 | /** 168 | * @param {string} x 169 | */ 170 | function foo(x) {} 171 | ``` 172 | 173 | 如果是可选参数,需要将参数名放在方括号`[]`里面。 174 | 175 | ```javascript 176 | /** 177 | * @param {string} [x] 178 | */ 179 | function foo(x) {} 180 | ``` 181 | 182 | 方括号里面,还可以指定参数默认值。 183 | 184 | ```javascript 185 | /** 186 | * @param {string} [x="bar"] 187 | */ 188 | function foo(x) {} 189 | ``` 190 | 191 | 上面示例中,参数`x`的默认值是字符串`bar`。 192 | 193 | ### @return,@returns 194 | 195 | `@return`和`@returns`命令的作用相同,指定函数返回值的类型。 196 | 197 | ```javascript 198 | /** 199 | * @return {boolean} 200 | */ 201 | function foo() { 202 | return true; 203 | } 204 | 205 | /** 206 | * @returns {number} 207 | */ 208 | function bar() { 209 | return 0; 210 | } 211 | ``` 212 | 213 | ### @extends 和类型修饰符 214 | 215 | `@extends`命令用于定义继承的基类。 216 | 217 | ```javascript 218 | /** 219 | * @extends {Base} 220 | */ 221 | class Derived extends Base { 222 | } 223 | ``` 224 | 225 | `@public`、`@protected`、`@private`分别指定类的公开成员、保护成员和私有成员。 226 | 227 | `@readonly`指定只读成员。 228 | 229 | ```javascript 230 | class Base { 231 | /** 232 | * @public 233 | * @readonly 234 | */ 235 | x = 0; 236 | 237 | /** 238 | * @protected 239 | */ 240 | y = 0; 241 | } 242 | ``` 243 | 244 | -------------------------------------------------------------------------------- /docs/d.ts.md: -------------------------------------------------------------------------------- 1 | # d.ts 类型声明文件 2 | 3 | ## 简介 4 | 5 | 单独使用的模块,一般会同时提供一个单独的类型声明文件(declaration file),把本模块的外部接口的所有类型都写在这个文件里面,便于模块使用者了解接口,也便于编译器检查使用者的用法是否正确。 6 | 7 | 类型声明文件里面只有类型代码,没有具体的代码实现。它的文件名一般为`[模块名].d.ts`的形式,其中的`d`表示 declaration(声明)。 8 | 9 | 举例来说,有一个模块的代码如下。 10 | 11 | ```typescript 12 | const maxInterval = 12; 13 | 14 | function getArrayLength(arr) { 15 | return arr.length; 16 | } 17 | 18 | module.exports = { 19 | getArrayLength, 20 | maxInterval, 21 | }; 22 | ``` 23 | 24 | 它的类型声明文件可以写成下面这样。 25 | 26 | ```typescript 27 | export function getArrayLength(arr: any[]): number; 28 | export const maxInterval: 12; 29 | ``` 30 | 31 | 类型声明文件也可以使用`export =`命令,输出对外接口。下面是 moment 模块的类型声明文件的例子。 32 | 33 | ```typescript 34 | declare module 'moment' { 35 | function moment(): any; 36 | export = moment; 37 | } 38 | ``` 39 | 40 | 上面示例中,模块`moment`内部有一个函数`moment()`,而`export =`表示`module.exports`输出的就是这个函数。 41 | 42 | 除了使用`export =`,模块输出在类型声明文件中,也可以使用`export default`表示。 43 | 44 | ```typescript 45 | // 模块输出 46 | module.exports = 3.142; 47 | 48 | // 类型输出文件 49 | // 写法一 50 | declare const pi: number; 51 | export default pi; 52 | 53 | // 写法二 54 | declare const pi: number; 55 | export= pi; 56 | ``` 57 | 58 | 上面示例中,模块输出的是一个整数,那么可以用`export default`或`export =`表示输出这个值。 59 | 60 | 下面是一个如何使用类型声明文件的简单例子。有一个类型声明文件`types.d.ts`。 61 | 62 | ```typescript 63 | // types.d.ts 64 | export interface Character { 65 | catchphrase?: string; 66 | name: string; 67 | } 68 | ``` 69 | 70 | 然后,就可以在 TypeScript 脚本里面导入该文件声明的类型。 71 | 72 | ```typescript 73 | // index.ts 74 | import { Character } from "./types"; 75 | 76 | export const character:Character = { 77 | catchphrase: "Yee-haw!", 78 | name: "Sandy Cheeks", 79 | }; 80 | ``` 81 | 82 | 类型声明文件也可以包括在项目的 tsconfig.json 文件里面,这样的话,编译器打包项目时,会自动将类型声明文件加入编译,而不必在每个脚本里面加载类型声明文件。比如,moment 模块的类型声明文件是`moment.d.ts`,使用 moment 模块的项目可以将其加入项目的 tsconfig.json 文件。 83 | 84 | ```typescript 85 | { 86 | "compilerOptions": {}, 87 | "files": [ 88 | "src/index.ts", 89 | "typings/moment.d.ts" 90 | ] 91 | } 92 | ``` 93 | 94 | ## 类型声明文件的来源 95 | 96 | 类型声明文件主要有以下三种来源。 97 | 98 | - TypeScript 编译器自动生成。 99 | - TypeScript 内置类型文件。 100 | - 外部模块的类型声明文件,需要自己安装。 101 | 102 | ### 自动生成 103 | 104 | 只要使用编译选项`declaration`,编译器就会在编译时自动生成单独的类型声明文件。 105 | 106 | 下面是在`tsconfig.json`文件里面,打开这个选项。 107 | 108 | ```typescript 109 | { 110 | "compilerOptions": { 111 | "declaration": true 112 | } 113 | } 114 | ``` 115 | 116 | 你也可以在命令行打开这个选项。 117 | 118 | ```bash 119 | $ tsc --declaration 120 | ``` 121 | 122 | ### 内置声明文件 123 | 124 | 安装 TypeScript 语言时,会同时安装一些内置的类型声明文件,主要是内置的全局对象(JavaScript 语言接口和运行环境 API)的类型声明。 125 | 126 | 这些内置声明文件位于 TypeScript 语言安装目录的`lib`文件夹内,数量大概有几十个,下面是其中一些主要文件。 127 | 128 | - lib.d.ts 129 | - lib.dom.d.ts 130 | - lib.es2015.d.ts 131 | - lib.es2016.d.ts 132 | - lib.es2017.d.ts 133 | - lib.es2018.d.ts 134 | - lib.es2019.d.ts 135 | - lib.es2020.d.ts 136 | - lib.es5.d.ts 137 | - lib.es6.d.ts 138 | 139 | 这些内置声明文件的文件名统一为“lib.[description].d.ts”的形式,其中`description`部分描述了文件内容。比如,`lib.dom.d.ts`这个文件就描述了 DOM 结构的类型。 140 | 141 | 如果开发者想了解全局对象的类型接口(比如 ES6 全局对象的类型),那么就可以去查看这些内置声明文件。 142 | 143 | TypeScript 编译器会自动根据编译目标`target`的值,加载对应的内置声明文件,所以不需要特别的配置。但是,可以使用编译选项`lib`,指定加载哪些内置声明文件。 144 | 145 | ```javascript 146 | { 147 | "compilerOptions": { 148 | "lib": ["dom", "es2021"] 149 | } 150 | } 151 | ``` 152 | 153 | 上面示例中,`lib`选项指定加载`dom`和`es2021`这两个内置类型声明文件。 154 | 155 | 编译选项`noLib`会禁止加载任何内置声明文件。 156 | 157 | ### 外部类型声明文件 158 | 159 | 如果项目中使用了外部的某个第三方代码库,那么就需要这个库的类型声明文件。 160 | 161 | 这时又分成三种情况。 162 | 163 | (1)这个库自带了类型声明文件。 164 | 165 | 一般来说,如果这个库的源码包含了`[vendor].d.ts`文件,那么就自带了类型声明文件。其中的`vendor`表示这个库的名字,比如`moment`这个库就自带`moment.d.ts`。使用这个库可能需要单独加载它的类型声明文件。 166 | 167 | (2)这个库没有自带,但是可以找到社区制作的类型声明文件。 168 | 169 | 第三方库如果没有提供类型声明文件,社区往往会提供。TypeScript 社区主要使用 [DefinitelyTyped 仓库](https://github.com/DefinitelyTyped/DefinitelyTyped),各种类型声明文件都会提交到那里,已经包含了几千个第三方库。 170 | 171 | 这些声明文件都会作为一个单独的库,发布到 npm 的`@types`名称空间之下。比如,jQuery 的类型声明文件就发布成`@types/jquery`这个库,使用时安装这个库就可以了。 172 | 173 | ```bash 174 | $ npm install @types/jquery --save-dev 175 | ``` 176 | 177 | 执行上面的命令,`@types/jquery`这个库就安装到项目的`node_modules/@types/jquery`目录,里面的`index.d.ts`文件就是 jQuery 的类型声明文件。如果类型声明文件不是`index.d.ts`,那么就需要在`package.json`的`types`或`typings`字段,指定类型声明文件的文件名。 178 | 179 | TypeScript 会自动加载`node_modules/@types`目录下的模块,但可以使用编译选项`typeRoots`改变这种行为。 180 | 181 | ```typescript 182 | { 183 | "compilerOptions": { 184 | "typeRoots": ["./typings", "./vendor/types"] 185 | } 186 | } 187 | ``` 188 | 189 | 上面示例表示,TypeScript 不再去`node_modules/@types`目录,而是去跟当前`tsconfig.json`同级的`typings`和`vendor/types`子目录,加载类型模块了。 190 | 191 | 默认情况下,TypeScript 会自动加载`typeRoots`目录里的所有模块,编译选项`types`可以指定加载哪些模块。 192 | 193 | ```javascript 194 | { 195 | "compilerOptions": { 196 | "types" : ["jquery"] 197 | } 198 | } 199 | ``` 200 | 201 | 上面设置中,`types`属性是一个数组,成员是所要加载的类型模块,要加载几个模块,这个数组就有几个成员,每个类型模块在`typeRoots`目录下都有一个自己的子目录。这样的话,TypeScript 就会自动去`jquery`子目录,加载 jQuery 的类型声明文件。 202 | 203 | (3)找不到类型声明文件,需要自己写。 204 | 205 | 有时实在没有第三方库的类型声明文件,又很难完整给出该库的类型描述,这时你可以告诉 TypeScript 相关对象的类型是`any`。比如,使用 jQuery 的脚本可以写成下面这样。 206 | 207 | ```typescript 208 | declare var $:any 209 | 210 | // 或者 211 | declare type JQuery = any; 212 | declare var $:JQuery; 213 | ``` 214 | 215 | 上面代码表示,jQuery 的`$`对象是外部引入的,类型是`any`,也就是 TypeScript 不用对它进行类型检查。 216 | 217 | 也可以采用下面的写法,将整个外部模块的类型设为`any`。 218 | 219 | ```typescript 220 | declare module '模块名'; 221 | ``` 222 | 223 | 有了上面的命令,指定模块的所有接口都将视为`any`类型。 224 | 225 | ## declare 关键字 226 | 227 | 类型声明文件只包含类型描述,不包含具体实现,所以非常适合使用 declare 语句来描述类型。declare 关键字的具体用法,详见《declare 关键字》一章,这里讲解如何在类型声明文件里面使用它。 228 | 229 | 类型声明文件里面,变量的类型描述必须使用`declare`命令,否则会报错,因为变量声明语句是值相关代码。 230 | 231 | ```typescript 232 | declare let foo:string; 233 | ``` 234 | 235 | interface 类型有没有`declare`都可以,因为 interface 是完全的类型代码。 236 | 237 | ```typescript 238 | interface Foo {} // 正确 239 | declare interface Foo {} // 正确 240 | ``` 241 | 242 | 类型声明文件里面,顶层可以使用`export`命令,也可以不用,除非使用者脚本会显式使用`export`命令输入类型。 243 | 244 | ```typescript 245 | export interface Data { 246 | version: string; 247 | } 248 | ``` 249 | 250 | 下面是类型声明文件的一些例子。先看 moment 模块的类型描述文件`moment.d.ts`。 251 | 252 | ```typescript 253 | declare module 'moment' { 254 | export interface Moment { 255 | format(format:string): string; 256 | 257 | add( 258 | amount: number, 259 | unit: 'days' | 'months' | 'years' 260 | ): Moment; 261 | 262 | subtract( 263 | amount:number, 264 | unit:'days' | 'months' | 'years' 265 | ): Moment; 266 | } 267 | 268 | function moment( 269 | input?: string | Date 270 | ): Moment; 271 | 272 | export default moment; 273 | } 274 | ``` 275 | 276 | 上面示例中,可以注意一下默认接口`moment()`的写法。 277 | 278 | 下面是 D3 库的类型声明文件`D3.d.ts`。 279 | 280 | ```typescript 281 | declare namespace D3 { 282 | export interface Selectors { 283 | select: { 284 | (selector: string): Selection; 285 | (element: EventTarget): Selection; 286 | }; 287 | } 288 | 289 | export interface Event { 290 | x: number; 291 | y: number; 292 | } 293 | 294 | export interface Base extends Selectors { 295 | event: Event; 296 | } 297 | } 298 | 299 | declare var d3: D3.Base; 300 | ``` 301 | 302 | ## 模块发布 303 | 304 | 当前模块如果包含自己的类型声明文件,可以在 package.json 文件里面添加一个`types`字段或`typings`字段,指明类型声明文件的位置。 305 | 306 | ```typescript 307 | { 308 | "name": "awesome", 309 | "author": "Vandelay Industries", 310 | "version": "1.0.0", 311 | "main": "./lib/main.js", 312 | "types": "./lib/main.d.ts" 313 | } 314 | ``` 315 | 316 | 上面示例中,`types`字段给出了类型声明文件的位置。 317 | 318 | 注意,如果类型声明文件名为`index.d.ts`,且在项目的根目录中,那就不需要在`package.json`里面注明了。 319 | 320 | 有时,类型声明文件会单独发布成一个 npm 模块,这时用户就必须同时加载该模块。 321 | 322 | ```typescript 323 | { 324 | "name": "browserify-typescript-extension", 325 | "author": "Vandelay Industries", 326 | "version": "1.0.0", 327 | "main": "./lib/main.js", 328 | "types": "./lib/main.d.ts", 329 | "dependencies": { 330 | "browserify": "latest", 331 | "@types/browserify": "latest", 332 | "typescript": "next" 333 | } 334 | } 335 | ``` 336 | 337 | 上面示例是一个模块的 package.json 文件,该模块需要 browserify 模块。由于后者的类型声明文件是一个单独的模块`@types/browserify`,所以还需要加载那个模块。 338 | 339 | ## 三斜杠命令 340 | 341 | 如果类型声明文件的内容非常多,可以拆分成多个文件,然后入口文件使用三斜杠命令,加载其他拆分后的文件。 342 | 343 | 举例来说,入口文件是`main.d.ts`,里面的接口定义在`interfaces.d.ts`,函数定义在`functions.d.ts`。那么,`main.d.ts`里面可以用三斜杠命令,加载后面两个文件。 344 | 345 | ```typescript 346 | /// 347 | /// 348 | ``` 349 | 350 | 三斜杠命令(`///`)是一个 TypeScript 编译器命令,用来指定编译器行为。它只能用在文件的头部,如果用在其他地方,会被当作普通的注释。另外,若一个文件中使用了三斜线命令,那么在三斜线命令之前只允许使用单行注释、多行注释和其他三斜线命令,否则三斜杠命令也会被当作普通的注释。 351 | 352 | 除了拆分类型声明文件,三斜杠命令也可以用于普通脚本加载类型声明文件。 353 | 354 | 三斜杠命令主要包含三个参数,代表三种不同的命令。 355 | 356 | - path 357 | - types 358 | - lib 359 | 360 | 下面依次进行讲解。 361 | 362 | ### `/// ` 363 | 364 | `/// `是最常见的三斜杠命令,告诉编译器在编译时需要包括的文件,常用来声明当前脚本依赖的类型文件。 365 | 366 | ```typescript 367 | /// 368 | 369 | let count = add(1, 2); 370 | ``` 371 | 372 | 上面示例表示,当前脚本依赖于`./lib.ts`,里面是`add()`的定义。编译当前脚本时,还会同时编译`./lib.ts`。编译产物会有两个 JS 文件,一个当前脚本,另一个就是`./lib.js`。 373 | 374 | 下面的例子是当前脚本依赖于 Node.js 类型声明文件。 375 | 376 | ```typescript 377 | /// 378 | import * as URL from "url"; 379 | let myUrl = URL.parse("https://www.typescriptlang.org"); 380 | ``` 381 | 382 | 编译器会在预处理阶段,找出所有三斜杠引用的文件,将其添加到编译列表中,然后一起编译。 383 | 384 | `path`参数指定了所引入文件的路径。如果该路径是一个相对路径,则基于当前脚本的路径进行计算。 385 | 386 | 使用该命令时,有以下两个注意事项。 387 | 388 | - `path`参数必须指向一个存在的文件,若文件不存在会报错。 389 | - `path`参数不允许指向当前文件。 390 | 391 | 默认情况下,每个三斜杠命令引入的脚本,都会编译成单独的 JS 文件。如果希望编译后只产出一个合并文件,可以使用编译选项`outFile`。但是,`outFile`编译选项不支持合并 CommonJS 模块和 ES 模块,只有当编译参数`module`的值设为 None、System 或 AMD 时,才能编译成一个文件。 392 | 393 | 如果打开了编译参数`noResolve`,则忽略三斜杠指令。将其当作一般的注释,原样保留在编译产物中。 394 | 395 | ### `/// ` 396 | 397 | types 参数用来告诉编译器当前脚本依赖某个 DefinitelyTyped 类型库,通常安装在`node_modules/@types`目录。 398 | 399 | types 参数的值是类型库的名称,也就是安装到`node_modules/@types`目录中的子目录的名字。 400 | 401 | ```typescript 402 | /// 403 | ``` 404 | 405 | 上面示例中,这个三斜杠命令表示编译时添加 Node.js 的类型库,实际添加的脚本是`node_modules`目录里面的`@types/node/index.d.ts`。 406 | 407 | 可以看到,这个命令的作用类似于`import`命令。 408 | 409 | 注意,这个命令只在你自己手写类型声明文件(`.d.ts`文件)时,才有必要用到,也就是说,只应该用在`.d.ts`文件中,普通的`.ts`脚本文件不需要写这个命令。如果是普通的`.ts`脚本,可以使用`tsconfig.json`文件的`types`属性指定依赖的类型库。 410 | 411 | ### `/// ` 412 | 413 | `/// `命令允许脚本文件显式包含内置 lib 库,等同于在`tsconfig.json`文件里面使用`lib`属性指定 lib 库。 414 | 415 | 前文说过,安装 TypeScript 软件包时,会同时安装一些内置的类型声明文件,即内置的 lib 库。这些库文件位于 TypeScript 安装目录的`lib`文件夹中,它们描述了 JavaScript 语言和引擎的标准 API。 416 | 417 | 库文件并不是固定的,会随着 TypeScript 版本的升级而更新。库文件统一使用“lib.[description].d.ts”的命名方式,而`/// `里面的`lib`属性的值就是库文件名的`description`部分,比如`lib="es2015"`就表示加载库文件`lib.es2015.d.ts`。 418 | 419 | ```typescript 420 | /// 421 | ``` 422 | 423 | 上面示例中,`es2017.string`对应的库文件就是`lib.es2017.string.d.ts`。 424 | 425 | -------------------------------------------------------------------------------- /docs/declare.md: -------------------------------------------------------------------------------- 1 | # declare 关键字 2 | 3 | ## 简介 4 | 5 | declare 关键字用来告诉编译器,某个类型是存在的,可以在当前文件中使用。 6 | 7 | 它的主要作用,就是让当前文件可以使用其他文件声明的类型。举例来说,自己的脚本使用外部库定义的函数,编译器会因为不知道外部函数的类型定义而报错,这时就可以在自己的脚本里面使用`declare`关键字,告诉编译器外部函数的类型。这样的话,编译单个脚本就不会因为使用了外部类型而报错。 8 | 9 | declare 关键字可以描述以下类型。 10 | 11 | - 变量(const、let、var 命令声明) 12 | - type 或者 interface 命令声明的类型 13 | - class 14 | - enum 15 | - 函数(function) 16 | - 模块(module) 17 | - 命名空间(namespace) 18 | 19 | declare 关键字的重要特点是,它只是通知编译器某个类型是存在的,不用给出具体实现。比如,只描述函数的类型,不给出函数的实现,如果不使用`declare`,这是做不到的。 20 | 21 | declare 只能用来描述已经存在的变量和数据结构,不能用来声明新的变量和数据结构。另外,所有 declare 语句都不会出现在编译后的文件里面。 22 | 23 | ## declare variable 24 | 25 | declare 关键字可以给出外部变量的类型描述。 26 | 27 | 举例来说,当前脚本使用了其他脚本定义的全局变量`x`。 28 | 29 | ```typescript 30 | x = 123; // 报错 31 | ``` 32 | 33 | 上面示例中,变量`x`是其他脚本定义的,当前脚本不知道它的类型,编译器就会报错。 34 | 35 | 这时使用 declare 命令给出它的类型,就不会报错了。 36 | 37 | ```typescript 38 | declare let x:number; 39 | x = 1; 40 | ``` 41 | 42 | 如果 declare 关键字没有给出变量的具体类型,那么变量类型就是`any`。 43 | 44 | ```typescript 45 | declare let x; 46 | x = 1; 47 | ``` 48 | 49 | 上面示例中,变量`x`的类型为`any`。 50 | 51 | 下面的例子是脚本使用浏览器全局对象`document`。 52 | 53 | ```typescript 54 | declare var document; 55 | document.title = 'Hello'; 56 | ``` 57 | 58 | 上面示例中,declare 告诉编译器,变量`document`的类型是外部定义的(具体定义在 TypeScript 内置文件`lib.d.ts`)。 59 | 60 | 如果 TypeScript 没有找到`document`的外部定义,这里就会假定它的类型是`any`。 61 | 62 | 注意,declare 关键字只用来给出类型描述,是纯的类型代码,不允许设置变量的初始值,即不能涉及值。 63 | 64 | ```typescript 65 | // 报错 66 | declare let x:number = 1; 67 | ``` 68 | 69 | 上面示例中,declare 设置了变量的初始值,结果就报错了。 70 | 71 | ## declare function 72 | 73 | declare 关键字可以给出外部函数的类型描述。 74 | 75 | 下面是一个例子。 76 | 77 | ```typescript 78 | declare function sayHello( 79 | name:string 80 | ):void; 81 | 82 | sayHello('张三'); 83 | ``` 84 | 85 | 上面示例中,declare 命令给出了`sayHello()`的类型描述,表示这个函数是由外部文件定义的,因此这里可以直接使用该函数。 86 | 87 | 注意,这种单独的函数类型声明语句,只能用于`declare`命令后面。一方面,TypeScript 不支持单独的函数类型声明语句;另一方面,declare 关键字后面也不能带有函数的具体实现。 88 | 89 | ```typescript 90 | // 报错 91 | function sayHello( 92 | name:string 93 | ):void; 94 | 95 | let foo = 'bar'; 96 | 97 | function sayHello(name:string) { 98 | return '你好,' + name; 99 | } 100 | ``` 101 | 102 | 上面示例中,单独写函数的类型声明就会报错。 103 | 104 | ## declare class 105 | 106 | declare 给出 class 类型描述的写法如下。 107 | 108 | ```typescript 109 | declare class Animal { 110 | constructor(name:string); 111 | eat():void; 112 | sleep():void; 113 | } 114 | ``` 115 | 116 | 下面是一个复杂一点的例子。 117 | 118 | ```typescript 119 | declare class C { 120 | // 静态成员 121 | public static s0():string; 122 | private static s1:string; 123 | 124 | // 属性 125 | public a:number; 126 | private b:number; 127 | 128 | // 构造函数 129 | constructor(arg:number); 130 | 131 | // 方法 132 | m(x:number, y:number):number; 133 | 134 | // 存取器 135 | get c():number; 136 | set c(value:number); 137 | 138 | // 索引签名 139 | [index:string]:any; 140 | } 141 | ``` 142 | 143 | 同样的,declare 后面不能给出 Class 的具体实现或初始值。 144 | 145 | ## declare module,declare namespace 146 | 147 | 如果想把变量、函数、类组织在一起,可以将 declare 与 module 或 namespace 一起使用。 148 | 149 | ```typescript 150 | declare namespace AnimalLib { 151 | class Animal { 152 | constructor(name:string); 153 | eat():void; 154 | sleep():void; 155 | } 156 | 157 | type Animals = 'Fish' | 'Dog'; 158 | } 159 | 160 | // 或者 161 | declare module AnimalLib { 162 | class Animal { 163 | constructor(name:string); 164 | eat(): void; 165 | sleep(): void; 166 | } 167 | 168 | type Animals = 'Fish' | 'Dog'; 169 | } 170 | ``` 171 | 172 | 上面示例中,declare 关键字给出了 module 或 namespace 的类型描述。 173 | 174 | declare module 和 declare namespace 里面,加不加 export 关键字都可以。 175 | 176 | ```typescript 177 | declare namespace Foo { 178 | export var a: boolean; 179 | } 180 | 181 | declare module 'io' { 182 | export function readFile(filename:string):string; 183 | } 184 | ``` 185 | 186 | 上面示例中,namespace 和 module 里面使用了 export 关键字。 187 | 188 | 下面的例子是当前脚本使用了`myLib`这个外部库,它有方法`makeGreeting()`和属性`numberOfGreetings`。 189 | 190 | ```typescript 191 | let result = myLib.makeGreeting('你好'); 192 | console.log('欢迎词:' + result); 193 | 194 | let count = myLib.numberOfGreetings; 195 | ``` 196 | 197 | `myLib`的类型描述就可以这样写。 198 | 199 | ```typescript 200 | declare namespace myLib { 201 | function makeGreeting(s:string): string; 202 | let numberOfGreetings: number; 203 | } 204 | ``` 205 | 206 | declare 关键字的另一个用途,是为外部模块添加属性和方法时,给出新增部分的类型描述。 207 | 208 | ```typescript 209 | import { Foo as Bar } from 'moduleA'; 210 | 211 | declare module 'moduleA' { 212 | interface Foo { 213 | custom: { 214 | prop1: string; 215 | } 216 | } 217 | } 218 | ``` 219 | 220 | 上面示例中,从模块`moduleA`导入了类型`Foo`,它是一个接口(interface),并将其重命名为`Bar`,然后用 declare 关键字为`Foo`增加一个属性`custom`。这里需要注意的是,虽然接口`Foo`改名为`Bar`,但是扩充类型时,还是扩充原始的接口`Foo`,因为同名 interface 会自动合并类型声明。 221 | 222 | 下面是另一个例子。一个项目有多个模块,可以在一个模块中,对另一个模块的接口进行类型扩展。 223 | 224 | ```typescript 225 | // a.ts 226 | export interface A { 227 | x: number; 228 | } 229 | 230 | // b.ts 231 | import { A } from './a'; 232 | 233 | declare module './a' { 234 | interface A { 235 | y: number; 236 | } 237 | } 238 | 239 | const a:A = { x: 0, y: 0 }; 240 | ``` 241 | 242 | 上面示例中,脚本`a.ts`定义了一个接口`A`,脚本`b.ts`为这个接口添加了属性`y`。`declare module './a' {}`表示对`a.ts`里面的模块,进行类型声明,而同名 interface 会自动合并,所以等同于扩展类型。 243 | 244 | 使用这种语法进行模块的类型扩展时,有两点需要注意: 245 | 246 | (1)`declare module NAME`语法里面的模块名`NAME`,跟 import 和 export 的模块名规则是一样的,且必须跟当前文件加载该模块的语句写法(上例`import { A } from './a'`)保持一致。 247 | 248 | (2)不能创建新的顶层类型。也就是说,只能对`a.ts`模块中已经存在的类型进行扩展,不允许增加新的顶层类型,比如新定义一个接口`B`。 249 | 250 | (3)不能对默认的`default`接口进行扩展,只能对 export 命令输出的命名接口进行扩充。这是因为在进行类型扩展时,需要依赖输出的接口名。 251 | 252 | 某些第三方模块,原始作者没有提供接口类型,这时可以在自己的脚本顶部加上下面一行命令。 253 | 254 | ```typescript 255 | // 语法 256 | declare module "模块名"; 257 | 258 | // 例子 259 | declare module "hot-new-module"; 260 | ``` 261 | 262 | 加上上面的命令以后,外部模块即使没有类型声明,也可以通过编译。但是,从该模块输入的所有接口都将为`any`类型。 263 | 264 | declare module 描述的模块名可以使用通配符。 265 | 266 | ```typescript 267 | declare module 'my-plugin-*' { 268 | interface PluginOptions { 269 | enabled: boolean; 270 | priority: number; 271 | } 272 | 273 | function initialize(options: PluginOptions): void; 274 | export = initialize; 275 | } 276 | ``` 277 | 278 | 上面示例中,模块名`my-plugin-*`表示适配所有以`my-plugin-`开头的模块名(比如`my-plugin-logger`)。 279 | 280 | ## declare global 281 | 282 | 如果要为 JavaScript 引擎的原生对象添加属性和方法,可以使用`declare global {}`语法。 283 | 284 | ```typescript 285 | export {}; 286 | 287 | declare global { 288 | interface String { 289 | toSmallString(): string; 290 | } 291 | } 292 | 293 | String.prototype.toSmallString = ():string => { 294 | // 具体实现 295 | return ''; 296 | }; 297 | ``` 298 | 299 | 上面示例中,为 JavaScript 原生的`String`对象添加了`toSmallString()`方法。declare global 给出这个新增方法的类型描述。 300 | 301 | 这个示例第一行的空导出语句`export {}`,作用是强制编译器将这个脚本当作模块处理。这是因为`declare global`必须用在模块里面。 302 | 303 | 下面的示例是为 window 对象(类型接口为`Window`)添加一个属性`myAppConfig`。 304 | 305 | ```typescript 306 | export {}; 307 | 308 | declare global { 309 | interface Window { 310 | myAppConfig:object; 311 | } 312 | } 313 | 314 | const config = window.myAppConfig; 315 | ``` 316 | 317 | declare global 只能扩充现有对象的类型描述,不能增加新的顶层类型。 318 | 319 | ## declare enum 320 | 321 | declare 关键字给出 enum 类型描述的例子如下,下面的写法都是允许的。 322 | 323 | ```typescript 324 | declare enum E1 { 325 | A, 326 | B, 327 | } 328 | 329 | declare enum E2 { 330 | A = 0, 331 | B = 1, 332 | } 333 | 334 | declare const enum E3 { 335 | A, 336 | B, 337 | } 338 | 339 | declare const enum E4 { 340 | A = 0, 341 | B = 1, 342 | } 343 | ``` 344 | 345 | ## declare module 用于类型声明文件 346 | 347 | 我们可以为每个模块脚本,定义一个`.d.ts`文件,把该脚本用到的类型定义都放在这个文件里面。但是,更方便的做法是为整个项目,定义一个大的`.d.ts`文件,在这个文件里面使用`declare module`定义每个模块脚本的类型。 348 | 349 | 下面的示例是`node.d.ts`文件的一部分。 350 | 351 | ```typescript 352 | declare module "url" { 353 | export interface Url { 354 | protocol?: string; 355 | hostname?: string; 356 | pathname?: string; 357 | } 358 | 359 | export function parse( 360 | urlStr: string, 361 | parseQueryString?, 362 | slashesDenoteHost? 363 | ): Url; 364 | } 365 | 366 | declare module "path" { 367 | export function normalize(p: string): string; 368 | export function join(...paths: any[]): string; 369 | export var sep: string; 370 | } 371 | ``` 372 | 373 | 上面示例中,`url`和`path`都是单独的模块脚本,但是它们的类型都定义在`node.d.ts`这个文件里面。 374 | 375 | 另一种情况是,使用`declare module`命令,为模块名指定加载路径。 376 | 377 | ```typescript 378 | declare module "lodash" { 379 | export * from "../../dependencies/lodash"; 380 | export default from "../../dependencies/lodash"; 381 | } 382 | ``` 383 | 384 | 上面示例中,`declare module "lodash"`为模块`lodash`,指定具体的加载路径。 385 | 386 | 使用时,自己的脚本使用三斜杠命令,加载这个类型声明文件。 387 | 388 | ```typescript 389 | /// 390 | ``` 391 | 392 | 如果没有上面这一行命令,自己的脚本使用外部模块时,就需要在脚本里面使用 declare 命令单独给出外部模块的类型。 393 | 394 | ## 参考链接 395 | 396 | - [How Does The Declare Keyword Work In TypeScript?](https://timmousk.com/blog/typescript-declare/), Tim Mouskhelichvili 397 | 398 | -------------------------------------------------------------------------------- /docs/decorator-legacy.md: -------------------------------------------------------------------------------- 1 | # 装饰器(旧语法) 2 | 3 | 上一章介绍了装饰器的标准语法,那是在2022年通过成为标准的。但是在此之前,TypeScript 早在2014年就支持装饰器,不过使用的是旧语法。 4 | 5 | 装饰器的旧语法与标准语法,有相当大的差异。旧语法以后会被淘汰,但是目前大量现有项目依然在使用它,本章就介绍旧语法下的装饰器。 6 | 7 | ## experimentalDecorators 编译选项 8 | 9 | 使用装饰器的旧语法,需要打开`--experimentalDecorators`编译选项。 10 | 11 | ```bash 12 | $ tsc --target ES5 --experimentalDecorators 13 | ``` 14 | 15 | 此外,还有另外一个编译选项`--emitDecoratorMetadata`,用来产生一些装饰器的元数据,供其他工具或某些模块(比如 reflect-metadata )使用。 16 | 17 | 这两个编译选项可以在命令行设置,也可以在`tsconfig.json`文件里面进行设置。 18 | 19 | ```javascript 20 | { 21 | "compilerOptions": { 22 | "target": "ES6", 23 | "experimentalDecorators": true, 24 | "emitDecoratorMetadata": true 25 | } 26 | } 27 | ``` 28 | 29 | ## 装饰器的种类 30 | 31 | 按照所装饰的不同对象,装饰器可以分成五类。 32 | 33 | > - 类装饰器(Class Decorators):用于类。 34 | > - 属性装饰器(Property Decorators):用于属性。 35 | > - 方法装饰器(Method Decorators):用于方法。 36 | > - 存取器装饰器(Accessor Decorators):用于类的 set 或 get 方法。 37 | > - 参数装饰器(Parameter Decorators):用于方法的参数。 38 | 39 | 下面是这五种装饰器一起使用的一个示例。 40 | 41 | ```typescript 42 | @ClassDecorator() // (A) 43 | class A { 44 | 45 | @PropertyDecorator() // (B) 46 | name: string; 47 | 48 | @MethodDecorator() //(C) 49 | fly( 50 | @ParameterDecorator() // (D) 51 | meters: number 52 | ) { 53 | // code 54 | } 55 | 56 | @AccessorDecorator() // (E) 57 | get egg() { 58 | // code 59 | } 60 | set egg(e) { 61 | // code 62 | } 63 | } 64 | ``` 65 | 66 | 上面示例中,A 是类装饰器,B 是属性装饰器,C 是方法装饰器,D 是参数装饰器,E 是存取器装饰器。 67 | 68 | 注意,构造方法没有方法装饰器,只有参数装饰器。类装饰器其实就是在装饰构造方法。 69 | 70 | 另外,装饰器只能用于类,要么应用于类的整体,要么应用于类的内部成员,不能用于独立的函数。 71 | 72 | ```typescript 73 | function Decorator() { 74 | console.log('In Decorator'); 75 | } 76 | 77 | @Decorator // 报错 78 | function decorated() { 79 | console.log('in decorated'); 80 | } 81 | ``` 82 | 83 | 上面示例中,装饰器用于一个普通函数,这是无效的,结果报错。 84 | 85 | ## 类装饰器 86 | 87 | 类装饰器应用于类(class),但实际上是应用于类的构造方法。 88 | 89 | 类装饰器有唯一参数,就是构造方法,可以在装饰器内部,对构造方法进行各种改造。如果类装饰器有返回值,就会替换掉原来的构造方法。 90 | 91 | 类装饰器的类型定义如下。 92 | 93 | ```typescript 94 | type ClassDecorator = 95 | (target: TFunction) => TFunction | void; 96 | ``` 97 | 98 | 上面定义中,类型参数`TFunction`必须是函数,实际上就是构造方法。类装饰器的返回值,要么是返回处理后的原始构造方法,要么返回一个新的构造方法。 99 | 100 | 下面就是一个示例。 101 | 102 | ```typescript 103 | function f(target:any) { 104 | console.log('apply decorator') 105 | return target; 106 | } 107 | 108 | @f 109 | class A {} 110 | // 输出:apply decorator 111 | ``` 112 | 113 | 上面示例中,使用了装饰器`@f`,因此类`A`的构造方法会自动传入`f`。 114 | 115 | 类`A`不需要新建实例,装饰器也会执行。装饰器会在代码加载阶段执行,而不是在运行时执行,而且只会执行一次。 116 | 117 | 由于 TypeScript 存在编译阶段,所以装饰器对类的行为的改变,实际上发生在编译阶段。这意味着,TypeScript 装饰器能在编译阶段运行代码,也就是说,它本质就是编译时执行的函数。 118 | 119 | 下面再看一个示例。 120 | 121 | ```typescript 122 | @sealed 123 | class BugReport { 124 | type = "report"; 125 | title: string; 126 | 127 | constructor(t:string) { 128 | this.title = t; 129 | } 130 | } 131 | 132 | function sealed(constructor: Function) { 133 | Object.seal(constructor); 134 | Object.seal(constructor.prototype); 135 | } 136 | ``` 137 | 138 | 上面示例中,装饰器`@sealed()`会锁定`BugReport`这个类,使得它无法新增或删除静态成员和实例成员。 139 | 140 | 如果除了构造方法,类装饰器还需要其他参数,可以采取“工厂模式”,即把装饰器写在一个函数里面,该函数可以接受其他参数,执行后返回装饰器。但是,这样就需要调用装饰器的时候,先执行一次工厂函数。 141 | 142 | ```typescript 143 | function factory(info:string) { 144 | console.log('received: ', info); 145 | return function (target:any) { 146 | console.log('apply decorator'); 147 | return target; 148 | } 149 | } 150 | 151 | @factory('log something') 152 | class A {} 153 | ``` 154 | 155 | 上面示例中,函数`factory()`的返回值才是装饰器,所以加载装饰器的时候,要先执行一次`@factory('log something')`,才能得到装饰器。这样做的好处是,可以加入额外的参数,本例是参数`info`。 156 | 157 | 总之,`@`后面要么是一个函数名,要么是函数表达式,甚至可以写出下面这样的代码。 158 | 159 | ```typescript 160 | @((constructor: Function) => { 161 | console.log('log something'); 162 | }) 163 | class InlineDecoratorExample { 164 | // ... 165 | } 166 | ``` 167 | 168 | 上面示例中,`@`后面是一个箭头函数,这也是合法的。 169 | 170 | 类装饰器可以没有返回值,如果有返回值,就会替代所装饰的类的构造函数。由于 JavaScript 的类等同于构造函数的语法糖,所以装饰器通常返回一个新的类,对原有的类进行修改或扩展。 171 | 172 | ```typescript 173 | function decorator(target:any) { 174 | return class extends target { 175 | value = 123; 176 | }; 177 | } 178 | 179 | @decorator 180 | class Foo { 181 | value = 456; 182 | } 183 | 184 | const foo = new Foo(); 185 | console.log(foo.value); // 123 186 | ``` 187 | 188 | 上面示例中,装饰器`decorator`返回一个新的类,替代了原来的类。 189 | 190 | 上例的装饰器参数`target`类型是`any`,可以改成构造方法,这样就更准确了。 191 | 192 | ```typescript 193 | type Constructor = { 194 | new(...args: any[]): {} 195 | }; 196 | 197 | function decorator ( 198 | target: T 199 | ) { 200 | return class extends target { 201 | value = 123; 202 | }; 203 | } 204 | ``` 205 | 206 | 这时,装饰器的行为就是下面这样。 207 | 208 | ```javascript 209 | @decorator 210 | class A {} 211 | 212 | // 等同于 213 | class A {} 214 | A = decorator(A) || A; 215 | ``` 216 | 217 | 上面代码中,装饰器要么返回一个新的类`A`,要么不返回任何值,`A`保持装饰器处理后的状态。 218 | 219 | ## 方法装饰器 220 | 221 | 方法装饰器用来装饰类的方法,它的类型定义如下。 222 | 223 | ```typescript 224 | type MethodDecorator = ( 225 | target: Object, 226 | propertyKey: string|symbol, 227 | descriptor: TypedPropertyDescriptor 228 | ) => TypedPropertyDescriptor | void; 229 | ``` 230 | 231 | 方法装饰器一共可以接受三个参数。 232 | 233 | - target:(对于类的静态方法)类的构造函数,或者(对于类的实例方法)类的原型。 234 | - propertyKey:所装饰方法的方法名,类型为`string|symbol`。 235 | - descriptor:所装饰方法的描述对象。 236 | 237 | 方法装饰器的返回值(如果有的话),就是修改后的该方法的描述对象,可以覆盖原始方法的描述对象。 238 | 239 | 下面是一个示例。 240 | 241 | ```typescript 242 | function enumerable(value: boolean) { 243 | return function ( 244 | target: any, 245 | propertyKey: string, 246 | descriptor: PropertyDescriptor 247 | ) { 248 | descriptor.enumerable = value; 249 | }; 250 | } 251 | 252 | class Greeter { 253 | greeting: string; 254 | 255 | constructor(message:string) { 256 | this.greeting = message; 257 | } 258 | 259 | @enumerable(false) 260 | greet() { 261 | return 'Hello, ' + this.greeting; 262 | } 263 | } 264 | ``` 265 | 266 | 上面示例中,方法装饰器`@enumerable()`装饰 Greeter 类的`greet()`方法,作用是修改该方法的描述对象的可遍历性属性`enumerable`。`@enumerable(false)`表示将该方法修改成不可遍历。 267 | 268 | 下面再看一个例子。 269 | 270 | ```typescript 271 | function logger( 272 | target: any, 273 | propertyKey: string, 274 | descriptor: PropertyDescriptor 275 | ) { 276 | const original = descriptor.value; 277 | 278 | descriptor.value = function (...args) { 279 | console.log('params: ', ...args); 280 | const result = original.call(this, ...args); 281 | console.log('result: ', result); 282 | return result; 283 | } 284 | } 285 | 286 | class C { 287 | @logger 288 | add(x: number, y:number ) { 289 | return x + y; 290 | } 291 | } 292 | 293 | (new C()).add(1, 2) 294 | // params: 1 2 295 | // result: 3 296 | ``` 297 | 298 | 上面示例中,方法装饰器`@logger`用来装饰`add()`方法,它的作用是让该方法输出日志。每当`add()`调用一次,控制台就会打印出参数和运行结果。 299 | 300 | ## 属性装饰器 301 | 302 | 属性装饰器用来装饰属性,类型定义如下。 303 | 304 | ```typescript 305 | type PropertyDecorator = 306 | ( 307 | target: Object, 308 | propertyKey: string|symbol 309 | ) => void; 310 | ``` 311 | 312 | 属性装饰器函数接受两个参数。 313 | 314 | - target:(对于实例属性)类的原型对象(prototype),或者(对于静态属性)类的构造函数。 315 | - propertyKey:所装饰属性的属性名,注意类型有可能是字符串,也有可能是 Symbol 值。 316 | 317 | 属性装饰器不需要返回值,如果有的话,也会被忽略。 318 | 319 | 下面是一个示例。 320 | 321 | ```typescript 322 | function ValidRange(min:number, max:number) { 323 | return (target:Object, key:string) => { 324 | Object.defineProperty(target, key, { 325 | set: function(v:number) { 326 | if (v < min || v > max) { 327 | throw new Error(`Not allowed value ${v}`); 328 | } 329 | } 330 | }); 331 | } 332 | } 333 | 334 | // 输出 Installing ValidRange on year 335 | class Student { 336 | @ValidRange(1920, 2020) 337 | year!: number; 338 | } 339 | 340 | const stud = new Student(); 341 | 342 | // 报错 Not allowed value 2022 343 | stud.year = 2022; 344 | ``` 345 | 346 | 上面示例中,装饰器`ValidRange`对属性`year`设立了一个上下限检查器,只要该属性赋值时,超过了上下限,就会报错。 347 | 348 | 注意,属性装饰器的第一个参数,对于实例属性是类的原型对象,而不是实例对象(即不是`this`对象)。这是因为装饰器执行时,类还没有新建实例,所以实例对象不存在。 349 | 350 | 由于拿不到`this`,所以属性装饰器无法获得实例属性的值。这也是它没有在参数里面提供属性描述对象的原因。 351 | 352 | ```typescript 353 | function logProperty(target: Object, member: string) { 354 | const prop = Object.getOwnPropertyDescriptor(target, member); 355 | console.log(`Property ${member} ${prop}`); 356 | } 357 | 358 | class PropertyExample { 359 | @logProperty 360 | name:string = 'Foo'; 361 | } 362 | // 输出 Property name undefined 363 | ``` 364 | 365 | 上面示例中,属性装饰器`@logProperty`内部想要获取实例属性`name`的属性描述对象,结果拿到的是`undefined`。因为上例的`target`是类的原型对象,不是实例对象,所以拿不到`name`属性,也就是说`target.name`是不存在的,所以拿到的是`undefined`。只有通过`this.name`才能拿到`name`属性,但是这时`this`还不存在。 366 | 367 | 属性装饰器不仅无法获得实例属性的值,也不能初始化或修改实例属性,而且它的返回值也会被忽略。因此,它的作用很有限。 368 | 369 | 不过,如果属性装饰器设置了当前属性的存取器(getter/setter),然后在构造函数里面就可以对实例属性进行读写。 370 | 371 | ```typescript 372 | function Min(limit:number) { 373 | return function( 374 | target: Object, 375 | propertyKey: string 376 | ) { 377 | let value: string; 378 | 379 | const getter = function() { 380 | return value; 381 | }; 382 | 383 | const setter = function(newVal:string) { 384 | if(newVal.length < limit) { 385 | throw new Error(`Your password should be bigger than ${limit}`); 386 | } 387 | else { 388 | value = newVal; 389 | } 390 | }; 391 | Object.defineProperty(target, propertyKey, { 392 | get: getter, 393 | set: setter 394 | }); 395 | } 396 | } 397 | 398 | class User { 399 | username: string; 400 | 401 | @Min(8) 402 | password: string; 403 | 404 | constructor(username: string, password: string){ 405 | this.username = username; 406 | this.password = password; 407 | } 408 | } 409 | 410 | const u = new User('Foo', 'pass'); 411 | // 报错 Your password should be bigger than 8 412 | ``` 413 | 414 | 上面示例中,属性装饰器`@Min`通过设置存取器,拿到了实例属性的值。 415 | 416 | ## 存取器装饰器 417 | 418 | 存取器装饰器用来装饰类的存取器(accessor)。所谓“存取器”指的是某个属性的取值器(getter)和存值器(setter)。 419 | 420 | 存取器装饰器的类型定义,与方法装饰器一致。 421 | 422 | ```typescript 423 | type AccessorDecorator = ( 424 | target: Object, 425 | propertyKey: string|symbol, 426 | descriptor: TypedPropertyDescriptor 427 | ) => TypedPropertyDescriptor | void; 428 | ``` 429 | 430 | 存取器装饰器有三个参数。 431 | 432 | - target:(对于静态属性的存取器)类的构造函数,或者(对于实例属性的存取器)类的原型。 433 | - propertyKey:存取器的属性名。 434 | - descriptor:存取器的属性描述对象。 435 | 436 | 存取器装饰器的返回值(如果有的话),会作为该属性新的描述对象。 437 | 438 | 下面是一个示例。 439 | 440 | ```typescript 441 | function configurable(value: boolean) { 442 | return function ( 443 | target: any, 444 | propertyKey: string, 445 | descriptor: PropertyDescriptor 446 | ) { 447 | descriptor.configurable = value; 448 | }; 449 | } 450 | 451 | class Point { 452 | private _x: number; 453 | private _y: number; 454 | constructor(x:number, y:number) { 455 | this._x = x; 456 | this._y = y; 457 | } 458 | 459 | @configurable(false) 460 | get x() { 461 | return this._x; 462 | } 463 | 464 | @configurable(false) 465 | get y() { 466 | return this._y; 467 | } 468 | } 469 | ``` 470 | 471 | 上面示例中,装饰器`@configurable(false)`关闭了所装饰属性(`x`和`y`)的属性描述对象的`configurable`键(即关闭了属性的可配置性)。 472 | 473 | 下面的示例是将装饰器用来验证属性值,如果赋值不满足条件就报错。 474 | 475 | ```typescript 476 | function validator( 477 | target: Object, 478 | propertyKey: string, 479 | descriptor: PropertyDescriptor 480 | ){ 481 | const originalGet = descriptor.get; 482 | const originalSet = descriptor.set; 483 | 484 | if (originalSet) { 485 | descriptor.set = function (val) { 486 | if (val > 100) { 487 | throw new Error(`Invalid value for ${propertyKey}`); 488 | } 489 | originalSet.call(this, val); 490 | }; 491 | } 492 | } 493 | 494 | class C { 495 | #foo!: number; 496 | 497 | @validator 498 | set foo(v) { 499 | this.#foo = v; 500 | } 501 | 502 | get foo() { 503 | return this.#foo; 504 | } 505 | } 506 | 507 | const c = new C(); 508 | c.foo = 150; 509 | // 报错 510 | ``` 511 | 512 | 上面示例中,装饰器用自己定义的存值器,取代了原来的存值器,加入了验证条件。 513 | 514 | TypeScript 不允许对同一个属性的存取器(getter 和 setter)使用同一个装饰器,也就是说只能装饰两个存取器里面的一个,且必须是排在前面的那一个,否则报错。 515 | 516 | ```typescript 517 | // 报错 518 | class Person { 519 | #name:string; 520 | 521 | @Decorator 522 | set name(n:string) { 523 | this.#name = n; 524 | } 525 | 526 | @Decorator // 报错 527 | get name() { 528 | return this.#name; 529 | } 530 | } 531 | ``` 532 | 533 | 上面示例中,`@Decorator`同时装饰`name`属性的存值器和取值器,所以报错。 534 | 535 | 但是,下面的写法不会报错。 536 | 537 | ```typescript 538 | class Person { 539 | #name:string; 540 | 541 | @Decorator 542 | set name(n:string) { 543 | this.#name = n; 544 | } 545 | get name() { 546 | return this.#name; 547 | } 548 | } 549 | ``` 550 | 551 | 上面示例中,`@Decorator`只装饰它后面第一个出现的存值器(`set name()`),并不装饰取值器(`get name()`),所以不报错。 552 | 553 | 装饰器之所以不能同时用于同一个属性的存值器和取值器,原因是装饰器可以从属性描述对象上面,同时拿到取值器和存值器,因此只调用一次就够了。 554 | 555 | ## 参数装饰器 556 | 557 | 参数装饰器用来装饰构造方法或者其他方法的参数。它的类型定义如下。 558 | 559 | ```typescript 560 | type ParameterDecorator = ( 561 | target: Object, 562 | propertyKey: string|symbol, 563 | parameterIndex: number 564 | ) => void; 565 | ``` 566 | 567 | 参数装饰器接受三个参数。 568 | 569 | - target:(对于静态方法)类的构造函数,或者(对于类的实例方法)类的原型对象。 570 | - propertyKey:所装饰的方法的名字,类型为`string|symbol`。 571 | - parameterIndex:当前参数在方法的参数序列的位置(从0开始)。 572 | 573 | 该装饰器不需要返回值,如果有的话会被忽略。 574 | 575 | 下面是一个示例。 576 | 577 | ```typescript 578 | function log( 579 | target: Object, 580 | propertyKey: string|symbol, 581 | parameterIndex: number 582 | ) { 583 | console.log(`${String(propertyKey)} NO.${parameterIndex} Parameter`); 584 | } 585 | 586 | class C { 587 | member( 588 | @log x:number, 589 | @log y:number 590 | ) { 591 | console.log(`member Parameters: ${x} ${y}`); 592 | } 593 | } 594 | 595 | const c = new C(); 596 | c.member(5, 5); 597 | // member NO.1 Parameter 598 | // member NO.0 Parameter 599 | // member Parameters: 5 5 600 | ``` 601 | 602 | 上面示例中,参数装饰器会输出参数的位置序号。注意,后面的参数会先输出。 603 | 604 | 跟其他装饰器不同,参数装饰器主要用于输出信息,没有办法修改类的行为。 605 | 606 | ## 装饰器的执行顺序 607 | 608 | 前面说过,装饰器只会执行一次,就是在代码解析时执行,哪怕根本没有调用类新建实例,也会执行,而且从此就不再执行了。 609 | 610 | 执行装饰器时,按照如下顺序执行。 611 | 612 | 1. 实例相关的装饰器。 613 | 1. 静态相关的装饰器。 614 | 1. 构造方法的参数装饰器。 615 | 1. 类装饰器。 616 | 617 | 请看下面的示例。 618 | 619 | ```typescript 620 | function f(key:string):any { 621 | return function () { 622 | console.log('执行:', key); 623 | }; 624 | } 625 | 626 | @f('类装饰器') 627 | class C { 628 | @f('静态方法') 629 | static method() {} 630 | 631 | @f('实例方法') 632 | method() {} 633 | 634 | constructor(@f('构造方法参数') foo:any) {} 635 | } 636 | ``` 637 | 638 | 加载上面的示例,输出如下。 639 | 640 | ```typescript 641 | 执行: 实例方法 642 | 执行: 静态方法 643 | 执行: 构造方法参数 644 | 执行: 类装饰器 645 | ``` 646 | 647 | 同一级装饰器的执行顺序,是按照它们的代码顺序。但是,参数装饰器的执行总是早于方法装饰器。 648 | 649 | ```typescript 650 | function f(key:string):any { 651 | return function () { 652 | console.log('执行:', key); 653 | }; 654 | } 655 | 656 | class C { 657 | @f('方法1') 658 | m1(@f('参数1') foo:any) {} 659 | 660 | @f('属性1') 661 | p1: number; 662 | 663 | @f('方法2') 664 | m2(@f('参数2') foo:any) {} 665 | 666 | @f('属性2') 667 | p2: number; 668 | } 669 | ``` 670 | 671 | 加载上面的示例,输出如下。 672 | 673 | ```typescript 674 | 执行: 参数1 675 | 执行: 方法1 676 | 执行: 属性1 677 | 执行: 参数2 678 | 执行: 方法2 679 | 执行: 属性2 680 | ``` 681 | 682 | 上面示例中,实例装饰器的执行顺序,完全是按照代码顺序的。但是,同一个方法的参数装饰器,总是早于该方法的方法装饰器执行。 683 | 684 | 如果同一个方法或属性有多个装饰器,那么装饰器将顺序加载、逆序执行。 685 | 686 | ```typescript 687 | function f(key:string):any { 688 | console.log('加载:', key); 689 | return function () { 690 | console.log('执行:', key); 691 | }; 692 | } 693 | 694 | class C { 695 | @f('A') 696 | @f('B') 697 | @f('C') 698 | m1() {} 699 | } 700 | // 加载: A 701 | // 加载: B 702 | // 加载: C 703 | // 执行: C 704 | // 执行: B 705 | // 执行: A 706 | ``` 707 | 708 | 如果同一个方法有多个参数,那么参数也是顺序加载、逆序执行。 709 | 710 | ```typescript 711 | function f(key:string):any { 712 | console.log('加载:', key); 713 | return function () { 714 | console.log('执行:', key); 715 | }; 716 | } 717 | 718 | class C { 719 | method( 720 | @f('A') a:any, 721 | @f('B') b:any, 722 | @f('C') c:any, 723 | ) {} 724 | } 725 | // 加载: A 726 | // 加载: B 727 | // 加载: C 728 | // 执行: C 729 | // 执行: B 730 | // 执行: A 731 | ``` 732 | 733 | ## 为什么装饰器不能用于函数? 734 | 735 | 装饰器只能用于类和类的方法,不能用于函数,主要原因是存在函数提升。 736 | 737 | JavaScript 的函数不管在代码的什么位置,都会提升到代码顶部。 738 | 739 | ```typescript 740 | addOne(1); 741 | function addOne(n:number) { 742 | return n + 1; 743 | } 744 | ``` 745 | 746 | 上面示例中,函数`addOne()`不会因为在定义之前执行而报错,原因就是函数存在提升,会自动提升到代码顶部。 747 | 748 | 如果允许装饰器可以用于普通函数,那么就有可能导致意想不到的情况。 749 | 750 | ```typescript 751 | let counter = 0; 752 | 753 | let add = function (target:any) { 754 | counter++; 755 | }; 756 | 757 | @add 758 | function foo() { 759 | //... 760 | } 761 | ``` 762 | 763 | 上面示例中,本来的意图是装饰器`@add`每使用一次,变量`counter`就加`1`,但是实际上会报错,因为函数提升的存在,使得实际执行的代码是下面这样。 764 | 765 | ```javascript 766 | @add // 报错 767 | function foo() { 768 | //... 769 | } 770 | 771 | let counter = 0; 772 | let add = function (target:any) { 773 | counter++; 774 | }; 775 | ``` 776 | 777 | 上面示例中,`@add`还没有定义就调用了,从而报错。 778 | 779 | 总之,由于存在函数提升,使得装饰器不能用于函数。类是不会提升的,所以就没有这方面的问题。 780 | 781 | 另一方面,如果一定要装饰函数,可以采用高阶函数的形式直接执行,没必要写成装饰器。 782 | 783 | ```javascript 784 | function doSomething(name) { 785 | console.log('Hello, ' + name); 786 | } 787 | 788 | function loggingDecorator(wrapped) { 789 | return function() { 790 | console.log('Starting'); 791 | const result = wrapped.apply(this, arguments); 792 | console.log('Finished'); 793 | return result; 794 | } 795 | } 796 | 797 | const wrapped = loggingDecorator(doSomething); 798 | ``` 799 | 800 | 上面示例中,`loggingDecorator()`是一个装饰器,只要把原始函数传入它执行,就能起到装饰器的效果。 801 | 802 | ## 多个装饰器的合成 803 | 804 | 多个装饰器可以应用于同一个目标对象,可以写在一行。 805 | 806 | ```typescript 807 | @f @g x 808 | ``` 809 | 810 | 上面示例中,装饰器`@f`和`@g`同时装饰目标对象`x`。 811 | 812 | 多个装饰器也可以写成多行。 813 | 814 | ```typescript 815 | @f 816 | @g 817 | x 818 | ``` 819 | 820 | 多个装饰器的效果,类似于函数的合成,按照从里到外的顺序执行。对于上例来说,就是执行`f(g(x))`。 821 | 822 | 前面也说过,如果`f`和`g`是表达式,那么需要先从外到里求值。 823 | 824 | ## 参考链接 825 | 826 | - [A Complete Guide to TypeScript Decorators](https://saul-mirone.github.io/a-complete-guide-to-typescript-decorator/), by Saul Mirone 827 | - [Deep introduction to using and implementing TypeScript decorators](https://techsparx.com/nodejs/typescript/decorators/introduction.html), by David Herron 828 | - [Deep introduction to property decorators in TypeScript](https://techsparx.com/nodejs/typescript/decorators/properties.html), by David Herron 829 | - [Deep introduction to accessor decorators in TypeScript](https://techsparx.com/nodejs/typescript/decorators/accessors.html), by David Herron 830 | - [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 831 | 832 | -------------------------------------------------------------------------------- /docs/enum.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的 Enum 类型 2 | 3 | Enum 是 TypeScript 新增的一种数据结构和类型,中文译为“枚举”。 4 | 5 | ## 简介 6 | 7 | 实际开发中,经常需要定义一组相关的常量。 8 | 9 | ```typescript 10 | const RED = 1; 11 | const GREEN = 2; 12 | const BLUE = 3; 13 | 14 | let color = userInput(); 15 | 16 | if (color === RED) {/* */} 17 | if (color === GREEN) {/* */} 18 | if (color === BLUE) {/* */} 19 | 20 | throw new Error('wrong color'); 21 | ``` 22 | 23 | 上面示例中,常量`RED`、`GREEN`、`BLUE`是相关的,意为变量`color`的三个可能的取值。它们具体等于什么值其实并不重要,只要不相等就可以了。 24 | 25 | TypeScript 就设计了 Enum 结构,用来将相关常量放在一个容器里面,方便使用。 26 | 27 | ```typescript 28 | enum Color { 29 | Red, // 0 30 | Green, // 1 31 | Blue // 2 32 | } 33 | ``` 34 | 35 | 上面示例声明了一个 Enum 结构`Color`,里面包含三个成员`Red`、`Green`和`Blue`。第一个成员的值默认为整数`0`,第二个为`1`,第三个为`2`,以此类推。 36 | 37 | 使用时,调用 Enum 的某个成员,与调用对象属性的写法一样,可以使用点运算符,也可以使用方括号运算符。 38 | 39 | ```typescript 40 | let c = Color.Green; // 1 41 | // 等同于 42 | let c = Color['Green']; // 1 43 | ``` 44 | 45 | Enum 结构本身也是一种类型。比如,上例的变量`c`等于`1`,它的类型可以是 Color,也可以是`number`。 46 | 47 | ```typescript 48 | let c:Color = Color.Green; // 正确 49 | let c:number = Color.Green; // 正确 50 | ``` 51 | 52 | 上面示例中,变量`c`的类型写成`Color`或`number`都可以。但是,`Color`类型的语义更好。 53 | 54 | Enum 结构的特别之处在于,它既是一种类型,也是一个值。绝大多数 TypeScript 语法都是类型语法,编译后会全部去除,但是 Enum 结构是一个值,编译后会变成 JavaScript 对象,留在代码中。 55 | 56 | ```typescript 57 | // 编译前 58 | enum Color { 59 | Red, // 0 60 | Green, // 1 61 | Blue // 2 62 | } 63 | 64 | // 编译后 65 | let Color = { 66 | Red: 0, 67 | Green: 1, 68 | Blue: 2 69 | }; 70 | ``` 71 | 72 | 上面示例是 Enum 结构编译前后的对比。 73 | 74 | 由于 TypeScript 的定位是 JavaScript 语言的类型增强,所以官方建议谨慎使用 Enum 结构,因为它不仅仅是类型,还会为编译后的代码加入一个对象。 75 | 76 | Enum 结构比较适合的场景是,成员的值不重要,名字更重要,从而增加代码的可读性和可维护性。 77 | 78 | ```typescript 79 | enum Operator { 80 | ADD, 81 | DIV, 82 | MUL, 83 | SUB 84 | } 85 | 86 | function compute( 87 | op:Operator, 88 | a:number, 89 | b:number 90 | ) { 91 | switch (op) { 92 | case Operator.ADD: 93 | return a + b; 94 | case Operator.DIV: 95 | return a / b; 96 | case Operator.MUL: 97 | return a * b; 98 | case Operator.SUB: 99 | return a - b; 100 | default: 101 | throw new Error('wrong operator'); 102 | } 103 | } 104 | 105 | compute(Operator.ADD, 1, 3) // 4 106 | ``` 107 | 108 | 上面示例中,Enum 结构`Operator`的四个成员表示四则运算“加减乘除”。代码根本不需要用到这四个成员的值,只用成员名就够了。 109 | 110 | [TypeScript 5.0](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-5-0.html#enum-overhaul) 之前,Enum 有一个 Bug,就是 Enum 类型的变量可以赋值为任何数值。 111 | 112 | ```typescript 113 | enum Bool { 114 | No, 115 | Yes 116 | } 117 | 118 | function foo(noYes:Bool) { 119 | // ... 120 | } 121 | 122 | foo(33); // TypeScript 5.0 之前不报错 123 | ``` 124 | 125 | 上面示例中,函数`foo`的参数`noYes`是 Enum 类型,只有两个可用的值。但是,TypeScript 5.0 之前,任何数值作为函数`foo`的参数,编译都不会报错,TypeScript 5.0 纠正了这个问题。 126 | 127 | 另外,由于 Enum 结构编译后是一个对象,所以不能有与它同名的变量(包括对象、函数、类等)。 128 | 129 | ```typescript 130 | enum Color { 131 | Red, 132 | Green, 133 | Blue 134 | } 135 | 136 | const Color = 'red'; // 报错 137 | ``` 138 | 139 | 上面示例,Enum 结构与变量同名,导致报错。 140 | 141 | 很大程度上,Enum 结构可以被对象的`as const`断言替代。 142 | 143 | ```typescript 144 | enum Foo { 145 | A, 146 | B, 147 | C, 148 | } 149 | 150 | const Bar = { 151 | A: 0, 152 | B: 1, 153 | C: 2, 154 | } as const; 155 | 156 | if (x === Foo.A) {} 157 | // 等同于 158 | if (x === Bar.A) {} 159 | ``` 160 | 161 | 上面示例中,对象`Bar`使用了`as const`断言,作用就是使得它的属性无法修改。这样的话,`Foo`和`Bar`的行为就很类似了,前者完全可以用后者替代,而且后者还是 JavaScript 的原生数据结构。 162 | 163 | ## Enum 成员的值 164 | 165 | Enum 成员默认不必赋值,系统会从零开始逐一递增,按照顺序为每个成员赋值,比如0、1、2…… 166 | 167 | 但是,也可以为 Enum 成员显式赋值。 168 | 169 | ```typescript 170 | enum Color { 171 | Red, 172 | Green, 173 | Blue 174 | } 175 | 176 | // 等同于 177 | enum Color { 178 | Red = 0, 179 | Green = 1, 180 | Blue = 2 181 | } 182 | ``` 183 | 184 | 上面示例中,Enum 每个成员的值都是显式赋值。 185 | 186 | 成员的值可以是任意数值,但不能是大整数(Bigint)。 187 | 188 | ```typescript 189 | enum Color { 190 | Red = 90, 191 | Green = 0.5, 192 | Blue = 7n // 报错 193 | } 194 | ``` 195 | 196 | 上面示例中,Enum 成员的值可以是小数,但不能是 Bigint。 197 | 198 | 成员的值甚至可以相同。 199 | 200 | ```typescript 201 | enum Color { 202 | Red = 0, 203 | Green = 0, 204 | Blue = 0 205 | } 206 | ``` 207 | 208 | 如果只设定第一个成员的值,后面成员的值就会从这个值开始递增。 209 | 210 | ```typescript 211 | enum Color { 212 | Red = 7, 213 | Green, // 8 214 | Blue // 9 215 | } 216 | 217 | // 或者 218 | enum Color { 219 | Red, // 0 220 | Green = 7, 221 | Blue // 8 222 | } 223 | ``` 224 | 225 | Enum 成员的值也可以使用计算式。 226 | 227 | ```typescript 228 | enum Permission { 229 | UserRead = 1 << 8, 230 | UserWrite = 1 << 7, 231 | UserExecute = 1 << 6, 232 | GroupRead = 1 << 5, 233 | GroupWrite = 1 << 4, 234 | GroupExecute = 1 << 3, 235 | AllRead = 1 << 2, 236 | AllWrite = 1 << 1, 237 | AllExecute = 1 << 0, 238 | } 239 | 240 | enum Bool { 241 | No = 123, 242 | Yes = Math.random(), 243 | } 244 | ``` 245 | 246 | 上面示例中,Enum 成员的值等于一个计算式,或者等于函数的返回值,都是正确的。 247 | 248 | Enum 成员值都是只读的,不能重新赋值。 249 | 250 | ```typescript 251 | enum Color { 252 | Red, 253 | Green, 254 | Blue 255 | } 256 | 257 | Color.Red = 4; // 报错 258 | ``` 259 | 260 | 上面示例中,重新为 Enum 成员赋值就会报错。 261 | 262 | 为了让这一点更醒目,通常会在 enum 关键字前面加上`const`修饰,表示这是常量,不能再次赋值。 263 | 264 | ```typescript 265 | const enum Color { 266 | Red, 267 | Green, 268 | Blue 269 | } 270 | ``` 271 | 272 | 加上`const`还有一个好处,就是编译为 JavaScript 代码后,代码中 Enum 成员会被替换成对应的值,这样能提高性能表现。 273 | 274 | ```typescript 275 | const enum Color { 276 | Red, 277 | Green, 278 | Blue 279 | } 280 | 281 | const x = Color.Red; 282 | const y = Color.Green; 283 | const z = Color.Blue; 284 | 285 | // 编译后 286 | const x = 0 /* Color.Red */; 287 | const y = 1 /* Color.Green */; 288 | const z = 2 /* Color.Blue */; 289 | ``` 290 | 291 | 上面示例中,由于 Enum 结构前面加了`const`关键字,所以编译产物里面就没有生成对应的对象,而是把所有 Enum 成员出现的场合,都替换成对应的常量。 292 | 293 | 如果希望加上`const`关键词后,运行时还能访问 Enum 结构(即编译后依然将 Enum 转成对象),需要在编译时打开`preserveConstEnums`编译选项。 294 | 295 | ## 同名 Enum 的合并 296 | 297 | 多个同名的 Enum 结构会自动合并。 298 | 299 | ```typescript 300 | enum Foo { 301 | A, 302 | } 303 | 304 | enum Foo { 305 | B = 1, 306 | } 307 | 308 | enum Foo { 309 | C = 2, 310 | } 311 | 312 | // 等同于 313 | enum Foo { 314 | A, 315 | B = 1, 316 | C = 2 317 | } 318 | ``` 319 | 320 | 上面示例中,`Foo`分成三段定义,系统会自动把它们合并。 321 | 322 | Enum 结构合并时,只允许其中一个的首成员省略初始值,否则报错。 323 | 324 | ```typescript 325 | enum Foo { 326 | A, 327 | } 328 | 329 | enum Foo { 330 | B, // 报错 331 | } 332 | ``` 333 | 334 | 上面示例中,`Foo`的两段定义的第一个成员,都没有设置初始值,导致报错。 335 | 336 | 同名 Enum 合并时,不能有同名成员,否则报错。 337 | 338 | ```typescript 339 | enum Foo { 340 | A, 341 | B 342 | } 343 | 344 | enum Foo { 345 | B = 1, // 报错 346 | C 347 | } 348 | ``` 349 | 350 | 上面示例中,`Foo`的两段定义有一个同名成员`B`,导致报错。 351 | 352 | 同名 Enum 合并的另一个限制是,所有定义必须同为 const 枚举或者非 const 枚举,不允许混合使用。 353 | 354 | ```typescript 355 | // 正确 356 | enum E { 357 | A, 358 | } 359 | enum E { 360 | B = 1, 361 | } 362 | 363 | // 正确 364 | const enum E { 365 | A, 366 | } 367 | const enum E { 368 | B = 1, 369 | } 370 | 371 | // 报错 372 | enum E { 373 | A, 374 | } 375 | const enum E { 376 | B = 1, 377 | } 378 | ``` 379 | 380 | 同名 Enum 的合并,最大用处就是补充外部定义的 Enum 结构。 381 | 382 | ## 字符串 Enum 383 | 384 | Enum 成员的值除了设为数值,还可以设为字符串。也就是说,Enum 也可以用作一组相关字符串的集合。 385 | 386 | ```typescript 387 | enum Direction { 388 | Up = 'UP', 389 | Down = 'DOWN', 390 | Left = 'LEFT', 391 | Right = 'RIGHT', 392 | } 393 | ``` 394 | 395 | 上面示例中,`Direction`就是字符串枚举,每个成员的值都是字符串。 396 | 397 | 注意,字符串枚举的所有成员值,都必须显式设置。如果没有设置,成员值默认为数值,且位置必须在字符串成员之前。 398 | 399 | ```typescript 400 | enum Foo { 401 | A, // 0 402 | B = 'hello', 403 | C // 报错 404 | } 405 | ``` 406 | 407 | 上面示例中,`A`之前没有其他成员,所以可以不设置初始值,默认等于`0`;`C`之前有一个字符串成员,所以`C`必须有初始值,不赋值就报错了。 408 | 409 | Enum 成员可以是字符串和数值混合赋值。 410 | 411 | ```typescript 412 | enum Enum { 413 | One = 'One', 414 | Two = 'Two', 415 | Three = 3, 416 | Four = 4, 417 | } 418 | ``` 419 | 420 | 除了数值和字符串,Enum 成员不允许使用其他值(比如 Symbol 值)。 421 | 422 | 变量类型如果是字符串 Enum,就不能再赋值为字符串,这跟数值 Enum 不一样。 423 | 424 | ```typescript 425 | enum MyEnum { 426 | One = 'One', 427 | Two = 'Two', 428 | } 429 | 430 | let s = MyEnum.One; 431 | s = 'One'; // 报错 432 | ``` 433 | 434 | 上面示例中,变量`s`的类型是`MyEnum`,再赋值为字符串就报错。 435 | 436 | 由于这个原因,如果函数的参数类型是字符串 Enum,传参时就不能直接传入字符串,而要传入 Enum 成员。 437 | 438 | ```typescript 439 | enum MyEnum { 440 | One = 'One', 441 | Two = 'Two', 442 | } 443 | 444 | function f(arg:MyEnum) { 445 | return 'arg is ' + arg; 446 | } 447 | 448 | f('One') // 报错 449 | ``` 450 | 451 | 上面示例中,参数类型是`MyEnum`,直接传入字符串会报错。 452 | 453 | 所以,字符串 Enum 作为一种类型,有限定函数参数的作用。 454 | 455 | 前面说过,数值 Enum 的成员值往往不重要。但是有些场合,开发者可能希望 Enum 成员值可以保存一些有用的信息,所以 TypeScript 才设计了字符串 Enum。 456 | 457 | ```typescript 458 | const enum MediaTypes { 459 | JSON = 'application/json', 460 | XML = 'application/xml', 461 | } 462 | 463 | const url = 'localhost'; 464 | 465 | fetch(url, { 466 | headers: { 467 | Accept: MediaTypes.JSON, 468 | }, 469 | }).then(response => { 470 | // ... 471 | }); 472 | ``` 473 | 474 | 上面示例中,函数`fetch()`的参数对象的属性`Accept`,只能接受一些指定的字符串。这时就很适合把字符串放进一个 Enum 结构,通过成员值来引用这些字符串。 475 | 476 | 字符串 Enum 可以使用联合类型(union)代替。 477 | 478 | ```typescript 479 | function move( 480 | where:'Up'|'Down'|'Left'|'Right' 481 | ) { 482 | // ... 483 | } 484 | ``` 485 | 486 | 上面示例中,函数参数`where`属于联合类型,效果跟指定为字符串 Enum 是一样的。 487 | 488 | 注意,字符串 Enum 的成员值,不能使用表达式赋值。 489 | 490 | ```typescript 491 | enum MyEnum { 492 | A = 'one', 493 | B = ['T', 'w', 'o'].join('') // 报错 494 | } 495 | ``` 496 | 497 | 上面示例中,成员`B`的值是一个字符串表达式,导致报错。 498 | 499 | ## keyof 运算符 500 | 501 | keyof 运算符可以取出 Enum 结构的所有成员名,作为联合类型返回。 502 | 503 | ```typescript 504 | enum MyEnum { 505 | A = 'a', 506 | B = 'b' 507 | } 508 | 509 | // 'A'|'B' 510 | type Foo = keyof typeof MyEnum; 511 | ``` 512 | 513 | 上面示例中,`keyof typeof MyEnum`可以取出`MyEnum`的所有成员名,所以类型`Foo`等同于联合类型`'A'|'B'`。 514 | 515 | 注意,这里的`typeof`是必需的,否则`keyof MyEnum`相当于`keyof string`。 516 | 517 | ```typescript 518 | type Foo = keyof MyEnum; 519 | // number | typeof Symbol.iterator | "toString" | "charAt" | "charCodeAt" | ... 520 | ``` 521 | 522 | 上面示例中,类型`Foo`等于类型`string`的所有原生属性名组成的联合类型。这是`MyEnum`为字符串 Enum 的结果,如果`MyEnum`是数值 Enum,那么`keyof MyEnum`相当于`keyof number`。 523 | 524 | 这是因为 Enum 作为类型,本质上属于`number`或`string`的一种变体,而`typeof MyEnum`会将`MyEnum`当作一个值处理,从而先其转为对象类型,就可以再用`keyof`运算符返回该对象的所有属性名。 525 | 526 | 如果要返回 Enum 所有的成员值,可以使用`in`运算符。 527 | 528 | ```typescript 529 | enum MyEnum { 530 | A = 'a', 531 | B = 'b' 532 | } 533 | 534 | // { a: any, b: any } 535 | type Foo = { [key in MyEnum]: any }; 536 | ``` 537 | 538 | 上面示例中,采用属性索引可以取出`MyEnum`的所有成员值。 539 | 540 | ## 反向映射 541 | 542 | 数值 Enum 存在反向映射,即可以通过成员值获得成员名。 543 | 544 | ```typescript 545 | enum Weekdays { 546 | Monday = 1, 547 | Tuesday, 548 | Wednesday, 549 | Thursday, 550 | Friday, 551 | Saturday, 552 | Sunday 553 | } 554 | 555 | console.log(Weekdays[3]) // Wednesday 556 | ``` 557 | 558 | 上面示例中,Enum 成员`Wednesday`的值等于3,从而可以从成员值`3`取到对应的成员名`Wednesday`,这就叫反向映射。 559 | 560 | 这是因为 TypeScript 会将上面的 Enum 结构,编译成下面的 JavaScript 代码。 561 | 562 | ```javascript 563 | var Weekdays; 564 | (function (Weekdays) { 565 | Weekdays[Weekdays["Monday"] = 1] = "Monday"; 566 | Weekdays[Weekdays["Tuesday"] = 2] = "Tuesday"; 567 | Weekdays[Weekdays["Wednesday"] = 3] = "Wednesday"; 568 | Weekdays[Weekdays["Thursday"] = 4] = "Thursday"; 569 | Weekdays[Weekdays["Friday"] = 5] = "Friday"; 570 | Weekdays[Weekdays["Saturday"] = 6] = "Saturday"; 571 | Weekdays[Weekdays["Sunday"] = 7] = "Sunday"; 572 | })(Weekdays || (Weekdays = {})); 573 | ``` 574 | 575 | 上面代码中,实际进行了两组赋值,以第一个成员为例。 576 | 577 | ```javascript 578 | Weekdays[ 579 | Weekdays["Monday"] = 1 580 | ] = "Monday"; 581 | ``` 582 | 583 | 上面代码有两个赋值运算符(`=`),实际上等同于下面的代码。 584 | 585 | ```javascript 586 | Weekdays["Monday"] = 1; 587 | Weekdays[1] = "Monday"; 588 | ``` 589 | 590 | 注意,这种情况只发生在数值 Enum,对于字符串 Enum,不存在反向映射。这是因为字符串 Enum 编译后只有一组赋值。 591 | 592 | ```typescript 593 | enum MyEnum { 594 | A = 'a', 595 | B = 'b' 596 | } 597 | 598 | // 编译后 599 | var MyEnum; 600 | (function (MyEnum) { 601 | MyEnum["A"] = "a"; 602 | MyEnum["B"] = "b"; 603 | })(MyEnum || (MyEnum = {})); 604 | ``` 605 | 606 | -------------------------------------------------------------------------------- /docs/es6.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的 ES6 类型 2 | 3 | ## `Map` 4 | 5 | ```typescript 6 | let map2 = new Map(); // Key any, value any 7 | let map3 = new Map(); // Key string, value number 8 | ``` 9 | 10 | TypeScript 使用 Map 类型,描述 Map 结构。 11 | 12 | ```typescript 13 | const myMap: Map = new Map([ 14 | [false, 'no'], 15 | [true, 'yes'], 16 | ]); 17 | ``` 18 | 19 | Map 是一个泛型,使用时,比如给出类型变量。 20 | 21 | 由于存在类型推断,也可以省略类型参数。 22 | 23 | ```typescript 24 | const myMap = new Map([ 25 | [false, 'no'], 26 | [true, 'yes'], 27 | ]); 28 | ``` 29 | 30 | ## `Set` 31 | 32 | ## `Promise` 33 | 34 | ## async 函数 35 | 36 | async 函数的的返回值是一个 Promise 对象。 37 | 38 | ```typescript 39 | const p:Promise = /* ... */; 40 | 41 | async function fn(): Promise { 42 | var i = await p; 43 | return i + 1; 44 | } 45 | ``` 46 | 47 | ## `Iterable` 48 | 49 | 对象只要部署了 Iterator 接口,就可以用`for...of`循环遍历。Generator 函数(生成器)返回的就是一个具有 Iterator 接口的对象。 50 | 51 | TypeScript 使用泛型`Iterable`表示具有 Iterator 接口的对象,其中`T`表示 Iterator 接口包含的值类型(每一轮遍历获得的值)。 52 | 53 | ```typescript 54 | interface Iterable { 55 | [Symbol.iterator](): Iterator; 56 | } 57 | ``` 58 | 59 | 上面是`Iterable`接口的定义,表示一个具有`Symbol.iterator`属性的对象,该属性是一个函数,调用后返回的是一个 Iterator 对象。 60 | 61 | Iterator 对象必须具有`next()`方法,另外还具有两个可选方法`return()`和`throw()`,类型表述如下。 62 | 63 | ```typescript 64 | interface Iterator { 65 | next(value?: any): IteratorResult; 66 | return?(value?: any): IteratorResult; 67 | throw?(e?: any): IteratorResult; 68 | } 69 | ``` 70 | 71 | 上面的类型定义中,可以看到`next()`、`return()`、`throw()`这三个方法的返回值是一个部署了`IteratorResult`接口的对象。 72 | 73 | `IteratorResult`接口的定义如下。 74 | 75 | ```typescript 76 | interface IteratorResult { 77 | done: boolean; //表示遍历是否结束 78 | value: T; // 当前遍历得到的值 79 | } 80 | ``` 81 | 82 | 上面的类型定义表示,Iterator 对象的`next()`等方法的返回值,具有`done`和`value`两个属性。 83 | 84 | 下面的例子是 Generator 函数返回一个具有 Iterator 接口的对象。 85 | 86 | ```typescript 87 | function* g():Iterable { 88 | for (var i = 0; i < 100; i++) { 89 | yield ''; 90 | } 91 | yield* otherStringGenerator(); 92 | } 93 | ``` 94 | 95 | 上面示例中,生成器`g()`返回的类型是`Iterable`,其中`string`表示 Iterator 接口包含的是字符串。 96 | 97 | 这个例子的类型声明可以省略,因为 TypeScript 可以自己推断出来 Iterator 接口的类型。 98 | 99 | ```typescript 100 | function* g() { 101 | for (var i = 0; i < 100; i++) { 102 | yield ""; // infer string 103 | } 104 | yield* otherStringGenerator(); 105 | } 106 | ``` 107 | 108 | 另外,扩展运算符(`...`)后面的值必须具有 Iterator 接口,下面是一个例子。 109 | 110 | ```typescript 111 | function toArray(xs: Iterable):X[] { 112 | return [...xs] 113 | } 114 | ``` 115 | 116 | ## Generator 函数 117 | 118 | Generator 函数返回一个同时具有 Iterable 接口(具有`[Symbol.iterator]`属性)和 Iterator 接口(具有`next()`方法)的对象,因此 TypeScript 提供了一个泛型`IterableIterator`,表示同时满足`Iterable`和`Iterator`两个接口。 119 | 120 | ```typescript 121 | interface IterableIterator extends Iterator { 122 | [Symbol.iterator](): IterableIterator; 123 | } 124 | ``` 125 | 126 | 上面类型定义中,`IterableIterator`接口就是在`Iterator`接口的基础上,加上`[Symbol.iterator]`属性。 127 | 128 | 下面是一个例子。 129 | 130 | ```typescript 131 | function* createNumbers(): IterableIterator { 132 | let n = 0; 133 | while (1) { 134 | yield n++; 135 | } 136 | } 137 | 138 | let numbers = createNumbers() 139 | 140 | // {value: 0, done: false} 141 | numbers.next() 142 | 143 | // {value: 1, done: false} 144 | numbers.next() 145 | 146 | // {value: 2, done: false} 147 | numbers.next() 148 | ``` 149 | 150 | 上面示例中,`createNumbers()`返回的对象`numbers`即具有`next()`方法,也具有`[Symbol.iterator]`属性,所以满足`IterableIterator`接口。 151 | 152 | ## 参考链接 153 | 154 | - [Typing Iterables and Iterators with TypeScript](https://www.geekabyte.io/2019/06/typing-iterables-and-iterators-with.html) -------------------------------------------------------------------------------- /docs/generics.md: -------------------------------------------------------------------------------- 1 | # TypeScript 泛型 2 | 3 | ## 简介 4 | 5 | 有些时候,函数返回值的类型与参数类型是相关的。 6 | 7 | ```javascript 8 | function getFirst(arr) { 9 | return arr[0]; 10 | } 11 | ``` 12 | 13 | 上面示例中,函数`getFirst()`总是返回参数数组的第一个成员。参数数组是什么类型,返回值就是什么类型。 14 | 15 | 这个函数的类型声明只能写成下面这样。 16 | 17 | ```typescript 18 | function f(arr:any[]):any { 19 | return arr[0]; 20 | } 21 | ``` 22 | 23 | 上面的类型声明,就反映不出参数与返回值之间的类型关系。 24 | 25 | 为了解决这个问题,TypeScript 就引入了“泛型”(generics)。泛型的特点就是带有“类型参数”(type parameter)。 26 | 27 | ```typescript 28 | function getFirst(arr:T[]):T { 29 | return arr[0]; 30 | } 31 | ``` 32 | 33 | 上面示例中,函数`getFirst()`的函数名后面尖括号的部分``,就是类型参数,参数要放在一对尖括号(`<>`)里面。本例只有一个类型参数`T`,可以将其理解为类型声明需要的变量,需要在调用时传入具体的参数类型。 34 | 35 | 上例的函数`getFirst()`的参数类型是`T[]`,返回值类型是`T`,就清楚地表示了两者之间的关系。比如,输入的参数类型是`number[]`,那么 T 的值就是`number`,因此返回值类型也是`number`。 36 | 37 | 函数调用时,需要提供类型参数。 38 | 39 | ```typescript 40 | getFirst([1, 2, 3]) 41 | ``` 42 | 43 | 上面示例中,调用函数`getFirst()`时,需要在函数名后面使用尖括号,给出类型参数`T`的值,本例是``。 44 | 45 | 不过为了方便,函数调用时,往往省略不写类型参数的值,让 TypeScript 自己推断。 46 | 47 | ```typescript 48 | getFirst([1, 2, 3]) 49 | ``` 50 | 51 | 上面示例中,TypeScript 会从实际参数`[1, 2, 3]`,推断出类型参数 T 的值为`number`。 52 | 53 | 有些复杂的使用场景,TypeScript 可能推断不出类型参数的值,这时就必须显式给出了。 54 | 55 | ```typescript 56 | function comb(arr1:T[], arr2:T[]):T[] { 57 | return arr1.concat(arr2); 58 | } 59 | ``` 60 | 61 | 上面示例中,两个参数`arr1`、`arr2`和返回值都是同一个类型。如果不给出类型参数的值,下面的调用会报错。 62 | 63 | ```typescript 64 | comb([1, 2], ['a', 'b']) // 报错 65 | ``` 66 | 67 | 上面示例会报错,TypeScript 认为两个参数不是同一个类型。但是,如果类型参数是一个联合类型,就不会报错。 68 | 69 | ```typescript 70 | comb([1, 2], ['a', 'b']) // 正确 71 | ``` 72 | 73 | 上面示例中,类型参数是一个联合类型,使得两个参数都符合类型参数,就不报错了。这种情况下,类型参数是不能省略不写的。 74 | 75 | 类型参数的名字,可以随便取,但是必须为合法的标识符。习惯上,类型参数的第一个字符往往采用大写字母。一般会使用`T`(type 的第一个字母)作为类型参数的名字。如果有多个类型参数,则使用 T 后面的 U、V 等字母命名,各个参数之间使用逗号(“,”)分隔。 76 | 77 | 下面是多个类型参数的例子。 78 | 79 | ```typescript 80 | function map( 81 | arr:T[], 82 | f:(arg:T) => U 83 | ):U[] { 84 | return arr.map(f); 85 | } 86 | 87 | // 用法实例 88 | map( 89 | ['1', '2', '3'], 90 | (n) => parseInt(n) 91 | ); // 返回 [1, 2, 3] 92 | ``` 93 | 94 | 上面示例将数组的实例方法`map()`改写成全局函数,它有两个类型参数`T`和`U`。含义是,原始数组的类型为`T[]`,对该数组的每个成员执行一个处理函数`f`,将类型`T`转成类型`U`,那么就会得到一个类型为`U[]`的数组。 95 | 96 | 总之,泛型可以理解成一段类型逻辑,需要类型参数来表达。有了类型参数以后,可以在输入类型与输出类型之间,建立一一对应关系。 97 | 98 | ## 泛型的写法 99 | 100 | 泛型主要用在四个场合:函数、接口、类和别名。 101 | 102 | ### 函数的泛型写法 103 | 104 | 上一节提到,`function`关键字定义的泛型函数,类型参数放在尖括号中,写在函数名后面。 105 | 106 | ```typescript 107 | function id(arg:T):T { 108 | return arg; 109 | } 110 | ``` 111 | 112 | 那么对于变量形式定义的函数,泛型有下面两种写法。 113 | 114 | ```typescript 115 | // 写法一 116 | let myId:(arg:T) => T = id; 117 | 118 | // 写法二 119 | let myId:{ (arg:T): T } = id; 120 | ``` 121 | 122 | ### 接口的泛型写法 123 | 124 | interface 也可以采用泛型的写法。 125 | 126 | ```typescript 127 | interface Box { 128 | contents: Type; 129 | } 130 | 131 | let box:Box; 132 | ``` 133 | 134 | 上面示例中,使用泛型接口时,需要给出类型参数的值(本例是`string`)。 135 | 136 | 下面是另一个例子。 137 | 138 | ```typescript 139 | interface Comparator { 140 | compareTo(value:T): number; 141 | } 142 | 143 | class Rectangle implements Comparator { 144 | 145 | compareTo(value:Rectangle): number { 146 | // ... 147 | } 148 | } 149 | ``` 150 | 151 | 上面示例中,先定义了一个泛型接口,然后将这个接口用于一个类。 152 | 153 | 泛型接口还有第二种写法。 154 | 155 | ```typescript 156 | interface Fn { 157 | (arg:Type): Type; 158 | } 159 | 160 | function id(arg:Type): Type { 161 | return arg; 162 | } 163 | 164 | let myId:Fn = id; 165 | ``` 166 | 167 | 上面示例中,`Fn`的类型参数`Type`的具体类型,需要函数`id`在使用时提供。所以,最后一行的赋值语句不需要给出`Type`的具体类型。 168 | 169 | 此外,第二种写法还有一个差异之处。那就是它的类型参数定义在某个方法之中,其他属性和方法不能使用该类型参数。前面的第一种写法,类型参数定义在整个接口,接口内部的所有属性和方法都可以使用该类型参数。 170 | 171 | ### 类的泛型写法 172 | 173 | 泛型类的类型参数写在类名后面。 174 | 175 | ```typescript 176 | class Pair { 177 | key: K; 178 | value: V; 179 | } 180 | ``` 181 | 182 | 下面是继承泛型类的例子。 183 | 184 | ```typescript 185 | class A { 186 | value: T; 187 | } 188 | 189 | class B extends A { 190 | } 191 | ``` 192 | 193 | 上面示例中,类`A`有一个类型参数`T`,使用时必须给出`T`的类型,所以类`B`继承时要写成`A`。 194 | 195 | 泛型也可以用在类表达式。 196 | 197 | ```typescript 198 | const Container = class { 199 | constructor(private readonly data:T) {} 200 | }; 201 | 202 | const a = new Container(true); 203 | const b = new Container(0); 204 | ``` 205 | 206 | 上面示例中,新建实例时,需要同时给出类型参数`T`和类参数`data`的值。 207 | 208 | 下面是另一个例子。 209 | 210 | ```typescript 211 | class C { 212 | value!: NumType; 213 | add!: (x: NumType, y: NumType) => NumType; 214 | } 215 | 216 | let foo = new C(); 217 | 218 | foo.value = 0; 219 | foo.add = function (x, y) { 220 | return x + y; 221 | }; 222 | ``` 223 | 224 | 上面示例中,先新建类`C`的实例`foo`,然后再定义实例的`value`属性和`add()`方法。类的定义中,属性和方法后面的感叹号是非空断言,告诉 TypeScript 它们都是非空的,后面会赋值。 225 | 226 | JavaScript 的类本质上是一个构造函数,因此也可以把泛型类写成构造函数。 227 | 228 | ```typescript 229 | type MyClass = new (...args: any[]) => T; 230 | 231 | // 或者 232 | interface MyClass { 233 | new(...args: any[]): T; 234 | } 235 | 236 | // 用法实例 237 | function createInstance( 238 | AnyClass: MyClass, 239 | ...args: any[] 240 | ):T { 241 | return new AnyClass(...args); 242 | } 243 | ``` 244 | 245 | 上面示例中,函数`createInstance()`的第一个参数`AnyClass`是构造函数(也可以是一个类),它的类型是`MyClass`,这里的`T`是`createInstance()`的类型参数,在该函数调用时再指定具体类型。 246 | 247 | 注意,泛型类描述的是类的实例,不包括静态属性和静态方法,因为这两者定义在类的本身。因此,它们不能引用类型参数。 248 | 249 | ```typescript 250 | class C { 251 | static data: T; // 报错 252 | constructor(public value:T) {} 253 | } 254 | ``` 255 | 256 | 上面示例中,静态属性`data`引用了类型参数`T`,这是不可以的,因为类型参数只能用于实例属性和实例方法,所以报错了。 257 | 258 | ### 类型别名的泛型写法 259 | 260 | type 命令定义的类型别名,也可以使用泛型。 261 | 262 | ```typescript 263 | type Nullable = T | undefined | null; 264 | ``` 265 | 266 | 上面示例中,`Nullable`是一个泛型,只要传入一个类型,就可以得到这个类型与`undefined`和`null`的一个联合类型。 267 | 268 | 下面是另一个例子。 269 | 270 | ```typescript 271 | type Container = { value: T }; 272 | 273 | const a: Container = { value: 0 }; 274 | const b: Container = { value: 'b' }; 275 | ``` 276 | 277 | 下面是定义树形结构的例子。 278 | 279 | ```typescript 280 | type Tree = { 281 | value: T; 282 | left: Tree | null; 283 | right: Tree | null; 284 | }; 285 | ``` 286 | 287 | 上面示例中,类型别名`Tree`内部递归引用了`Tree`自身。 288 | 289 | ## 类型参数的默认值 290 | 291 | 类型参数可以设置默认值。使用时,如果没有给出类型参数的值,就会使用默认值。 292 | 293 | ```typescript 294 | function getFirst( 295 | arr:T[] 296 | ):T { 297 | return arr[0]; 298 | } 299 | ``` 300 | 301 | 上面示例中,`T = string`表示类型参数的默认值是`string`。调用`getFirst()`时,如果不给出`T`的值,TypeScript 就认为`T`等于`string`。 302 | 303 | 但是,因为 TypeScript 会从实际参数推断出`T`的值,从而覆盖掉默认值,所以下面的代码不会报错。 304 | 305 | ```typescript 306 | getFirst([1, 2, 3]) // 正确 307 | ``` 308 | 309 | 上面示例中,实际参数是`[1, 2, 3]`,TypeScript 推断 T 等于`number`,从而覆盖掉默认值`string`。 310 | 311 | 类型参数的默认值,往往用在类中。 312 | 313 | ```typescript 314 | class Generic { 315 | list:T[] = [] 316 | 317 | add(t:T) { 318 | this.list.push(t) 319 | } 320 | } 321 | ``` 322 | 323 | 上面示例中,类`Generic`有一个类型参数`T`,默认值为`string`。这意味着,属性`list`默认是一个字符串数组,方法`add()`的默认参数是一个字符串。 324 | 325 | ```typescript 326 | const g = new Generic(); 327 | 328 | g.add(4) // 报错 329 | g.add('hello') // 正确 330 | ``` 331 | 332 | 上面示例中,新建`Generic`的实例`g`时,没有给出类型参数`T`的值,所以`T`就等于`string`。因此,向`add()`方法传入一个数值会报错,传入字符串就不会。 333 | 334 | ```typescript 335 | const g = new Generic(); 336 | 337 | g.add(4) // 正确 338 | g.add('hello') // 报错 339 | ``` 340 | 341 | 上面示例中,新建实例`g`时,给出了类型参数`T`的值是`number`,因此`add()`方法传入数值不会报错,传入字符串会报错。 342 | 343 | 一旦类型参数有默认值,就表示它是可选参数。如果有多个类型参数,可选参数必须在必选参数之后。 344 | 345 | ```typescript 346 | // 错误 347 | 348 | // 正确 349 | ``` 350 | 351 | 上面示例中,依次有两个类型参数`T`和`U`。如果`T`是可选参数,`U`不是,就会报错。 352 | 353 | ## 数组的泛型表示 354 | 355 | 《数组》一章提到过,数组类型有一种表示方法是`Array`。这就是泛型的写法,`Array`是 TypeScript 原生的一个类型接口,`T`是它的类型参数。声明数组时,需要提供`T`的值。 356 | 357 | ```typescript 358 | let arr:Array = [1, 2, 3]; 359 | ``` 360 | 361 | 上面的示例中,`Array`就是一个泛型,类型参数的值是`number`,表示该数组的全部成员都是数值。 362 | 363 | 同样的,如果数组成员都是字符串,那么类型就写成`Array`。事实上,在 TypeScript 内部,数组类型的另一种写法`number[]`、`string[]`,只是`Array`、`Array`的简写形式。 364 | 365 | 在 TypeScript 内部,`Array`是一个泛型接口,类型定义基本是下面的样子。 366 | 367 | ```typescript 368 | interface Array { 369 | 370 | length: number; 371 | 372 | pop(): Type|undefined; 373 | 374 | push(...items:Type[]): number; 375 | 376 | // ... 377 | } 378 | ``` 379 | 380 | 上面代码中,`push()`方法的参数`item`的类型是`Type[]`,跟`Array()`的参数类型`Type`保持一致,表示只能添加同类型的成员。调用`push()`的时候,TypeScript 就会检查两者是否一致。 381 | 382 | 其他的 TypeScript 内部数据结构,比如`Map`、`Set`和`Promise`,其实也是泛型接口,完整的写法是`Map`、`Set`和`Promise`。 383 | 384 | TypeScript 默认还提供一个`ReadonlyArray`接口,表示只读数组。 385 | 386 | ```typescript 387 | function doStuff( 388 | values:ReadonlyArray 389 | ) { 390 | values.push('hello!'); // 报错 391 | } 392 | ``` 393 | 394 | 上面示例中,参数`values`的类型是`ReadonlyArray`,表示不能修改这个数组,所以函数体内部新增数组成员就会报错。因此,如果不希望函数内部改动参数数组,就可以将该参数数组声明为`ReadonlyArray`类型。 395 | 396 | ## 类型参数的约束条件 397 | 398 | 很多类型参数并不是无限制的,对于传入的类型存在约束条件。 399 | 400 | ```typescript 401 | function comp(a:Type, b:Type) { 402 | if (a.length >= b.length) { 403 | return a; 404 | } 405 | return b; 406 | } 407 | ``` 408 | 409 | 上面示例中,类型参数 Type 有一个隐藏的约束条件:它必须存在`length`属性。如果不满足这个条件,就会报错。 410 | 411 | TypeScript 提供了一种语法,允许在类型参数上面写明约束条件,如果不满足条件,编译时就会报错。这样也可以有良好的语义,对类型参数进行说明。 412 | 413 | ```typescript 414 | function comp( 415 | a: T, 416 | b: T 417 | ) { 418 | if (a.length >= b.length) { 419 | return a; 420 | } 421 | return b; 422 | } 423 | ``` 424 | 425 | 上面示例中,`T extends { length: number }`就是约束条件,表示类型参数 T 必须满足`{ length: number }`,否则就会报错。 426 | 427 | ```typescript 428 | comp([1, 2], [1, 2, 3]) // 正确 429 | comp('ab', 'abc') // 正确 430 | comp(1, 2) // 报错 431 | ``` 432 | 433 | 上面示例中,只要传入的参数类型不满足约束条件,就会报错。 434 | 435 | 类型参数的约束条件采用下面的形式。 436 | 437 | ```typescript 438 | 439 | ``` 440 | 441 | 上面语法中,`TypeParameter`表示类型参数,`extends`是关键字,这是必须的,`ConstraintType`表示类型参数要满足的条件,即类型参数应该是`ConstraintType`的子类型。 442 | 443 | 类型参数可以同时设置约束条件和默认值,前提是默认值必须满足约束条件。 444 | 445 | ```typescript 446 | type Fn 447 | = [A, B]; 448 | 449 | type Result = Fn<'hello'> // ["hello", "world"] 450 | ``` 451 | 452 | 上面示例中,类型参数`A`和`B`都有约束条件,并且`B`还有默认值。所以,调用`Fn`的时候,可以只给出`A`的值,不给出`B`的值。 453 | 454 | 另外,上例也可以看出,泛型本质上是一个类型函数,通过输入参数,获得结果,两者是一一对应关系。 455 | 456 | 如果有多个类型参数,一个类型参数的约束条件,可以引用其他参数。 457 | 458 | ```typescript 459 | 460 | // 或者 461 | 462 | ``` 463 | 464 | 上面示例中,`U`的约束条件引用`T`,或者`T`的约束条件引用`U`,都是正确的。 465 | 466 | 但是,约束条件不能引用类型参数自身。 467 | 468 | ```typescript 469 | // 报错 470 | // 报错 471 | ``` 472 | 473 | 上面示例中,`T`的约束条件不能是`T`自身。同理,多个类型参数也不能互相约束(即`T`的约束条件是`U`、`U`的约束条件是`T`),因为互相约束就意味着约束条件就是类型参数自身。 474 | 475 | ## 使用注意点 476 | 477 | 泛型有一些使用注意点。 478 | 479 | **(1)尽量少用泛型。** 480 | 481 | 泛型虽然灵活,但是会加大代码的复杂性,使其变得难读难写。一般来说,只要使用了泛型,类型声明通常都不太易读,容易写得很复杂。因此,可以不用泛型就不要用。 482 | 483 | **(2)类型参数越少越好。** 484 | 485 | 多一个类型参数,多一道替换步骤,加大复杂性。因此,类型参数越少越好。 486 | 487 | ```typescript 488 | function filter< 489 | T, 490 | Fn extends (arg:T) => boolean 491 | >( 492 | arr:T[], 493 | func:Fn 494 | ): T[] { 495 | return arr.filter(func); 496 | } 497 | ``` 498 | 499 | 上面示例有两个类型参数,但是第二个类型参数`Fn`是不必要的,完全可以直接写在函数参数的类型声明里面。 500 | 501 | ```typescript 502 | function filter( 503 | arr:T[], 504 | func:(arg:T) => boolean 505 | ): T[] { 506 | return arr.filter(func); 507 | } 508 | ``` 509 | 510 | 上面示例中,类型参数简化成了一个,效果与前一个示例是一样的。 511 | 512 | **(3)类型参数需要出现两次。** 513 | 514 | 如果类型参数在定义后只出现一次,那么很可能是不必要的。 515 | 516 | ```typescript 517 | function greet( 518 | s:Str 519 | ) { 520 | console.log('Hello, ' + s); 521 | } 522 | ``` 523 | 524 | 上面示例中,类型参数`Str`只在函数声明中出现一次(除了它的定义部分),这往往表明这个类型参数是不必要。 525 | 526 | ```typescript 527 | function greet(s:string) { 528 | console.log('Hello, ' + s); 529 | } 530 | ``` 531 | 532 | 上面示例把前面的类型参数省略了,效果与前一个示例是一样的。 533 | 534 | 也就是说,只有当类型参数用到两次或两次以上,才是泛型的适用场合。 535 | 536 | **(4)泛型可以嵌套。** 537 | 538 | 类型参数可以是另一个泛型。 539 | 540 | ```typescript 541 | type OrNull = Type|null; 542 | 543 | type OneOrMany = Type|Type[]; 544 | 545 | type OneOrManyOrNull = OrNull>; 546 | ``` 547 | 548 | 上面示例中,最后一行的泛型`OrNull`的类型参数,就是另一个泛型`OneOrMany`。 549 | 550 | -------------------------------------------------------------------------------- /docs/interface.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的 interface 接口 2 | 3 | ## 简介 4 | 5 | interface 是对象的模板,可以看作是一种类型约定,中文译为“接口”。使用了某个模板的对象,就拥有了指定的类型结构。 6 | 7 | ```typescript 8 | interface Person { 9 | firstName: string; 10 | lastName: string; 11 | age: number; 12 | } 13 | ``` 14 | 15 | 上面示例中,定义了一个接口`Person`,它指定一个对象模板,拥有三个属性`firstName`、`lastName`和`age`。任何实现这个接口的对象,都必须部署这三个属性,并且必须符合规定的类型。 16 | 17 | 实现该接口很简单,只要指定它作为对象的类型即可。 18 | 19 | ```typescript 20 | const p:Person = { 21 | firstName: 'John', 22 | lastName: 'Smith', 23 | age: 25 24 | }; 25 | ``` 26 | 27 | 上面示例中,变量`p`的类型就是接口`Person`,所以必须符合`Person`指定的结构。 28 | 29 | 方括号运算符可以取出 interface 某个属性的类型。 30 | 31 | ```typescript 32 | interface Foo { 33 | a: string; 34 | } 35 | 36 | type A = Foo['a']; // string 37 | ``` 38 | 39 | 上面示例中,`Foo['a']`返回属性`a`的类型,所以类型`A`就是`string`。 40 | 41 | interface 可以表示对象的各种语法,它的成员有5种形式。 42 | 43 | - 对象属性 44 | - 对象的属性索引 45 | - 对象方法 46 | - 函数 47 | - 构造函数 48 | 49 | (1)对象属性 50 | 51 | ```typescript 52 | interface Point { 53 | x: number; 54 | y: number; 55 | } 56 | ``` 57 | 58 | 上面示例中,`x`和`y`都是对象的属性,分别使用冒号指定每个属性的类型。 59 | 60 | 属性之间使用分号或逗号分隔,最后一个属性结尾的分号或逗号可以省略。 61 | 62 | 如果属性是可选的,就在属性名后面加一个问号。 63 | 64 | ```typescript 65 | interface Foo { 66 | x?: string; 67 | } 68 | ``` 69 | 70 | 如果属性是只读的,需要加上`readonly`修饰符。 71 | 72 | ```typescript 73 | interface A { 74 | readonly a: string; 75 | } 76 | ``` 77 | 78 | (2)对象的属性索引 79 | 80 | ```typescript 81 | interface A { 82 | [prop: string]: number; 83 | } 84 | ``` 85 | 86 | 上面示例中,`[prop: string]`就是属性的字符串索引,表示属性名只要是字符串,都符合类型要求。 87 | 88 | 属性索引共有`string`、`number`和`symbol`三种类型。 89 | 90 | 一个接口中,最多只能定义一个字符串索引。字符串索引会约束该类型中所有名字为字符串的属性。 91 | 92 | ```typescript 93 | interface MyObj { 94 | [prop: string]: number; 95 | 96 | a: boolean; // 编译错误 97 | } 98 | ``` 99 | 100 | 上面示例中,属性索引指定所有名称为字符串的属性,它们的属性值必须是数值(`number`)。属性`a`的值为布尔值就报错了。 101 | 102 | 属性的数值索引,其实是指定数组的类型。 103 | 104 | ```typescript 105 | interface A { 106 | [prop: number]: string; 107 | } 108 | 109 | const obj:A = ['a', 'b', 'c']; 110 | ``` 111 | 112 | 上面示例中,`[prop: number]`表示属性名的类型是数值,所以可以用数组对变量`obj`赋值。 113 | 114 | 同样的,一个接口中最多只能定义一个数值索引。数值索引会约束所有名称为数值的属性。 115 | 116 | 如果一个 interface 同时定义了字符串索引和数值索引,那么数值索引必须服从于字符串索引。因为在 JavaScript 中,数值属性名最终是自动转换成字符串属性名。 117 | 118 | ```typescript 119 | interface A { 120 | [prop: string]: number; 121 | [prop: number]: string; // 报错 122 | } 123 | 124 | interface B { 125 | [prop: string]: number; 126 | [prop: number]: number; // 正确 127 | } 128 | ``` 129 | 130 | 上面示例中,数值索引的属性值类型与字符串索引不一致,就会报错。数值索引必须兼容字符串索引的类型声明。 131 | 132 | (3)对象的方法 133 | 134 | 对象的方法共有三种写法。 135 | 136 | ```typescript 137 | // 写法一 138 | interface A { 139 | f(x: boolean): string; 140 | } 141 | 142 | // 写法二 143 | interface B { 144 | f: (x: boolean) => string; 145 | } 146 | 147 | // 写法三 148 | interface C { 149 | f: { (x: boolean): string }; 150 | } 151 | ``` 152 | 153 | 属性名可以采用表达式,所以下面的写法也是可以的。 154 | 155 | ```typescript 156 | const f = 'f'; 157 | 158 | interface A { 159 | [f](x: boolean): string; 160 | } 161 | ``` 162 | 163 | 类型方法可以重载。 164 | 165 | ```typescript 166 | interface A { 167 | f(): number; 168 | f(x: boolean): boolean; 169 | f(x: string, y: string): string; 170 | } 171 | ``` 172 | 173 | interface 里面的函数重载,不需要给出实现。但是,由于对象内部定义方法时,无法使用函数重载的语法,所以需要额外在对象外部给出函数方法的实现。 174 | 175 | ```typescript 176 | interface A { 177 | f(): number; 178 | f(x: boolean): boolean; 179 | f(x: string, y: string): string; 180 | } 181 | 182 | function MyFunc(): number; 183 | function MyFunc(x: boolean): boolean; 184 | function MyFunc(x: string, y: string): string; 185 | function MyFunc( 186 | x?:boolean|string, y?:string 187 | ):number|boolean|string { 188 | if (x === undefined && y === undefined) return 1; 189 | if (typeof x === 'boolean' && y === undefined) return true; 190 | if (typeof x === 'string' && typeof y === 'string') return 'hello'; 191 | throw new Error('wrong parameters'); 192 | } 193 | 194 | const a:A = { 195 | f: MyFunc 196 | } 197 | ``` 198 | 199 | 上面示例中,接口`A`的方法`f()`有函数重载,需要额外定义一个函数`MyFunc()`实现这个重载,然后部署接口`A`的对象`a`的属性`f`等于函数`MyFunc()`就可以了。 200 | 201 | (4)函数 202 | 203 | interface 也可以用来声明独立的函数。 204 | 205 | ```typescript 206 | interface Add { 207 | (x:number, y:number): number; 208 | } 209 | 210 | const myAdd:Add = (x,y) => x + y; 211 | ``` 212 | 213 | 上面示例中,接口`Add`声明了一个函数类型。 214 | 215 | (5)构造函数 216 | 217 | interface 内部可以使用`new`关键字,表示构造函数。 218 | 219 | ```typescript 220 | interface ErrorConstructor { 221 | new (message?: string): Error; 222 | } 223 | ``` 224 | 225 | 上面示例中,接口`ErrorConstructor`内部有`new`命令,表示它是一个构造函数。 226 | 227 | TypeScript 里面,构造函数特指具有`constructor`属性的类,详见《Class》一章。 228 | 229 | ## interface 的继承 230 | 231 | interface 可以继承其他类型,主要有下面几种情况。 232 | 233 | ### interface 继承 interface 234 | 235 | interface 可以使用`extends`关键字,继承其他 interface。 236 | 237 | ```typescript 238 | interface Shape { 239 | name: string; 240 | } 241 | 242 | interface Circle extends Shape { 243 | radius: number; 244 | } 245 | ``` 246 | 247 | 上面示例中,`Circle`继承了`Shape`,所以`Circle`其实有两个属性`name`和`radius`。这时,`Circle`是子接口,`Shape`是父接口。 248 | 249 | `extends`关键字会从继承的接口里面拷贝属性类型,这样就不必书写重复的属性。 250 | 251 | interface 允许多重继承。 252 | 253 | ```typescript 254 | interface Style { 255 | color: string; 256 | } 257 | 258 | interface Shape { 259 | name: string; 260 | } 261 | 262 | interface Circle extends Style, Shape { 263 | radius: number; 264 | } 265 | ``` 266 | 267 | 上面示例中,`Circle`同时继承了`Style`和`Shape`,所以拥有三个属性`color`、`name`和`radius`。 268 | 269 | 多重接口继承,实际上相当于多个父接口的合并。 270 | 271 | 如果子接口与父接口存在同名属性,那么子接口的属性会覆盖父接口的属性。注意,子接口与父接口的同名属性必须是类型兼容的,不能有冲突,否则会报错。 272 | 273 | ```typescript 274 | interface Foo { 275 | id: string; 276 | } 277 | 278 | interface Bar extends Foo { 279 | id: number; // 报错 280 | } 281 | ``` 282 | 283 | 上面示例中,`Bar`继承了`Foo`,但是两者的同名属性`id`的类型不兼容,导致报错。 284 | 285 | 多重继承时,如果多个父接口存在同名属性,那么这些同名属性不能有类型冲突,否则会报错。 286 | 287 | ```typescript 288 | interface Foo { 289 | id: string; 290 | } 291 | 292 | interface Bar { 293 | id: number; 294 | } 295 | 296 | // 报错 297 | interface Baz extends Foo, Bar { 298 | type: string; 299 | } 300 | ``` 301 | 302 | 上面示例中,`Baz`同时继承了`Foo`和`Bar`,但是后两者的同名属性`id`有类型冲突,导致报错。 303 | 304 | ### interface 继承 type 305 | 306 | interface 可以继承`type`命令定义的对象类型。 307 | 308 | ```typescript 309 | type Country = { 310 | name: string; 311 | capital: string; 312 | } 313 | 314 | interface CountryWithPop extends Country { 315 | population: number; 316 | } 317 | ``` 318 | 319 | 上面示例中,`CountryWithPop`继承了`type`命令定义的`Country`对象,并且新增了一个`population`属性。 320 | 321 | 注意,如果`type`命令定义的类型不是对象,interface 就无法继承。 322 | 323 | ### interface 继承 class 324 | 325 | interface 还可以继承 class,即继承该类的所有成员。关于 class 的详细解释,参见下一章。 326 | 327 | ```typescript 328 | class A { 329 | x:string = ''; 330 | 331 | y():boolean { 332 | return true; 333 | } 334 | } 335 | 336 | interface B extends A { 337 | z: number 338 | } 339 | ``` 340 | 341 | 上面示例中,`B`继承了`A`,因此`B`就具有属性`x`、`y()`和`z`。 342 | 343 | 实现`B`接口的对象就需要实现这些属性。 344 | 345 | ```typescript 346 | const b:B = { 347 | x: '', 348 | y: function(){ return true }, 349 | z: 123 350 | } 351 | ``` 352 | 353 | 上面示例中,对象`b`就实现了接口`B`,而接口`B`又继承了类`A`。 354 | 355 | 某些类拥有私有成员和保护成员,interface 可以继承这样的类,但是意义不大。 356 | 357 | ```typescript 358 | class A { 359 | private x: string = ''; 360 | protected y: string = ''; 361 | } 362 | 363 | interface B extends A { 364 | z: number 365 | } 366 | 367 | // 报错 368 | const b:B = { /* ... */ } 369 | 370 | // 报错 371 | class C implements B { 372 | // ... 373 | } 374 | ``` 375 | 376 | 上面示例中,`A`有私有成员和保护成员,`B`继承了`A`,但无法用于对象,因为对象不能实现这些成员。这导致`B`只能用于其他 class,而这时其他 class 与`A`之间不构成父类和子类的关系,使得`x`与`y`无法部署。 377 | 378 | ## 接口合并 379 | 380 | 多个同名接口会合并成一个接口。 381 | 382 | ```typescript 383 | interface Box { 384 | height: number; 385 | width: number; 386 | } 387 | 388 | interface Box { 389 | length: number; 390 | } 391 | ``` 392 | 393 | 上面示例中,两个`Box`接口会合并成一个接口,同时有`height`、`width`和`length`三个属性。 394 | 395 | 这样的设计主要是为了兼容 JavaScript 的行为。JavaScript 开发者常常对全局对象或者外部库,添加自己的属性和方法。那么,只要使用 interface 给出这些自定义属性和方法的类型,就能自动跟原始的 interface 合并,使得扩展外部类型非常方便。 396 | 397 | 举例来说,Web 网页开发经常会对`window`对象和`document`对象添加自定义属性,但是 TypeScript 会报错,因为原始定义没有这些属性。解决方法就是把自定义属性写成 interface,合并进原始定义。 398 | 399 | ```typescript 400 | interface Document { 401 | foo: string; 402 | } 403 | 404 | document.foo = 'hello'; 405 | ``` 406 | 407 | 上面示例中,接口`Document`增加了一个自定义属性`foo`,从而就可以在`document`对象上使用自定义属性。 408 | 409 | 同名接口合并时,同一个属性如果有多个类型声明,彼此不能有类型冲突。 410 | 411 | ```typescript 412 | interface A { 413 | a: number; 414 | } 415 | 416 | interface A { 417 | a: string; // 报错 418 | } 419 | ``` 420 | 421 | 上面示例中,接口`A`的属性`a`有两个类型声明,彼此是冲突的,导致报错。 422 | 423 | 同名接口合并时,如果同名方法有不同的类型声明,那么会发生函数重载。而且,后面的定义比前面的定义具有更高的优先级。 424 | 425 | ```typescript 426 | interface Cloner { 427 | clone(animal: Animal): Animal; 428 | } 429 | 430 | interface Cloner { 431 | clone(animal: Sheep): Sheep; 432 | } 433 | 434 | interface Cloner { 435 | clone(animal: Dog): Dog; 436 | clone(animal: Cat): Cat; 437 | } 438 | 439 | // 等同于 440 | interface Cloner { 441 | clone(animal: Dog): Dog; 442 | clone(animal: Cat): Cat; 443 | clone(animal: Sheep): Sheep; 444 | clone(animal: Animal): Animal; 445 | } 446 | ``` 447 | 448 | 上面示例中,`clone()`方法有不同的类型声明,会发生函数重载。这时,越靠后的定义,优先级越高,排在函数重载的越前面。比如,`clone(animal: Animal)`是最先出现的类型声明,就排在函数重载的最后,属于`clone()`函数最后匹配的类型。 449 | 450 | 这个规则有一个例外。同名方法之中,如果有一个参数是字面量类型,字面量类型有更高的优先级。 451 | 452 | ```typescript 453 | interface A { 454 | f(x:'foo'): boolean; 455 | } 456 | 457 | interface A { 458 | f(x:any): void; 459 | } 460 | 461 | // 等同于 462 | interface A { 463 | f(x:'foo'): boolean; 464 | f(x:any): void; 465 | } 466 | ``` 467 | 468 | 上面示例中,`f()`方法有一个类型声明的参数`x`是字面量类型,这个类型声明的优先级最高,会排在函数重载的最前面。 469 | 470 | 一个实际的例子是 Document 对象的`createElement()`方法,它会根据参数的不同,而生成不同的 HTML 节点对象。 471 | 472 | ```typescript 473 | interface Document { 474 | createElement(tagName: any): Element; 475 | } 476 | interface Document { 477 | createElement(tagName: "div"): HTMLDivElement; 478 | createElement(tagName: "span"): HTMLSpanElement; 479 | } 480 | interface Document { 481 | createElement(tagName: string): HTMLElement; 482 | createElement(tagName: "canvas"): HTMLCanvasElement; 483 | } 484 | 485 | // 等同于 486 | interface Document { 487 | createElement(tagName: "canvas"): HTMLCanvasElement; 488 | createElement(tagName: "div"): HTMLDivElement; 489 | createElement(tagName: "span"): HTMLSpanElement; 490 | createElement(tagName: string): HTMLElement; 491 | createElement(tagName: any): Element; 492 | } 493 | ``` 494 | 495 | 上面示例中,`createElement()`方法的函数重载,参数为字面量的类型声明会排到最前面,返回具体的 HTML 节点对象。类型越不具体的参数,排在越后面,返回通用的 HTML 节点对象。 496 | 497 | 如果两个 interface 组成的联合类型存在同名属性,那么该属性的类型也是联合类型。 498 | 499 | ```typescript 500 | interface Circle { 501 | area: bigint; 502 | } 503 | 504 | interface Rectangle { 505 | area: number; 506 | } 507 | 508 | declare const s: Circle | Rectangle; 509 | 510 | s.area; // bigint | number 511 | ``` 512 | 513 | 上面示例中,接口`Circle`和`Rectangle`组成一个联合类型`Circle | Rectangle`。因此,这个联合类型的同名属性`area`,也是一个联合类型。本例中的`declare`命令表示变量`s`的具体定义,由其他脚本文件给出,详见《declare 命令》一章。 514 | 515 | ## interface 与 type 的异同 516 | 517 | `interface`命令与`type`命令作用类似,都可以表示对象类型。 518 | 519 | 很多对象类型既可以用 interface 表示,也可以用 type 表示。而且,两者往往可以换用,几乎所有的 interface 命令都可以改写为 type 命令。 520 | 521 | 它们的相似之处,首先表现在都能为对象类型起名。 522 | 523 | ```typescript 524 | type Country = { 525 | name: string; 526 | capital: string; 527 | } 528 | 529 | interface Country { 530 | name: string; 531 | capital: string; 532 | } 533 | ``` 534 | 535 | 上面示例是`type`命令和`interface`命令,分别定义同一个类型。 536 | 537 | `class`命令也有类似作用,通过定义一个类,同时定义一个对象类型。但是,它会创造一个值,编译后依然存在。如果只是单纯想要一个类型,应该使用`type`或`interface`。 538 | 539 | interface 与 type 的区别有下面几点。 540 | 541 | (1)`type`能够表示非对象类型,而`interface`只能表示对象类型(包括数组、函数等)。 542 | 543 | (2)`interface`可以继承其他类型,`type`不支持继承。 544 | 545 | 继承的主要作用是添加属性,`type`定义的对象类型如果想要添加属性,只能使用`&`运算符,重新定义一个类型。 546 | 547 | ```typescript 548 | type Animal = { 549 | name: string 550 | } 551 | 552 | type Bear = Animal & { 553 | honey: boolean 554 | } 555 | ``` 556 | 557 | 上面示例中,类型`Bear`在`Animal`的基础上添加了一个属性`honey`。 558 | 559 | 上例的`&`运算符,表示同时具备两个类型的特征,因此可以起到两个对象类型合并的作用。 560 | 561 | 作为比较,`interface`添加属性,采用的是继承的写法。 562 | 563 | ```typescript 564 | interface Animal { 565 | name: string 566 | } 567 | 568 | interface Bear extends Animal { 569 | honey: boolean 570 | } 571 | ``` 572 | 573 | 继承时,type 和 interface 是可以换用的。interface 可以继承 type。 574 | 575 | ```typescript 576 | type Foo = { x: number; }; 577 | 578 | interface Bar extends Foo { 579 | y: number; 580 | } 581 | ``` 582 | 583 | type 也可以继承 interface。 584 | 585 | ```typescript 586 | interface Foo { 587 | x: number; 588 | } 589 | 590 | type Bar = Foo & { y: number; }; 591 | ``` 592 | 593 | (3)同名`interface`会自动合并,同名`type`则会报错。也就是说,TypeScript 不允许使用`type`多次定义同一个类型。 594 | 595 | ```typescript 596 | type A = { foo:number }; // 报错 597 | type A = { bar:number }; // 报错 598 | ``` 599 | 600 | 上面示例中,`type`两次定义了类型`A`,导致两行都会报错。 601 | 602 | 作为比较,`interface`则会自动合并。 603 | 604 | ```typescript 605 | interface A { foo:number }; 606 | interface A { bar:number }; 607 | 608 | const obj:A = { 609 | foo: 1, 610 | bar: 1 611 | }; 612 | ``` 613 | 614 | 上面示例中,`interface`把类型`A`的两个定义合并在一起。 615 | 616 | 这表明,interface 是开放的,可以添加属性,type 是封闭的,不能添加属性,只能定义新的 type。 617 | 618 | (4)`interface`不能包含属性映射(mapping),`type`可以,详见《映射》一章。 619 | 620 | ```typescript 621 | interface Point { 622 | x: number; 623 | y: number; 624 | } 625 | 626 | // 正确 627 | type PointCopy1 = { 628 | [Key in keyof Point]: Point[Key]; 629 | }; 630 | 631 | // 报错 632 | interface PointCopy2 { 633 | [Key in keyof Point]: Point[Key]; 634 | }; 635 | ``` 636 | 637 | (5)`this`关键字只能用于`interface`。 638 | 639 | ```typescript 640 | // 正确 641 | interface Foo { 642 | add(num:number): this; 643 | }; 644 | 645 | // 报错 646 | type Foo = { 647 | add(num:number): this; 648 | }; 649 | ``` 650 | 651 | 上面示例中,type 命令声明的方法`add()`,返回`this`就报错了。interface 命令没有这个问题。 652 | 653 | 下面是返回`this`的实际对象的例子。 654 | 655 | ```typescript 656 | class Calculator implements Foo { 657 | result = 0; 658 | add(num:number) { 659 | this.result += num; 660 | return this; 661 | } 662 | } 663 | ``` 664 | 665 | (6)type 可以扩展原始数据类型,interface 不行。 666 | 667 | ```typescript 668 | // 正确 669 | type MyStr = string & { 670 | type: 'new' 671 | }; 672 | 673 | // 报错 674 | interface MyStr extends string { 675 | type: 'new' 676 | } 677 | ``` 678 | 679 | 上面示例中,type 可以扩展原始数据类型 string,interface 就不行。 680 | 681 | (7)`interface`无法表达某些复杂类型(比如交叉类型和联合类型),但是`type`可以。 682 | 683 | ```typescript 684 | type A = { /* ... */ }; 685 | type B = { /* ... */ }; 686 | 687 | type AorB = A | B; 688 | type AorBwithName = AorB & { 689 | name: string 690 | }; 691 | ``` 692 | 693 | 上面示例中,类型`AorB`是一个联合类型,`AorBwithName`则是为`AorB`添加一个属性。这两种运算,`interface`都没法表达。 694 | 695 | 综上所述,如果有复杂的类型运算,那么没有其他选择只能使用`type`;一般情况下,`interface`灵活性比较高,便于扩充类型或自动合并,建议优先使用。 696 | 697 | -------------------------------------------------------------------------------- /docs/intro.md: -------------------------------------------------------------------------------- 1 | # TypeScript 语言简介 2 | 3 | ## 概述 4 | 5 | TypeScript(简称 TS)是微软公司开发的一种基于 JavaScript (简称 JS)语言的编程语言。 6 | 7 | 它的目的并不是创造一种全新语言,而是增强 JavaScript 的功能,使其更适合多人合作的企业级项目。 8 | 9 | TypeScript 可以看成是 JavaScript 的超集(superset),即它继承了后者的全部语法,所有 JavaScript 脚本都可以当作 TypeScript 脚本(但是可能会报错),此外它再增加了一些自己的语法。 10 | 11 | TypeScript 对 JavaScript 添加的最主要部分,就是一个独立的类型系统。 12 | 13 | ## 类型的概念 14 | 15 | 类型(type)指的是一组具有相同特征的值。如果两个值具有某种共同的特征,就可以说,它们属于同一种类型。 16 | 17 | 举例来说,`123`和`456`这两个值,共同特征是都能进行数值运算,所以都属于“数值”(number)这个类型。 18 | 19 | 一旦确定某个值的类型,就意味着,这个值具有该类型的所有特征,可以进行该类型的所有运算。凡是适用该类型的地方,都可以使用这个值;凡是不适用该类型的地方,使用这个值都会报错。 20 | 21 | 可以这样理解,**类型是人为添加的一种编程约束和用法提示。** 主要目的是在软件开发过程中,为编译器和开发工具提供更多的验证和帮助,帮助提高代码质量,减少错误。 22 | 23 | 下面是一段简单的 TypeScript 代码,演示一下类型系统的作用。 24 | 25 | ```typescript 26 | function addOne(n:number) { 27 | return n + 1; 28 | } 29 | ``` 30 | 31 | 上面示例中,函数`addOne()`有一个参数`n`,类型为数值(number),表示这个位置只能使用数值,传入其他类型的值就会报错。 32 | 33 | ```typescript 34 | addOne('hello') // 报错 35 | ``` 36 | 37 | 上面示例中,函数`addOne()`传入了一个字符串`hello`,TypeScript 发现类型不对,就报错了,指出这个位置只能传入数值,不能传入字符串。 38 | 39 | JavaScript 语言就没有这个功能,不会检查类型对不对。开发阶段很可能发现不了这个问题,代码也许就会原样发布,导致用户在使用时遇到错误。 40 | 41 | 作为比较,TypeScript 是在开发阶段报错,这样有利于提早发现错误,避免使用时报错。另一方面,函数定义里面加入类型,具有提示作用,可以告诉开发者这个函数怎么用。 42 | 43 | ## 动态类型与静态类型 44 | 45 | 前面说了,TypeScript 的主要功能是为 JavaScript 添加类型系统。大家可能知道,JavaScript 语言本身就有一套自己的类型系统,比如数值`123`和字符串`Hello`。 46 | 47 | 但是,JavaScript 的类型系统非常弱,而且没有使用限制,运算符可以接受各种类型的值。在语法上,JavaScript 属于动态类型语言。 48 | 49 | 请看下面的 JavaScript 代码。 50 | 51 | ```javascript 52 | // 例一 53 | let x = 1; 54 | x = 'hello'; 55 | 56 | // 例二 57 | let y = { foo: 1 }; 58 | delete y.foo; 59 | y.bar = 2; 60 | ``` 61 | 62 | 上面的例一,变量`x`声明时,值的类型是数值,但是后面可以改成字符串。所以,无法提前知道变量的类型是什么,也就是说,变量的类型是动态的。 63 | 64 | 上面的例二,变量`y`是一个对象,有一个属性`foo`,但是这个属性是可以删掉的,并且还可以新增其他属性。所以,对象有什么属性,这个属性还在不在,也是动态的,没法提前知道。 65 | 66 | 正是因为存在这些动态变化,所以 JavaScript 的类型系统是动态的,不具有很强的约束性。这对于提前发现代码错误,非常不利。 67 | 68 | TypeScript 引入了一个更强大、更严格的类型系统,属于静态类型语言。 69 | 70 | 上面的代码在 TypeScript 里面都会报错。 71 | 72 | ```javascript 73 | // 例一 74 | let x = 1; 75 | x = 'hello'; // 报错 76 | 77 | // 例二 78 | let y = { foo: 1 }; 79 | delete y.foo; // 报错 80 | y.bar = 2; // 报错 81 | ``` 82 | 83 | 上面示例中,例一的报错是因为变量赋值时,TypeScript 已经推断确定了类型,后面就不允许再赋值为其他类型的值,即变量的类型是静态的。例二的报错是因为对象的属性也是静态的,不允许随意增删。 84 | 85 | TypeScript 的作用,就是为 JavaScript 引入这种静态类型特征。 86 | 87 | ## 静态类型的优点 88 | 89 | 静态类型有很多好处,这也是 TypeScript 想要达到的目的。 90 | 91 | (1)有利于代码的静态分析。 92 | 93 | 有了静态类型,不必运行代码,就可以确定变量的类型,从而推断代码有没有错误。这就叫做代码的静态分析。 94 | 95 | 这对于大型项目非常重要,单单在开发阶段运行静态检查,就可以发现很多问题,避免交付有问题的代码,大大降低了线上风险。 96 | 97 | (2)有利于发现错误。 98 | 99 | 由于每个值、每个变量、每个运算符都有严格的类型约束,TypeScript 就能轻松发现拼写错误、语义错误和方法调用错误,节省程序员的时间。 100 | 101 | ```typescript 102 | let obj = { message: '' }; 103 | console.log(obj.messege); // 报错 104 | ``` 105 | 106 | 上面示例中,不小心把`message`拼错了,写成`messege`。TypeScript 就会报错,指出没有定义过这个属性。JavaScript 遇到这种情况是不报错的。 107 | 108 | ```typescript 109 | const a = 0; 110 | const b = true; 111 | const result = a + b; // 报错 112 | ``` 113 | 114 | 上面示例是合法的 JavaScript 代码,但是没有意义,不应该将数值`a`与布尔值`b`相加。TypeScript 就会直接报错,提示运算符`+`不能用于数值和布尔值的相加。 115 | 116 | ```typescript 117 | function hello() { 118 | return 'hello world'; 119 | } 120 | 121 | hello().find('hello'); // 报错 122 | ``` 123 | 124 | 上面示例中,`hello()`返回的是一个字符串,TypeScript 发现字符串没有`find()`方法,所以报错了。如果是 JavaScript,只有到运行阶段才会报错。 125 | 126 | (3)更好的 IDE 支持,做到语法提示和自动补全。 127 | 128 | IDE(集成开发环境,比如 VSCode)一般都会利用类型信息,提供语法提示功能(编辑器自动提示函数用法、参数等)和自动补全功能(只键入一部分的变量名或函数名,编辑器补全后面的部分)。 129 | 130 | (4)提供了代码文档。 131 | 132 | 类型信息可以部分替代代码文档,解释应该如何使用这些代码,熟练的开发者往往只看类型,就能大致推断代码的作用。借助类型信息,很多工具能够直接生成文档。 133 | 134 | (5)有助于代码重构。 135 | 136 | 修改他人的 JavaScript 代码,往往非常痛苦,项目越大越痛苦,因为不确定修改后是否会影响到其他部分的代码。 137 | 138 | 类型信息大大减轻了重构的成本。一般来说,只要函数或对象的参数和返回值保持类型不变,就能基本确定,重构后的代码也能正常运行。如果还有配套的单元测试,就完全可以放心重构。越是大型的、多人合作的项目,类型信息能够提供的帮助越大。 139 | 140 | 综上所述,TypeScript 有助于提高代码质量,保证代码安全,更适合用在大型的企业级项目。这就是为什么大量 JavaScript 项目转成 TypeScript 的原因。 141 | 142 | ## 静态类型的缺点 143 | 144 | 静态类型也存在一些缺点。 145 | 146 | (1)丧失了动态类型的代码灵活性。 147 | 148 | 动态类型有非常高的灵活性,给予程序员很大的自由,静态类型将这些灵活性都剥夺了。 149 | 150 | (2)增加了编程工作量。 151 | 152 | 有了类型之后,程序员不仅需要编写功能,还需要编写类型声明,确保类型正确。这增加了不少工作量,有时会显著拖长项目的开发时间。 153 | 154 | (3)更高的学习成本。 155 | 156 | 类型系统通常比较复杂,要学习的东西更多,要求开发者付出更高的学习成本。 157 | 158 | (4)引入了独立的编译步骤。 159 | 160 | 原生的 JavaScript 代码,可以直接在 JavaScript 引擎运行。添加类型系统以后,就多出了一个单独的编译步骤,检查类型是否正确,并将 TypeScript 代码转成 JavaScript 代码,这样才能运行。 161 | 162 | (5)兼容性问题。 163 | 164 | TypeScript 依赖 JavaScript 生态,需要用到很多外部模块。但是,过去大部分 JavaScript 项目都没有做 TypeScript 适配,虽然可以自己动手做适配,不过使用时难免还是会有一些兼容性问题。 165 | 166 | 总的来说,这些缺点使得 TypeScript 不一定适合那些小型的、短期的个人项目。 167 | 168 | ## TypeScript 的历史 169 | 170 | 下面简要介绍 TypeScript 的发展历史。 171 | 172 | 2012年,微软公司宣布推出 TypeScript 语言,设计者是著名的编程语言设计大师 Anders Hejlsberg,他也是 C# 和 .NET 的设计师。 173 | 174 | 微软推出这门语言的主要目的,是让 JavaScript 程序员可以参与 Windows 8 应用程序的开发。 175 | 176 | 当时,Windows 8 即将发布,它的应用程序开发除了使用 C# 和 Visual Basic,还可以使用 HTML + JavaScript。微软希望,TypeScript 既能让 JavaScript 程序员快速上手,也能让 .Net 程序员感到熟悉。 177 | 178 | 这就是说,TypeScript 的最初动机是减少 .NET 程序员的转移和学习成本。所以,它的很多语法概念跟 .NET 很类似。 179 | 180 | 另外,TypeScript 是一个开源项目,接受社区的参与,核心的编译器采用 Apache 2.0 许可证。微软希望通过这种做法,迅速提高这门语言在社区的接受度。 181 | 182 | 2013年,微软的 Visual Studio 2013 开始内置支持 TypeScript 语言。 183 | 184 | 2014年,TypeScript 1.0 版本发布。同年,代码仓库搬到了 GitHub。 185 | 186 | 2016年,TypeScript 2.0 版本发布,引入了很多重大的语法功能。 187 | 188 | 2018年,TypeScript 3.0 版本发布。 189 | 190 | 2020年,TypeScript 4.0 版本发布。 191 | 192 | 2023年,TypeScript 5.0 版本发布。 193 | 194 | ## 如何学习 195 | 196 | 学习 TypeScript,必须先了解 JavaScript 的语法。因为真正的实际功能都是 JavaScript 引擎完成的,TypeScript 只是添加了一个类型系统。 197 | 198 | 本书假定读者已经了解 JavaScript 语言,就不再介绍它的语法了,只介绍 TypeScript 引入的新语法,主要是类型系统。 199 | 200 | 如果你对 JavaScript 还不熟悉,建议先阅读[《JavaScript 教程》](https://wangdoc.com/javascript)和[《ES6 教程》](https://wangdoc.com/es6),再来阅读本书。 201 | 202 | -------------------------------------------------------------------------------- /docs/mapping.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的类型映射 2 | 3 | ## 简介 4 | 5 | 映射(mapping)指的是,将一种类型按照映射规则,转换成另一种类型,通常用于对象类型。 6 | 7 | 举例来说,现有一个类型`A`和另一个类型`B`。 8 | 9 | ```typescript 10 | type A = { 11 | foo: number; 12 | bar: number; 13 | }; 14 | 15 | type B = { 16 | foo: string; 17 | bar: string; 18 | }; 19 | ``` 20 | 21 | 上面示例中,这两个类型的属性结构是一样的,但是属性的类型不一样。如果属性数量多的话,逐个写起来就很麻烦。 22 | 23 | 使用类型映射,就可以从类型`A`得到类型`B`。 24 | 25 | ```typescript 26 | type A = { 27 | foo: number; 28 | bar: number; 29 | }; 30 | 31 | type B = { 32 | [prop in keyof A]: string; 33 | }; 34 | ``` 35 | 36 | 上面示例中,类型`B`采用了属性名索引的写法,`[prop in keyof A]`表示依次得到类型`A`的所有属性名,然后将每个属性的类型改成`string`。 37 | 38 | 在语法上,`[prop in keyof A]`是一个属性名表达式,表示这里的属性名需要计算得到。具体的计算规则如下: 39 | 40 | - `prop`:属性名变量,名字可以随便起。 41 | - `in`:运算符,用来取出右侧的联合类型的每一个成员。 42 | - `keyof A`:返回类型`A`的每一个属性名,组成一个联合类型。 43 | 44 | 下面是复制原始类型的例子。 45 | 46 | ```typescript 47 | type A = { 48 | foo: number; 49 | bar: string; 50 | }; 51 | 52 | type B = { 53 | [prop in keyof A]: A[prop]; 54 | }; 55 | ``` 56 | 57 | 上面示例中,类型`B`原样复制了类型`A`。 58 | 59 | 为了增加代码复用性,可以把常用的映射写成泛型。 60 | 61 | ```typescript 62 | type ToBoolean = { 63 | [Property in keyof Type]: boolean; 64 | }; 65 | ``` 66 | 67 | 上面示例中,定义了一个泛型,可以将其他对象的所有属性值都改成 boolean 类型。 68 | 69 | 下面是另一个例子。 70 | 71 | ```typescript 72 | type MyObj = { 73 | [P in 0|1|2]: string; 74 | }; 75 | 76 | // 等同于 77 | type MyObj = { 78 | 0: string; 79 | 1: string; 80 | 2: string; 81 | }; 82 | ``` 83 | 84 | 上面示例中,联合类型`0|1|2`映射成了三个属性名。 85 | 86 | 不使用联合类型,直接使用某种具体类型进行属性名映射,也是可以的。 87 | 88 | ```typescript 89 | type MyObj = { 90 | [p in 'foo']: number; 91 | }; 92 | 93 | // 等同于 94 | type MyObj = { 95 | foo: number; 96 | }; 97 | ``` 98 | 99 | 上面示例中,`p in 'foo'`可以看成只有一个成员的联合类型,因此得到了只有这一个属性的对象类型。 100 | 101 | 甚至还可以写成`p in string`。 102 | 103 | ```typescript 104 | type MyObj = { 105 | [p in string]: boolean; 106 | }; 107 | 108 | // 等同于 109 | type MyObj = { 110 | [p: string]: boolean; 111 | }; 112 | ``` 113 | 114 | 上面示例中,`[p in string]`就是属性名索引形式`[p: string]`的映射写法。 115 | 116 | 通过映射,可以把某个对象的所有属性改成可选属性。 117 | 118 | ```typescript 119 | type A = { 120 | a: string; 121 | b: number; 122 | }; 123 | 124 | type B = { 125 | [Prop in keyof A]?: A[Prop]; 126 | }; 127 | ``` 128 | 129 | 上面示例中,类型`B`在类型`A`的所有属性名后面添加问号,使得这些属性都变成了可选属性。 130 | 131 | 事实上,TypeScript 的内置工具类型`Partial`,就是这样实现的。 132 | 133 | TypeScript内置的工具类型`Readonly`可以将所有属性改为只读属性,实现也是通过映射。 134 | 135 | ```typescript 136 | // 将 T 的所有属性改为只读属性 137 | type Readonly = { 138 | readonly [P in keyof T]: T[P]; 139 | }; 140 | ``` 141 | 142 | 它的用法如下。 143 | 144 | ```typescript 145 | type T = { a: string; b: number }; 146 | 147 | type ReadonlyT = Readonly; 148 | // { 149 | // readonly a: string; 150 | // readonly b: number; 151 | // } 152 | ``` 153 | 154 | ## 映射修饰符 155 | 156 | 映射会原样复制原始对象的可选属性和只读属性。 157 | 158 | ```typescript 159 | type A = { 160 | a?: string; 161 | readonly b: number; 162 | }; 163 | 164 | type B = { 165 | [Prop in keyof A]: A[Prop]; 166 | }; 167 | 168 | // 等同于 169 | type B = { 170 | a?: string; 171 | readonly b: number; 172 | }; 173 | ``` 174 | 175 | 上面示例中,类型`B`是类型`A`的映射,把`A`的可选属性和只读属性都保留下来。 176 | 177 | 如果要删改可选和只读这两个特性,并不是很方便。为了解决这个问题,TypeScript 引入了两个映射修饰符,用来在映射时添加或移除某个属性的`?`修饰符和`readonly`修饰符。 178 | 179 | - `+`修饰符:写成`+?`或`+readonly`,为映射属性添加`?`修饰符或`readonly`修饰符。 180 | - `–`修饰符:写成`-?`或`-readonly`,为映射属性移除`?`修饰符或`readonly`修饰符。 181 | 182 | 下面是添加或移除可选属性的例子。 183 | 184 | ```typescript 185 | // 添加可选属性 186 | type Optional = { 187 | [Prop in keyof Type]+?: Type[Prop]; 188 | }; 189 | 190 | // 移除可选属性 191 | type Concrete = { 192 | [Prop in keyof Type]-?: Type[Prop]; 193 | }; 194 | ``` 195 | 196 | 注意,`+?`或`-?`要写在属性名的后面。 197 | 198 | 下面是添加或移除只读属性的例子。 199 | 200 | ```typescript 201 | // 添加 readonly 202 | type CreateImmutable = { 203 | +readonly [Prop in keyof Type]: Type[Prop]; 204 | }; 205 | 206 | // 移除 readonly 207 | type CreateMutable = { 208 | -readonly [Prop in keyof Type]: Type[Prop]; 209 | }; 210 | ``` 211 | 212 | 注意,`+readonly`和`-readonly`要写在属性名的前面。 213 | 214 | 如果同时增删`?`和`readonly`这两个修饰符,写成下面这样。 215 | 216 | ```typescript 217 | // 增加 218 | type MyObj = { 219 | +readonly [P in keyof T]+?: T[P]; 220 | }; 221 | 222 | // 移除 223 | type MyObj = { 224 | -readonly [P in keyof T]-?: T[P]; 225 | } 226 | ``` 227 | 228 | TypeScript 原生的工具类型`Required`专门移除可选属性,就是使用`-?`修饰符实现的。 229 | 230 | 注意,`–?`修饰符移除了可选属性以后,该属性就不能等于`undefined`了,实际变成必选属性了。但是,这个修饰符不会移除`null`类型。 231 | 232 | 另外,`+?`修饰符可以简写成`?`,`+readonly`修饰符可以简写成`readonly`。 233 | 234 | ```typescript 235 | type A = { 236 | +readonly [P in keyof T]+?: T[P]; 237 | }; 238 | 239 | // 等同于 240 | type A = { 241 | readonly [P in keyof T]?: T[P]; 242 | }; 243 | ``` 244 | 245 | ## 键名重映射 246 | 247 | ### 语法 248 | 249 | TypeScript 4.1 引入了键名重映射(key remapping),允许改变键名。 250 | 251 | ```typescript 252 | type A = { 253 | foo: number; 254 | bar: number; 255 | }; 256 | 257 | type B = { 258 | [p in keyof A as `${p}ID`]: number; 259 | }; 260 | 261 | // 等同于 262 | type B = { 263 | fooID: number; 264 | barID: number; 265 | }; 266 | ``` 267 | 268 | 上面示例中,类型`B`是类型`A`的映射,但在映射时把属性名改掉了,在原始属性名后面加上了字符串`ID`。 269 | 270 | 可以看到,键名重映射的语法是在键名映射的后面加上`as + 新类型`子句。这里的“新类型”通常是一个模板字符串,里面可以对原始键名进行各种操作。 271 | 272 | 下面是另一个例子。 273 | 274 | ```typescript 275 | interface Person { 276 | name: string; 277 | age: number; 278 | location: string; 279 | } 280 | 281 | type Getters = { 282 | [P in keyof T 283 | as `get${Capitalize}`]: () => T[P]; 284 | }; 285 | 286 | type LazyPerson = Getters; 287 | // 等同于 288 | type LazyPerson = { 289 | getName: () => string; 290 | getAge: () => number; 291 | getLocation: () => string; 292 | } 293 | ``` 294 | 295 | 上面示例中,类型`LazyPerson`是类型`Person`的映射,并且把键名改掉了。 296 | 297 | 它的修改键名的代码是一个模板字符串`get${Capitalize}`,下面是各个部分的解释。 298 | 299 | - `get`:为键名添加的前缀。 300 | - `Capitalize`:一个原生的工具泛型,用来将`T`的首字母变成大写。 301 | - `string & P`:一个交叉类型,其中的`P`是 keyof 运算符返回的键名联合类型`string|number|symbol`,但是`Capitalize`只能接受字符串作为类型参数,因此`string & P`只返回`P`的字符串属性名。 302 | 303 | ### 属性过滤 304 | 305 | 键名重映射还可以过滤掉某些属性。下面的例子是只保留字符串属性。 306 | 307 | ```typescript 308 | type User = { 309 | name: string, 310 | age: number 311 | } 312 | 313 | type Filter = { 314 | [K in keyof T 315 | as T[K] extends string ? K : never]: string 316 | } 317 | 318 | type FilteredUser = Filter // { name: string } 319 | ``` 320 | 321 | 上面示例中,映射`K in keyof T`获取类型`T`的每一个属性以后,然后使用`as Type`修改键名。 322 | 323 | 它的键名重映射`as T[K] extends string ? K : never]`,使用了条件运算符。如果属性值`T[K]`的类型是字符串,那么属性名不变,否则属性名类型改为`never`,即这个属性名不存在。这样就等于过滤了不符合条件的属性,只保留属性值为字符串的属性。 324 | 325 | ### 联合类型的映射 326 | 327 | 由于键名重映射可以修改键名类型,所以原始键名的类型不必是`string|number|symbol`,任意的联合类型都可以用来进行键名重映射。 328 | 329 | ```typescript 330 | type S = { 331 | kind: 'square', 332 | x: number, 333 | y: number, 334 | }; 335 | 336 | type C = { 337 | kind: 'circle', 338 | radius: number, 339 | }; 340 | 341 | type MyEvents = { 342 | [E in Events as E['kind']]: (event: E) => void; 343 | } 344 | 345 | type Config = MyEvents; 346 | // 等同于 347 | type Config = { 348 | square: (event:S) => void; 349 | circle: (event:C) => void; 350 | } 351 | ``` 352 | 353 | 上面示例中,原始键名的映射是`E in Events`,这里的`Events`是两个对象组成的联合类型`S|C`。所以,`E`是一个对象,然后再通过键名重映射,得到字符串键名`E['kind']`。 354 | 355 | ## 参考链接 356 | 357 | - [Mapped Type Modifiers in TypeScript](https://mariusschulz.com/blog/mapped-type-modifiers-in-typescript), Marius Schulz 358 | 359 | -------------------------------------------------------------------------------- /docs/module.md: -------------------------------------------------------------------------------- 1 | # TypeScript 模块 2 | 3 | ## 简介 4 | 5 | 任何包含 import 或 export 语句的文件,就是一个模块(module)。相应地,如果文件不包含 export 语句,就是一个全局的脚本文件。 6 | 7 | 模块本身就是一个作用域,不属于全局作用域。模块内部的变量、函数、类只在内部可见,对于模块外部是不可见的。暴露给外部的接口,必须用 export 命令声明;如果其他文件要使用模块的接口,必须用 import 命令来输入。 8 | 9 | 如果一个文件不包含 export 语句,但是希望把它当作一个模块(即内部变量对外不可见),可以在脚本头部添加一行语句。 10 | 11 | ```typescript 12 | export {}; 13 | ``` 14 | 15 | 上面这行语句不产生任何实际作用,但会让当前文件被当作模块处理,所有它的代码都变成了内部代码。 16 | 17 | ES 模块的详细介绍,请参考 ES6 教程,这里就不重复了。本章主要介绍 TypeScript 的模块处理。 18 | 19 | TypeScript 模块除了支持所有 ES 模块的语法,特别之处在于允许输出和输入类型。 20 | 21 | ```typescript 22 | export type Bool = true | false; 23 | ``` 24 | 25 | 上面示例中,当前脚本输出一个类型别名`Bool`。这行语句把类型定义和接口输出写在一行,也可以写成两行。 26 | 27 | ```typescript 28 | type Bool = true | false; 29 | 30 | export { Bool }; 31 | ``` 32 | 33 | 假定上面的模块文件为`a.ts`,另一个文件`b.ts`就可以使用 import 语句,输入这个类型。 34 | 35 | ```typescript 36 | import { Bool } from './a'; 37 | 38 | let foo:Bool = true; 39 | ``` 40 | 41 | 上面示例中,import 语句加载的是一个类型。注意,加载文件写成`./a`,没有写脚本文件的后缀名。TypeScript 允许加载模块时,省略模块文件的后缀名,它会自动定位,将`./a`定位到`./a.ts`。 42 | 43 | 编译时,可以两个脚本同时编译。 44 | 45 | ```bash 46 | $ tsc a.ts b.ts 47 | ``` 48 | 49 | 上面命令会将`a.ts`和`b.ts`分别编译成`a.js`和`b.js`。 50 | 51 | 也可以只编译`b.ts`,因为它是入口脚本,tsc 会自动编译它依赖的所有脚本。 52 | 53 | ```bash 54 | $ tsc b.ts 55 | ``` 56 | 57 | 上面命令发现`b.ts`依赖`a.ts`,就会自动寻找`a.ts`,也将其同时编译,因此编译产物还是`a.js`和`b.js`两个文件。 58 | 59 | ## import type 语句 60 | 61 | import 在一条语句中,可以同时输入类型和正常接口。 62 | 63 | ```typescript 64 | // a.ts 65 | export interface A { 66 | foo: string; 67 | } 68 | 69 | export let a = 123; 70 | 71 | // b.ts 72 | import { A, a } from './a'; 73 | ``` 74 | 75 | 上面示例中,文件`a.ts`的 export 语句输出了一个类型`A`和一个正常接口`a`,另一个文件`b.ts`则在同一条语句中输入了类型和正常接口。 76 | 77 | 这样很不利于区分类型和正常接口,容易造成混淆。为了解决这个问题,TypeScript 引入了两个解决方法。 78 | 79 | 第一个方法是在 import 语句输入的类型前面加上`type`关键字。 80 | 81 | ```typescript 82 | import { type A, a } from './a'; 83 | ``` 84 | 85 | 上面示例中,import 语句输入的类型`A`前面有`type`关键字,表示这是一个类型。 86 | 87 | 第二个方法是使用 import type 语句,这个语句只用来输入类型,不用来输入正常接口。 88 | 89 | ```typescript 90 | // 正确 91 | import type { A } from './a'; 92 | let b:A = 'hello'; 93 | 94 | // 报错 95 | import type { a } from './a'; 96 | let b = a; 97 | ``` 98 | 99 | 上面示例中,import type 输入类型`A`是正确的,可以把`A`当作类型使用。但是,输入正常接口`a`,并把`a`当作一个值使用,就会报错。这就是说,看到`import type`,你就知道它输入的肯定是类型。 100 | 101 | import type 语句也可以输入默认类型。 102 | 103 | ```typescript 104 | import type DefaultType from 'moduleA'; 105 | ``` 106 | 107 | import type 在一个名称空间下,输入所有类型的写法如下。 108 | 109 | ```typescript 110 | import type * as TypeNS from 'moduleA'; 111 | ``` 112 | 113 | 同样的,export 语句也有两种方法,表示输出的是类型。 114 | 115 | ```typescript 116 | type A = 'a'; 117 | type B = 'b'; 118 | 119 | // 方法一 120 | export {type A, type B}; 121 | 122 | // 方法二 123 | export type {A, B}; 124 | ``` 125 | 126 | 上面示例中,方法一是使用`type`关键字作为前缀,表示输出的是类型;方法二是使用 export type 语句,表示整行输出的都是类型。 127 | 128 | 下面是 export type 将一个类作为类型输出的例子。 129 | 130 | ```typescript 131 | class Point { 132 | x: number; 133 | y: number; 134 | } 135 | 136 | export type { Point }; 137 | ``` 138 | 139 | 上面示例中,由于使用了 export type 语句,输出的并不是 Point 这个类,而是 Point 代表的实例类型。输入时,只能作为类型输入。 140 | 141 | ```typescript 142 | import type { Point } from './module'; 143 | 144 | const p:Point = { x: 0, y: 0 }; 145 | ``` 146 | 147 | 上面示例中,`Point`只能作为类型输入,不能当作正常接口使用。 148 | 149 | ## importsNotUsedAsValues 编译设置 150 | 151 | TypeScript 特有的输入类型(type)的 import 语句,编译成 JavaScript 时怎么处理呢? 152 | 153 | TypeScript 提供了`importsNotUsedAsValues`编译设置项,有三个可能的值。 154 | 155 | (1)`remove`:这是默认值,自动删除输入类型的 import 语句。 156 | 157 | (2)`preserve`:保留输入类型的 import 语句。 158 | 159 | (3)`error`:保留输入类型的 import 语句(与`preserve`相同),但是必须写成`import type`的形式,否则报错。 160 | 161 | 请看示例,下面是一个输入类型的 import 语句。 162 | 163 | ```typescript 164 | import { TypeA } from './a'; 165 | ``` 166 | 167 | 上面示例中,`TypeA`是一个类型。 168 | 169 | `remove`的编译结果会将该语句删掉。 170 | 171 | `preserve`的编译结果会保留该语句,但会删掉其中涉及类型的部分。 172 | 173 | ```typescript 174 | import './a'; 175 | ``` 176 | 177 | 上面就是`preserve`的编译结果,可以看到编译后的`import`语句不从`a.js`输入任何接口(包括类型),但是会引发`a.js`的执行,因此会保留`a.js`里面的副作用。 178 | 179 | `error`的编译结果与`preserve`相同,但在编译过程中会报错,因为它要求输入类型的`import`语句必须写成`import type` 的形式。原始语句改成下面的形式,就不会报错。 180 | 181 | ```typescript 182 | import type { TypeA } from './a'; 183 | ``` 184 | 185 | ## CommonJS 模块 186 | 187 | CommonJS 是 Node.js 的专用模块格式,与 ES 模块格式不兼容。 188 | 189 | ### import = 语句 190 | 191 | TypeScript 使用`import =`语句输入 CommonJS 模块。 192 | 193 | ```typescript 194 | import fs = require('fs'); 195 | const code = fs.readFileSync('hello.ts', 'utf8'); 196 | ``` 197 | 198 | 上面示例中,使用`import =`语句和`require()`命令输入了一个 CommonJS 模块。模块本身的用法跟 Node.js 是一样的。 199 | 200 | 除了使用`import =`语句,TypeScript 还允许使用`import * as [接口名] from "模块文件"`输入 CommonJS 模块。 201 | 202 | ```typescript 203 | import * as fs from 'fs'; 204 | // 等同于 205 | import fs = require('fs'); 206 | ``` 207 | 208 | ### export = 语句 209 | 210 | TypeScript 使用`export =`语句,输出 CommonJS 模块的对象,等同于 CommonJS 的`module.exports`对象。 211 | 212 | ```typescript 213 | let obj = { foo: 123 }; 214 | 215 | export = obj; 216 | ``` 217 | 218 | `export =`语句输出的对象,只能使用`import =`语句加载。 219 | 220 | ```typescript 221 | import obj = require('./a'); 222 | 223 | console.log(obj.foo); // 123 224 | ``` 225 | 226 | ## 模块定位 227 | 228 | 模块定位(module resolution)指的是一种算法,用来确定 import 语句和 export 语句里面的模块文件位置。 229 | 230 | ```typescript 231 | // 相对模块 232 | import { TypeA } from './a'; 233 | 234 | // 非相对模块 235 | import * as $ from "jquery"; 236 | ``` 237 | 238 | 上面示例中,TypeScript 怎么确定`./a`或`jquery`到底是指哪一个模块,具体位置在哪里,用到的算法就叫做“模块定位”。 239 | 240 | 编译参数`moduleResolution`,用来指定具体使用哪一种定位算法。常用的算法有两种:`Classic`和`Node`。 241 | 242 | 如果没有指定`moduleResolution`,它的默认值与编译参数`module`有关。`module`设为`commonjs`时(项目脚本采用 CommonJS 模块格式),`moduleResolution`的默认值为`Node`,即采用 Node.js 的模块定位算法。其他情况下(`module`设为 es2015、 esnext、amd, system, umd 等等),就采用`Classic`定位算法。 243 | 244 | ### 相对模块,非相对模块 245 | 246 | 加载模块时,目标模块分为相对模块(relative import)和非相对模块两种(non-relative import)。 247 | 248 | 相对模块指的是路径以`/`、`./`、`../`开头的模块。下面 import 语句加载的模块,都是相对模块。 249 | 250 | - `import Entry from "./components/Entry";` 251 | - `import { DefaultHeaders } from "../constants/http";` 252 | - `import "/mod";` 253 | 254 | 相对模块的定位,是根据当前脚本的位置进行计算的,一般用于保存在当前项目目录结构中的模块脚本。 255 | 256 | 非相对模块指的是不带有路径信息的模块。下面 import 语句加载的模块,都是非相对模块。 257 | 258 | - `import * as $ from "jquery";` 259 | - `import { Component } from "@angular/core";` 260 | 261 | 非相对模块的定位,是由`baseUrl`属性或模块映射而确定的,通常用于加载外部模块。 262 | 263 | ### Classic 方法 264 | 265 | Classic 方法以当前脚本的路径作为“基准路径”,计算相对模块的位置。比如,脚本`a.ts`里面有一行代码`import { b } from "./b"`,那么 TypeScript 就会在`a.ts`所在的目录,查找`b.ts`和`b.d.ts`。 266 | 267 | 至于非相对模块,也是以当前脚本的路径作为起点,一层层查找上级目录。比如,脚本`a.ts`里面有一行代码`import { b } from "b"`,那么就会依次在每一级上层目录里面,查找`b.ts`和`b.d.ts`。 268 | 269 | ### Node 方法 270 | 271 | Node 方法就是模拟 Node.js 的模块加载方法,也就是`require()`的实现方法。 272 | 273 | 相对模块依然是以当前脚本的路径作为“基准路径”。比如,脚本文件`a.ts`里面有一行代码`let x = require("./b");`,TypeScript 按照以下顺序查找。 274 | 275 | 1. 当前目录是否包含`b.ts`、`b.tsx`、`b.d.ts`。如果不存在就执行下一步。 276 | 1. 当前目录是否存在子目录`b`,该子目录里面的`package.json`文件是否有`types`字段指定了模块入口文件。如果不存在就执行下一步。 277 | 1. 当前目录的子目录`b`是否包含`index.ts`、`index.tsx`、`index.d.ts`。如果不存在就报错。 278 | 279 | 非相对模块则是以当前脚本的路径作为起点,逐级向上层目录查找是否存在子目录`node_modules`。比如,脚本文件`a.js`有一行`let x = require("b");`,TypeScript 按照以下顺序进行查找。 280 | 281 | 1. 当前目录的子目录`node_modules`是否包含`b.ts`、`b.tsx`、`b.d.ts`。 282 | 2. 当前目录的子目录`node_modules`,是否存在文件`package.json`,该文件的`types`字段是否指定了入口文件,如果是的就加载该文件。 283 | 3. 当前目录的子目录`node_modules`里面,是否包含子目录`@types`,在该目录中查找文件`b.d.ts`。 284 | 4. 当前目录的子目录`node_modules`里面,是否包含子目录`b`,在该目录中查找`index.ts`、`index.tsx`、`index.d.ts`。 285 | 5. 进入上一层目录,重复上面4步,直到找到为止。 286 | 287 | ### 路径映射 288 | 289 | TypeScript 允许开发者在`tsconfig.json`文件里面,手动指定脚本模块的路径。 290 | 291 | (1)baseUrl 292 | 293 | `baseUrl`字段可以手动指定脚本模块的基准目录。 294 | 295 | ```typescript 296 | { 297 | "compilerOptions": { 298 | "baseUrl": "." 299 | } 300 | } 301 | ``` 302 | 303 | 上面示例中,`baseUrl`是一个点,表示基准目录就是`tsconfig.json`所在的目录。 304 | 305 | (2)paths 306 | 307 | `paths`字段指定非相对路径的模块与实际脚本的映射。 308 | 309 | ```typescript 310 | { 311 | "compilerOptions": { 312 | "baseUrl": ".", 313 | "paths": { 314 | "jquery": ["node_modules/jquery/dist/jquery"] 315 | } 316 | } 317 | } 318 | ``` 319 | 320 | 上面示例中,加载模块`jquery`时,实际加载的脚本是`node_modules/jquery/dist/jquery`,它的位置要根据`baseUrl`字段计算得到。 321 | 322 | 注意,上例的`jquery`属性的值是一个数组,可以指定多个路径。如果第一个脚本路径不存在,那么就加载第二个路径,以此类推。 323 | 324 | (3)rootDirs 325 | 326 | `rootDirs`字段指定模块定位时必须查找的其他目录。 327 | 328 | ```typescript 329 | { 330 | "compilerOptions": { 331 | "rootDirs": ["src/zh", "src/de", "src/#{locale}"] 332 | } 333 | } 334 | ``` 335 | 336 | 上面示例中,`rootDirs`指定了模块定位时,需要查找的不同的国际化目录。 337 | 338 | ### tsc 的`--traceResolution`参数 339 | 340 | 由于模块定位的过程很复杂,tsc 命令有一个`--traceResolution`参数,能够在编译时在命令行显示模块定位的每一步。 341 | 342 | ```bash 343 | $ tsc --traceResolution 344 | ``` 345 | 346 | 上面示例中,`traceResolution`会输出模块定位的判断过程。 347 | 348 | ### tsc 的`--noResolve`参数 349 | 350 | tsc 命令的`--noResolve`参数,表示模块定位时,只考虑在命令行传入的模块。 351 | 352 | 举例来说,`app.ts`包含如下两行代码。 353 | 354 | ```typescript 355 | import * as A from "moduleA"; 356 | import * as B from "moduleB"; 357 | ``` 358 | 359 | 使用下面的命令进行编译。 360 | 361 | ```bash 362 | $ tsc app.ts moduleA.ts --noResolve 363 | ``` 364 | 365 | 上面命令使用`--noResolve`参数,因此可以定位到`moduleA.ts`,因为它从命令行传入了;无法定位到`moduleB`,因为它没有传入,因此会报错。 366 | 367 | ## 参考链接 368 | 369 | - [tsconfig 之 importsNotUsedAsValues 属性](https://blog.51cto.com/u_13028258/5754309) 370 | 371 | -------------------------------------------------------------------------------- /docs/namespace.md: -------------------------------------------------------------------------------- 1 | # TypeScript namespace 2 | 3 | namespace 是一种将相关代码组织在一起的方式,中文译为“命名空间”。 4 | 5 | 它出现在 ES 模块诞生之前,作为 TypeScript 自己的模块格式而发明的。但是,自从有了 ES 模块,官方已经不推荐使用 namespace 了。 6 | 7 | ## 基本用法 8 | 9 | namespace 用来建立一个容器,内部的所有变量和函数,都必须在这个容器里面使用。 10 | 11 | ```typescript 12 | namespace Utils { 13 | function isString(value:any) { 14 | return typeof value === 'string'; 15 | } 16 | 17 | // 正确 18 | isString('yes'); 19 | } 20 | 21 | Utils.isString('no'); // 报错 22 | ``` 23 | 24 | 上面示例中,命名空间`Utils`里面定义了一个函数`isString()`,它只能在`Utils`里面使用,如果用于外部就会报错。 25 | 26 | 如果要在命名空间以外使用内部成员,就必须为该成员加上`export`前缀,表示对外输出该成员。 27 | 28 | ```typescript 29 | namespace Utility { 30 | export function log(msg:string) { 31 | console.log(msg); 32 | } 33 | export function error(msg:string) { 34 | console.error(msg); 35 | } 36 | } 37 | 38 | Utility.log('Call me'); 39 | Utility.error('maybe!'); 40 | ``` 41 | 42 | 上面示例中,只要加上`export`前缀,就可以在命名空间外部使用内部成员。 43 | 44 | 编译出来的 JavaScript 代码如下。 45 | 46 | ```typescript 47 | var Utility; 48 | 49 | (function (Utility) { 50 | function log(msg) { 51 | console.log(msg); 52 | } 53 | Utility.log = log; 54 | function error(msg) { 55 | console.error(msg); 56 | } 57 | Utility.error = error; 58 | })(Utility || (Utility = {})); 59 | ``` 60 | 61 | 上面代码中,命名空间`Utility`变成了 JavaScript 的一个对象,凡是`export`的内部成员,都成了该对象的属性。 62 | 63 | 这就是说,namespace 会变成一个值,保留在编译后的代码中。这一点要小心,它不是纯的类型代码。 64 | 65 | namespace 内部还可以使用`import`命令输入外部成员,相当于为外部成员起别名。当外部成员的名字比较长时,别名能够简化代码。 66 | 67 | ```typescript 68 | namespace Utils { 69 | export function isString(value:any) { 70 | return typeof value === 'string'; 71 | } 72 | } 73 | 74 | namespace App { 75 | import isString = Utils.isString; 76 | 77 | isString('yes'); 78 | // 等同于 79 | Utils.isString('yes'); 80 | } 81 | ``` 82 | 83 | 上面示例中,`import`命令指定在命名空间`App`里面,外部成员`Utils.isString`的别名为`isString`。 84 | 85 | `import`命令也可以在 namespace 外部,指定别名。 86 | 87 | ```typescript 88 | namespace Shapes { 89 | export namespace Polygons { 90 | export class Triangle {} 91 | export class Square {} 92 | } 93 | } 94 | 95 | import polygons = Shapes.Polygons; 96 | 97 | // 等同于 new Shapes.Polygons.Square() 98 | let sq = new polygons.Square(); 99 | ``` 100 | 101 | 上面示例中,`import`命令在命名空间`Shapes`的外部,指定` Shapes.Polygons`的别名为`polygons`。 102 | 103 | namespace 可以嵌套。 104 | 105 | ```typescript 106 | namespace Utils { 107 | export namespace Messaging { 108 | export function log(msg:string) { 109 | console.log(msg); 110 | } 111 | } 112 | } 113 | 114 | Utils.Messaging.log('hello') // "hello" 115 | ``` 116 | 117 | 上面示例中,命名空间`Utils`内部还有一个命名空间`Messaging`。注意,如果要在外部使用`Messaging`,必须在它前面加上`export`命令。 118 | 119 | 使用嵌套的命名空间,必须从最外层开始引用,比如`Utils.Messaging.log()`。 120 | 121 | namespace 不仅可以包含实义代码,还可以包括类型代码。 122 | 123 | ```typescript 124 | namespace N { 125 | export interface MyInterface{} 126 | export class MyClass{} 127 | } 128 | ``` 129 | 130 | 上面代码中,命令空间`N`不仅对外输出类,还对外输出一个接口,它们都可以用作类型。 131 | 132 | namespace 与模块的作用是一致的,都是把相关代码组织在一起,对外输出接口。区别是一个文件只能有一个模块,但可以有多个 namespace。由于模块可以取代 namespace,而且是 JavaScript 的标准语法,还不需要编译转换,所以建议总是使用模块,替代 namespace。 133 | 134 | 如果 namespace 代码放在一个单独的文件里,那么引入这个文件需要使用三斜杠的语法。 135 | 136 | ```typescript 137 | /// 138 | ``` 139 | 140 | ## namespace 的输出 141 | 142 | namespace 本身也可以使用`export`命令输出,供其他文件使用。 143 | 144 | ```typescript 145 | // shapes.ts 146 | export namespace Shapes { 147 | export class Triangle { 148 | // ... 149 | } 150 | export class Square { 151 | // ... 152 | } 153 | } 154 | ``` 155 | 156 | 上面示例是一个文件`shapes.ts`,里面使用`export`命令,输出了一个命名空间`Shapes`。 157 | 158 | 其他脚本文件使用`import`命令,加载这个命名空间。 159 | 160 | ```typescript 161 | // 写法一 162 | import { Shapes } from './shapes'; 163 | let t = new Shapes.Triangle(); 164 | 165 | // 写法二 166 | import * as shapes from "./shapes"; 167 | let t = new shapes.Shapes.Triangle(); 168 | ``` 169 | 170 | 不过,更好的方法还是建议使用模块,采用模块的输出和输入。 171 | 172 | ```typescript 173 | // shapes.ts 174 | export class Triangle { 175 | /* ... */ 176 | } 177 | export class Square { 178 | /* ... */ 179 | } 180 | 181 | // shapeConsumer.ts 182 | import * as shapes from "./shapes"; 183 | let t = new shapes.Triangle(); 184 | ``` 185 | 186 | 上面示例中,使用模块的输出和输入,改写了前面的例子。 187 | 188 | ## namespace 的合并 189 | 190 | 多个同名的 namespace 会自动合并,这一点跟 interface 一样。 191 | 192 | ```typescript 193 | namespace Animals { 194 | export class Cat {} 195 | } 196 | namespace Animals { 197 | export interface Legged { 198 | numberOfLegs: number; 199 | } 200 | export class Dog {} 201 | } 202 | 203 | // 等同于 204 | namespace Animals { 205 | export interface Legged { 206 | numberOfLegs: number; 207 | } 208 | export class Cat {} 209 | export class Dog {} 210 | } 211 | ``` 212 | 213 | 这样做的目的是,如果同名的命名空间分布在不同的文件中,TypeScript 最终会将它们合并在一起。这样就比较方便扩展别人的代码。 214 | 215 | 合并命名空间时,命名空间中的非`export`的成员不会被合并,但是它们只能在各自的命名空间中使用。 216 | 217 | ```typescript 218 | namespace N { 219 | const a = 0; 220 | 221 | export function foo() { 222 | console.log(a); // 正确 223 | } 224 | } 225 | 226 | namespace N { 227 | export function bar() { 228 | foo(); // 正确 229 | console.log(a); // 报错 230 | } 231 | } 232 | ``` 233 | 234 | 上面示例中,变量`a`是第一个名称空间`N`的非对外成员,它只在第一个名称空间可用。 235 | 236 | 命名空间还可以跟同名函数合并,但是要求同名函数必须在命名空间之前声明。这样做是为了确保先创建出一个函数对象,然后同名的命名空间就相当于给这个函数对象添加额外的属性。 237 | 238 | ```typescript 239 | function f() { 240 | return f.version; 241 | } 242 | 243 | namespace f { 244 | export const version = '1.0'; 245 | } 246 | 247 | f() // '1.0' 248 | f.version // '1.0' 249 | ``` 250 | 251 | 上面示例中,函数`f()`与命名空间`f`合并,相当于命名空间为函数对象`f`添加属性。 252 | 253 | 命名空间也能与同名 class 合并,同样要求class 必须在命名空间之前声明,原因同上。 254 | 255 | ```typescript 256 | class C { 257 | foo = 1; 258 | } 259 | 260 | namespace C { 261 | export const bar = 2; 262 | } 263 | 264 | C.bar // 2 265 | ``` 266 | 267 | 上面示例中,名称空间`C`为类`C`添加了一个静态属性`bar`。 268 | 269 | 命名空间还能与同名 Enum 合并。 270 | 271 | ```typescript 272 | enum E { 273 | A, 274 | B, 275 | C, 276 | } 277 | 278 | namespace E { 279 | export function foo() { 280 | console.log(E.C); 281 | } 282 | } 283 | 284 | E.foo() // 2 285 | ``` 286 | 287 | 上面示例中,命名空间`E`为枚举`E`添加了一个`foo()`方法。 288 | 289 | 注意,Enum 成员与命名空间导出成员不允许同名。 290 | 291 | ```typescript 292 | enum E { 293 | A, // 报错 294 | B, 295 | } 296 | 297 | namespace E { 298 | export function A() {} // 报错 299 | } 300 | ``` 301 | 302 | 上面示例中,同名 Enum 与命名空间有同名成员,结果报错。 303 | 304 | -------------------------------------------------------------------------------- /docs/narrowing.md: -------------------------------------------------------------------------------- 1 | # TypeScript 类型缩小 2 | 3 | TypeScript 变量的值可以变,但是类型通常是不变的。唯一允许的改变,就是类型缩小,就是将变量值的范围缩得更小。 4 | 5 | ## 手动类型缩小 6 | 7 | 如果一个变量属于联合类型,所以使用时一般需要缩小类型。 8 | 9 | 第一种方法是使用`if`判断。 10 | 11 | ```typescript 12 | function getScore(value: number|string): number { 13 | if (typeof value === 'number') { // (A) 14 | // %inferred-type: number 15 | value; 16 | return value; 17 | } 18 | if (typeof value === 'string') { // (B) 19 | // %inferred-type: string 20 | value; 21 | return value.length; 22 | } 23 | throw new Error('Unsupported value: ' + value); 24 | } 25 | ``` 26 | 27 | 如果一个值是`any`或`unknown`,你又想对它进行处理,就必须先缩小类型。 28 | 29 | ```typescript 30 | function parseStringLiteral(stringLiteral: string): string { 31 | const result: unknown = JSON.parse(stringLiteral); 32 | if (typeof result === 'string') { // (A) 33 | return result; 34 | } 35 | throw new Error('Not a string literal: ' + stringLiteral); 36 | } 37 | ``` 38 | 39 | 下面是另一个例子。 40 | 41 | ```typescript 42 | interface Book { 43 | title: null | string; 44 | isbn: string; 45 | } 46 | 47 | function getTitle(book: Book) { 48 | if (book.title === null) { 49 | // %inferred-type: null 50 | book.title; 51 | return '(Untitled)'; 52 | } else { 53 | // %inferred-type: string 54 | book.title; 55 | return book.title; 56 | } 57 | } 58 | ``` 59 | 60 | 缩小类型的前提是,需要先获取类型。获取类型的几种方法如下。 61 | 62 | ```typescript 63 | function func(value: Function|Date|number[]) { 64 | if (typeof value === 'function') { 65 | // %inferred-type: Function 66 | value; 67 | } 68 | 69 | if (value instanceof Date) { 70 | // %inferred-type: Date 71 | value; 72 | } 73 | 74 | if (Array.isArray(value)) { 75 | // %inferred-type: number[] 76 | value; 77 | } 78 | } 79 | ``` 80 | 81 | ### typeof 运算符 82 | 83 | 第二种方法是使用`switch`缩小类型。 84 | 85 | ```typescript 86 | function getScore(value: number|string): number { 87 | switch (typeof value) { 88 | case 'number': 89 | // %inferred-type: number 90 | value; 91 | return value; 92 | case 'string': 93 | // %inferred-type: string 94 | value; 95 | return value.length; 96 | default: 97 | throw new Error('Unsupported value: ' + value); 98 | } 99 | } 100 | ``` 101 | 102 | ### instanceof 运算符 103 | 104 | 第三种方法是instanceof运算符。它能够检测实例对象与构造函数之间的关系。instanceof运算符的左操作数为实例对象,右操作数为构造函数,若构造函数的prototype属性值存在于实例对象的原型链上,则返回true;否则,返回false。 105 | 106 | ```typescript 107 | function f(x: Date | RegExp) { 108 | if (x instanceof Date) { 109 | x; // Date 110 | } 111 | 112 | if (x instanceof RegExp) { 113 | x; // RegExp 114 | } 115 | } 116 | ``` 117 | 118 | instanceof类型守卫同样适用于自定义构造函数,并对其实例对象进行类型细化。 119 | 120 | ```typescript 121 | class A {} 122 | class B {} 123 | 124 | function f(x: A | B) { 125 | if (x instanceof A) { 126 | x; // A 127 | } 128 | 129 | if (x instanceof B) { 130 | x; // B 131 | } 132 | } 133 | ``` 134 | 135 | ### in 运算符 136 | 137 | 第四种方法是使用in运算符。 138 | 139 | in运算符是JavaScript中的关系运算符之一,用来判断对象自身或其原型链中是否存在给定的属性,若存在则返回true,否则返回false。in运算符有两个操作数,左操作数为待测试的属性名,右操作数为测试对象。 140 | 141 | in类型守卫根据in运算符的测试结果,将右操作数的类型细化为具体的对象类型。 142 | 143 | ```typescript 144 | interface A { 145 | x: number; 146 | } 147 | interface B { 148 | y: string; 149 | } 150 | 151 | function f(x: A | B) { 152 | if ('x' in x) { 153 | x; // A 154 | } else { 155 | x; // B 156 | } 157 | } 158 | ``` 159 | 160 | ```typescript 161 | interface A { a: number } 162 | interface B { b: number } 163 | function pickAB(ab: A | B) { 164 | if ('a' in ab) { 165 | ab // Type is A 166 | } else { 167 | ab // Type is B 168 | } 169 | ab // Type is A | B 170 | } 171 | ``` 172 | 173 | 缩小对象的属性,要用`in`运算符。 174 | 175 | ```typescript 176 | type FirstOrSecond = 177 | | {first: string} 178 | | {second: string}; 179 | 180 | function func(firstOrSecond: FirstOrSecond) { 181 | if ('second' in firstOrSecond) { 182 | // %inferred-type: { second: string; } 183 | firstOrSecond; 184 | } 185 | } 186 | 187 | // 错误 188 | function func(firstOrSecond: FirstOrSecond) { 189 | // @ts-expect-error: Property 'second' does not exist on 190 | // type 'FirstOrSecond'. [...] 191 | if (firstOrSecond.second !== undefined) { 192 | // ··· 193 | } 194 | } 195 | ``` 196 | 197 | `in`运算符只能用于联合类型,不能用于检查一个属性是否存在。 198 | 199 | ```typescript 200 | function func(obj: object) { 201 | if ('name' in obj) { 202 | // %inferred-type: object 203 | obj; 204 | 205 | // 报错 206 | obj.name; 207 | } 208 | } 209 | ``` 210 | 211 | ### 特征属性 212 | 213 | 对于不同对象之间的区分,还可以人为地为每一类对象设置一个特征属性。 214 | 215 | ```typescript 216 | interface UploadEvent { 217 | type: 'upload'; 218 | filename: string; 219 | contents: string 220 | } 221 | interface DownloadEvent { type: 'download'; filename: string; } 222 | type AppEvent = UploadEvent | DownloadEvent; 223 | 224 | function handleEvent(e: AppEvent) { 225 | switch (e.type) { 226 | case 'download': 227 | e // Type is DownloadEvent 228 | break; 229 | case 'upload': 230 | e; // Type is UploadEvent 231 | break; 232 | } 233 | } 234 | ``` 235 | 236 | ## any 类型的细化 237 | 238 | TypeScript 推断变量类型时,会根据获知的信息,不断改变推断出来的类型,越来越细化。这种现象在`any`身上特别明显。 239 | 240 | ```typescript 241 | function range( 242 | start:number, 243 | limit:number 244 | ) { 245 | const out = []; // 类型为 any[] 246 | for (let i = start; i < limit; i++) { 247 | out.push(i); 248 | } 249 | return out; // 类型为 number[] 250 | } 251 | ``` 252 | 253 | 上面示例中,变量`out`的类型一开始推断为`any[]`,后来在里面放入数值,类型就变为`number[]`。 254 | 255 | 再看下面的例子。 256 | 257 | ```typescript 258 | const result = []; // 类型为 any[] 259 | result.push('a'); 260 | result // 类型为 string[] 261 | result.push(1); 262 | result // 类型为 (string | number)[] 263 | ``` 264 | 265 | 上面示例中,数组`result`随着成员类型的不同,而不断改变自己的类型。 266 | 267 | 注意,这种`any`类型的细化,只在打开了编译选项`noImplicitAny`时发生。 268 | 269 | 这时,如果在变量的推断类型还为`any`时(即没有任何写操作),就去输出(或读取)该变量,则会报错,因为这时推断还没有完成,无法满足`noImplicitAny`的要求。 270 | 271 | ```typescript 272 | const result = []; // 类型为 any[] 273 | console.log(typeof result); // 报错 274 | result.push('a'); // 类型为 string[] 275 | ``` 276 | 277 | 上面示例中,只有运行完第三行,`result`的类型才能完成第一次推断,所以第二行读取`result`就会报错。 278 | 279 | ## is 运算符 280 | 281 | `is`运算符返回一个布尔值,用来判断左侧的值是否属于右侧的类型。 282 | 283 | ```typescript 284 | function isInputElement(el: HTMLElement): el is HTMLInputElement { 285 | return 'value' in el; 286 | } 287 | 288 | function getElementContent(el: HTMLElement) { 289 | if (isInputElement(el)) { 290 | el; // Type is HTMLInputElement 291 | return el.value; 292 | } 293 | el; // Type is HTMLElement 294 | return el.textContent; 295 | } 296 | ``` 297 | 298 | ```typescript 299 | function isDefined(x: T | undefined): x is T { 300 | return x !== undefined; 301 | } 302 | ``` 303 | 304 | -------------------------------------------------------------------------------- /docs/npm.md: -------------------------------------------------------------------------------- 1 | # TypeScript 项目使用 npm 模块 2 | 3 | ## 简介 4 | 5 | npm 模块都是 JavaScript 代码。即使模块是用 TypeScript 写的,还是必须编译成 JavaScript 再发布,保证模块可以在没有 TypeScript 的环境运行。 6 | 7 | 问题就来了,TypeScript 项目开发时,加载外部 npm 模块,如果拿不到该模块的类型信息,就会导致无法开发。所以,必须有一个方法,可以拿到模块的类型信息。 8 | 9 | 有些 npm 模块本身可能包含`.d.ts`文件甚至完整的 TypeScript 代码。它的`package.json`文件里面有一个`types`字段,指向一个`.d.ts`文件,这就是它的类型声明文件。 10 | 11 | ```javascript 12 | { 13 | "name": "left-pad", 14 | "version": "1.3.0", 15 | "description": "String left pad", 16 | "main": "index.js", 17 | "types": "index.d.ts", 18 | // ... 19 | } 20 | ``` 21 | 22 | 如果某个模块没有`.d.ts`文件,TypeScript 官方和社区就自发为常用模块添加类型描述,可以去[官方网站](https://www.typescriptlang.org/dt/search)搜索,然后安装网站给出的 npm 类型模块,通常是`@types/[模块名]`。 23 | 24 | ```bash 25 | $ npm install --save lodash 26 | $ npm install --save @types/lodash 27 | ``` 28 | 29 | lodash 的类型描述就是`@types/lodash`的文件`index.d.ts`。 30 | 31 | ## TS 模块转 npm 模块 32 | 33 | TS 代码放在`ts`子目录,编译出来的 CommonJS 代码放在`dist`子目录。 34 | 35 | ## 如何写 TypeScript 模块 36 | 37 | 首先,创建模块目录,然后在该目录里面新建一个`tsconfig.json`。 38 | 39 | ```json 40 | { 41 | "compilerOptions": { 42 | "module": "commonjs", 43 | "target": "es2015", 44 | "declaration": true, 45 | "outDir": "./dist" 46 | }, 47 | "include": [ 48 | "src/**/*" 49 | ] 50 | } 51 | ``` 52 | 53 | - `"declaration": true`:生成 .d.ts 文件,方便其他使用 TypeScript 的开发者加载你的库。 54 | - `"module": "commonjs"`:编译后的模块格式为`commonjs`,表示该模块供 Node.js 使用。如果供浏览器使用,则要写成`"module": "esnext"`。 55 | - `"target": "es2015"`:生成的 JavaScript 代码版本为 ES2015,需要 Node.js 8 以上版本。 56 | - `"outDir": "./dist"`:编译后的文件放在`./dist`目录。 57 | - `include`:指定需要编译的文件。 58 | 59 | 然后,使用 TypeScript 编写仓库代码。可以在`src`子目录里面,编写一个入口文件`index.ts`。 60 | 61 | 最后,编写`package.json`。 62 | 63 | ```typescript 64 | { 65 | "name": "hwrld", 66 | "version": "1.0.0", 67 | "description": "Can log \"hello world\" and \"goodbye world\" to the console!", 68 | "main": "dist/index.js", 69 | "types": "dist/index.d.ts", 70 | "files": [ 71 | "/dist" 72 | ] 73 | } 74 | ``` 75 | 76 | 里面的`"types": "dist/index.d.ts"`字段指定类型声明文件,否则使用这个库的 TypeScript 开发者找不到类型声明文件。`files`属性指定打包进入 npm 模块的文件。 77 | 78 | 然后,就是编译和发布。 79 | 80 | ```bash 81 | $ tsc 82 | $ npm publish 83 | ``` 84 | 85 | ## 参考链接 86 | 87 | - [How to Write a TypeScript Library](https://www.tsmean.com/articles/how-to-write-a-typescript-library/), by tsmean 88 | 89 | -------------------------------------------------------------------------------- /docs/object.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的对象类型 2 | 3 | ## 简介 4 | 5 | 除了原始类型,对象是 JavaScript 最基本的数据结构。TypeScript 对于对象类型有很多规则。 6 | 7 | 对象类型的最简单声明方法,就是使用大括号表示对象,在大括号内部声明每个属性和方法的类型。 8 | 9 | ```typescript 10 | const obj:{ 11 | x:number; 12 | y:number; 13 | } = { x: 1, y: 1 }; 14 | ``` 15 | 16 | 上面示例中,对象`obj`的类型就写在变量名后面,使用大括号描述,内部声明每个属性的属性名和类型。 17 | 18 | 属性的类型可以用分号结尾,也可以用逗号结尾。 19 | 20 | ```typescript 21 | // 属性类型以分号结尾 22 | type MyObj = { 23 | x:number; 24 | y:number; 25 | }; 26 | 27 | // 属性类型以逗号结尾 28 | type MyObj = { 29 | x:number, 30 | y:number, 31 | }; 32 | ``` 33 | 34 | 最后一个属性后面,可以写分号或逗号,也可以不写。 35 | 36 | 一旦声明了类型,对象赋值时,就不能缺少指定的属性,也不能有多余的属性。 37 | 38 | ```typescript 39 | type MyObj = { 40 | x:number; 41 | y:number; 42 | }; 43 | 44 | const o1:MyObj = { x: 1 }; // 报错 45 | const o2:MyObj = { x: 1, y: 1, z: 1 }; // 报错 46 | ``` 47 | 48 | 上面示例中,变量`o1`缺少了属性`y`,变量`o2`多出了属性`z`,都会报错。 49 | 50 | 读写不存在的属性也会报错。 51 | 52 | ```typescript 53 | const obj:{ 54 | x:number; 55 | y:number; 56 | } = { x: 1, y: 1 }; 57 | 58 | console.log(obj.z); // 报错 59 | obj.z = 1; // 报错 60 | ``` 61 | 62 | 上面示例中,读写不存在的属性`z`都会报错。 63 | 64 | 同样地,也不能删除类型声明中存在的属性,修改属性值是可以的。 65 | 66 | ```typescript 67 | const myUser = { 68 | name: "Sabrina", 69 | }; 70 | 71 | delete myUser.name // 报错 72 | myUser.name = "Cynthia"; // 正确 73 | ``` 74 | 75 | 上面声明中,删除类型声明中存在的属性`name`会报错,但是可以修改它的值。 76 | 77 | 对象的方法使用函数类型描述。 78 | 79 | ```typescript 80 | const obj:{ 81 | x: number; 82 | y: number; 83 | add(x:number, y:number): number; 84 | // 或者写成 85 | // add: (x:number, y:number) => number; 86 | } = { 87 | x: 1, 88 | y: 1, 89 | add(x, y) { 90 | return x + y; 91 | } 92 | }; 93 | ``` 94 | 95 | 上面示例中,对象`obj`有一个方法`add()`,需要定义它的参数类型和返回值类型。 96 | 97 | 对象类型可以使用方括号读取属性的类型。 98 | 99 | ```typescript 100 | type User = { 101 | name: string, 102 | age: number 103 | }; 104 | type Name = User['name']; // string 105 | ``` 106 | 107 | 上面示例中,对象类型`User`使用方括号,读取了属性`name`的类型(`string`)。 108 | 109 | 除了`type`命令可以为对象类型声明一个别名,TypeScript 还提供了`interface`命令,可以把对象类型提炼为一个接口。 110 | 111 | ```typescript 112 | // 写法一 113 | type MyObj = { 114 | x:number; 115 | y:number; 116 | }; 117 | 118 | const obj:MyObj = { x: 1, y: 1 }; 119 | 120 | // 写法二 121 | interface MyObj { 122 | x: number; 123 | y: number; 124 | } 125 | 126 | const obj:MyObj = { x: 1, y: 1 }; 127 | ``` 128 | 129 | 上面示例中,写法一是`type`命令的用法,写法二是`interface`命令的用法。`interface`命令的详细解释,以及与`type`命令的区别,详见《Interface》一章。 130 | 131 | 注意,TypeScript 不区分对象自身的属性和继承的属性,一律视为对象的属性。 132 | 133 | ```typescript 134 | interface MyInterface { 135 | toString(): string; // 继承的属性 136 | prop: number; // 自身的属性 137 | } 138 | 139 | const obj:MyInterface = { // 正确 140 | prop: 123, 141 | }; 142 | ``` 143 | 144 | 上面示例中,`obj`只写了`prop`属性,但是不报错。因为它可以继承原型上面的`toString()`方法。 145 | 146 | ## 可选属性 147 | 148 | 如果某个属性是可选的(即可以忽略),需要在属性名后面加一个问号。 149 | 150 | ```typescript 151 | const obj: { 152 | x: number; 153 | y?: number; 154 | } = { x: 1 }; 155 | ``` 156 | 157 | 上面示例中,属性`y`是可选的。 158 | 159 | 可选属性等同于允许赋值为`undefined`,下面两种写法是等效的。 160 | 161 | ```typescript 162 | type User = { 163 | firstName: string; 164 | lastName?: string; 165 | }; 166 | 167 | // 等同于 168 | type User = { 169 | firstName: string; 170 | lastName?: string|undefined; 171 | }; 172 | ``` 173 | 174 | 上面示例中,类型`User`的可选属性`lastName`可以是字符串,也可以是`undefined`,即可选属性可以赋值为`undefined`。 175 | 176 | ```typescript 177 | const obj: { 178 | x: number; 179 | y?: number; 180 | } = { x: 1, y: undefined }; 181 | ``` 182 | 183 | 上面示例中,可选属性`y`赋值为`undefined`,不会报错。 184 | 185 | 同样地,读取一个没有赋值的可选属性时,返回`undefined`。 186 | 187 | ```typescript 188 | type MyObj = { 189 | x: string, 190 | y?: string 191 | }; 192 | 193 | const obj:MyObj = { x: 'hello' }; 194 | obj.y.toLowerCase() // 报错 195 | ``` 196 | 197 | 上面示例中,最后一行会报错,因为`obj.y`返回`undefined`,无法对其调用`toLowerCase()`。 198 | 199 | 所以,读取可选属性之前,必须检查一下是否为`undefined`。 200 | 201 | ```typescript 202 | const user:{ 203 | firstName: string; 204 | lastName?: string; 205 | } = { firstName: 'Foo'}; 206 | 207 | if (user.lastName !== undefined) { 208 | console.log(`hello ${user.firstName} ${user.lastName}`) 209 | } 210 | ``` 211 | 212 | 上面示例中,`lastName`是可选属性,需要判断是否为`undefined`以后,才能使用。建议使用下面的写法。 213 | 214 | ```typescript 215 | // 写法一 216 | let firstName = (user.firstName === undefined) 217 | ? 'Foo' : user.firstName; 218 | let lastName = (user.lastName === undefined) 219 | ? 'Bar' : user.lastName; 220 | 221 | // 写法二 222 | let firstName = user.firstName ?? 'Foo'; 223 | let lastName = user.lastName ?? 'Bar'; 224 | ``` 225 | 226 | 上面示例中,写法一使用三元运算符`?:`,判断是否为`undefined`,并设置默认值。写法二使用 Null 判断运算符`??`,与写法一的作用完全相同。 227 | 228 | TypeScript 提供编译设置`ExactOptionalPropertyTypes`,只要同时打开这个设置和`strictNullChecks`,可选属性就不能设为`undefined`。 229 | 230 | ```typescript 231 | // 打开 ExactOptionsPropertyTypes 和 strictNullChecks 232 | const obj: { 233 | x: number; 234 | y?: number; 235 | } = { x: 1, y: undefined }; // 报错 236 | ``` 237 | 238 | 上面示例中,打开了这两个设置以后,可选属性就不能设为`undefined`了。 239 | 240 | 注意,可选属性与允许设为`undefined`的必选属性是不等价的。 241 | 242 | ```typescript 243 | type A = { x:number, y?:number }; 244 | type B = { x:number, y:number|undefined }; 245 | 246 | const ObjA:A = { x: 1 }; // 正确 247 | const ObjB:B = { x: 1 }; // 报错 248 | ``` 249 | 250 | 上面示例中,属性`y`如果是一个可选属性,那就可以省略不写;如果是允许设为`undefined`的必选属性,一旦省略就会报错,必须显式写成`{ x: 1, y: undefined }`。 251 | 252 | ## 只读属性 253 | 254 | 属性名前面加上`readonly`关键字,表示这个属性是只读属性,不能修改。 255 | 256 | ```typescript 257 | interface MyInterface { 258 | readonly prop: number; 259 | } 260 | ``` 261 | 262 | 上面示例中,`prop`属性是只读属性,不能修改它的值。 263 | 264 | ```typescript 265 | const person:{ 266 | readonly age: number 267 | } = { age: 20 }; 268 | 269 | person.age = 21; // 报错 270 | ``` 271 | 272 | 上面示例中,最后一行修改了只读属性`age`,就报错了。 273 | 274 | 只读属性只能在对象初始化期间赋值,此后就不能修改该属性。 275 | 276 | ```typescript 277 | type Point = { 278 | readonly x: number; 279 | readonly y: number; 280 | }; 281 | 282 | const p:Point = { x: 0, y: 0 }; 283 | 284 | p.x = 100; // 报错 285 | ``` 286 | 287 | 上面示例中,类型`Point`的属性`x`和`y`都带有修饰符`readonly`,表示这两个属性只能在初始化期间赋值,后面再修改就会报错。 288 | 289 | 注意,如果属性值是一个对象,`readonly`修饰符并不禁止修改该对象的属性,只是禁止完全替换掉该对象。 290 | 291 | ```typescript 292 | interface Home { 293 | readonly resident: { 294 | name: string; 295 | age: number 296 | }; 297 | } 298 | 299 | const h:Home = { 300 | resident: { 301 | name: 'Vicky', 302 | age: 42 303 | } 304 | }; 305 | 306 | h.resident.age = 32; // 正确 307 | h.resident = { 308 | name: 'Kate', 309 | age: 23 310 | } // 报错 311 | ``` 312 | 313 | 上面示例中,`h.resident`是只读属性,它的值是一个对象。修改这个对象的`age`属性是可以的,但是整个替换掉`h.resident`属性会报错。 314 | 315 | 另一个需要注意的地方是,如果一个对象有两个引用,即两个变量对应同一个对象,其中一个变量是可写的,另一个变量是只读的,那么从可写变量修改属性,会影响到只读变量。 316 | 317 | ```typescript 318 | interface Person { 319 | name: string; 320 | age: number; 321 | } 322 | 323 | interface ReadonlyPerson { 324 | readonly name: string; 325 | readonly age: number; 326 | } 327 | 328 | let w:Person = { 329 | name: 'Vicky', 330 | age: 42, 331 | }; 332 | 333 | let r:ReadonlyPerson = w; 334 | 335 | w.age += 1; 336 | r.age // 43 337 | ``` 338 | 339 | 上面示例中,变量`w`和`r`指向同一个对象,其中`w`是可写的,`r`是只读的。那么,对`w`的属性修改,会影响到`r`。 340 | 341 | 如果希望属性值是只读的,除了声明时加上`readonly`关键字,还有一种方法,就是在赋值时,在对象后面加上只读断言`as const`。 342 | 343 | ```typescript 344 | const myUser = { 345 | name: "Sabrina", 346 | } as const; 347 | 348 | myUser.name = "Cynthia"; // 报错 349 | ``` 350 | 351 | 上面示例中,对象后面加了只读断言`as const`,就变成只读对象了,不能修改属性了。 352 | 353 | 注意,上面的`as const`属于 TypeScript 的类型推断,如果变量明确地声明了类型,那么 TypeScript 会以声明的类型为准。 354 | 355 | ```typescript 356 | const myUser:{ name: string } = { 357 | name: "Sabrina", 358 | } as const; 359 | 360 | myUser.name = "Cynthia"; // 正确 361 | ``` 362 | 363 | 上面示例中,根据变量`myUser`的类型声明,`name`不是只读属性,但是赋值时又使用只读断言`as const`。这时会以声明的类型为准,因为`name`属性可以修改。 364 | 365 | ## 属性名的索引类型 366 | 367 | 如果对象的属性非常多,一个个声明类型就很麻烦,而且有些时候,无法事前知道对象会有多少属性,比如外部 API 返回的对象。这时 TypeScript 允许采用属性名表达式的写法来描述类型,称为“属性名的索引类型”。 368 | 369 | 索引类型里面,最常见的就是属性名的字符串索引。 370 | 371 | ```typescript 372 | type MyObj = { 373 | [property: string]: string 374 | }; 375 | 376 | const obj:MyObj = { 377 | foo: 'a', 378 | bar: 'b', 379 | baz: 'c', 380 | }; 381 | ``` 382 | 383 | 上面示例中,类型`MyObj`的属性名类型就采用了表达式形式,写在方括号里面。`[property: string]`的`property`表示属性名,这个是可以随便起的,它的类型是`string`,即属性名类型为`string`。也就是说,不管这个对象有多少属性,只要属性名为字符串,且属性值也是字符串,就符合这个类型声明。 384 | 385 | JavaScript 对象的属性名(即上例的`property`)的类型有三种可能,除了上例的`string`,还有`number`和`symbol`。 386 | 387 | ```typescript 388 | type T1 = { 389 | [property: number]: string 390 | }; 391 | 392 | type T2 = { 393 | [property: symbol]: string 394 | }; 395 | ``` 396 | 397 | 上面示例中,对象属性名的类型分别为`number`和`symbol`。 398 | 399 | ```typescript 400 | type MyArr = { 401 | [n:number]: number; 402 | }; 403 | 404 | const arr:MyArr = [1, 2, 3]; 405 | // 或者 406 | const arr:MyArr = { 407 | 0: 1, 408 | 1: 2, 409 | 2: 3, 410 | }; 411 | ``` 412 | 413 | 上面示例中,对象类型`MyArr`的属性名是`[n:number]`,就表示它的属性名都是数值,比如`0`、`1`、`2`。 414 | 415 | 对象可以同时有多种类型的属性名索引,比如同时有数值索引和字符串索引。但是,数值索引不能与字符串索引发生冲突,必须服从后者,这是因为在 JavaScript 语言内部,所有的数值属性名都会自动转为字符串属性名。 416 | 417 | ```typescript 418 | type MyType = { 419 | [x: number]: boolean; // 报错 420 | [x: string]: string; 421 | } 422 | ``` 423 | 424 | 上面示例中,类型`MyType`同时有两种属性名索引,但是数值索引与字符串索引冲突了,所以报错了。由于字符属性名的值类型是`string`,数值属性名的值类型只有同样为`string`,才不会报错。 425 | 426 | 同样地,可以既声明属性名索引,也声明具体的单个属性名。如果单个属性名不符合属性名索引的范围,两者发生冲突,就会报错。 427 | 428 | ```typescript 429 | type MyType = { 430 | foo: boolean; // 报错 431 | [x: string]: string; 432 | } 433 | ``` 434 | 435 | 上面示例中,属性名`foo`符合属性名的字符串索引,但是两者的属性值类型不一样,所以报错了。 436 | 437 | 属性的索引类型写法,建议谨慎使用,因为属性名的声明太宽泛,约束太少。另外,属性名的数值索引不宜用来声明数组,因为采用这种方式声明数组,就不能使用各种数组方法以及`length`属性,因为类型里面没有定义这些东西。 438 | 439 | ```typescript 440 | type MyArr = { 441 | [n:number]: number; 442 | }; 443 | 444 | const arr:MyArr = [1, 2, 3]; 445 | arr.length // 报错 446 | ``` 447 | 448 | 上面示例中,读取`arr.length`属性会报错,因为类型`MyArr`没有这个属性。 449 | 450 | ## 解构赋值 451 | 452 | 解构赋值用于直接从对象中提取属性。 453 | 454 | ```typescript 455 | const {id, name, price} = product; 456 | ``` 457 | 458 | 上面语句从对象`product`提取了三个属性,并声明属性名的同名变量。 459 | 460 | 解构赋值的类型写法,跟为对象声明类型是一样的。 461 | 462 | ```typescript 463 | const {id, name, price}:{ 464 | id: string; 465 | name: string; 466 | price: number 467 | } = product; 468 | ``` 469 | 470 | 注意,目前没法为解构变量指定类型,因为对象解构里面的冒号,JavaScript 指定了其他用途。 471 | 472 | ```typescript 473 | let { x: foo, y: bar } = obj; 474 | 475 | // 等同于 476 | let foo = obj.x; 477 | let bar = obj.y; 478 | ``` 479 | 480 | 上面示例中,冒号不是表示属性`x`和`y`的类型,而是为这两个属性指定新的变量名。如果要为`x`和`y`指定类型,不得不写成下面这样。 481 | 482 | ```typescript 483 | let { x: foo, y: bar } 484 | : { x: string; y: number } = obj; 485 | ``` 486 | 487 | 这一点要特别小心,TypeScript 里面很容易搞糊涂。 488 | 489 | ```typescript 490 | function draw({ 491 | shape: Shape, 492 | xPos: number = 100 493 | }) { 494 | let myShape = shape; // 报错 495 | let x = xPos; // 报错 496 | } 497 | ``` 498 | 499 | 上面示例中,函数`draw()`的参数是一个对象解构,里面的冒号很像是为变量指定类型,其实是为对应的属性指定新的变量名。所以,TypeScript 就会解读成,函数体内不存在变量`shape`,而是属性`shape`的值被赋值给了变量`Shape`。 500 | 501 | ## 结构类型原则 502 | 503 | 只要对象 B 满足 对象 A 的结构特征,TypeScript 就认为对象 B 兼容对象 A 的类型,这称为“结构类型”原则(structural typing)。 504 | 505 | ```typescript 506 | type A = { 507 | x: number; 508 | }; 509 | 510 | type B = { 511 | x: number; 512 | y: number; 513 | }; 514 | ``` 515 | 516 | 上面示例中,对象`A`只有一个属性`x`,类型为`number`。对象`B`满足这个特征,因此兼容对象`A`,只要可以使用`A`的地方,就可以使用`B`。 517 | 518 | ```typescript 519 | const B = { 520 | x: 1, 521 | y: 1 522 | }; 523 | 524 | const A:{ x: number } = B; // 正确 525 | ``` 526 | 527 | 上面示例中,`A`和`B`并不是同一个类型,但是`B`可以赋值给`A`,因为`B`满足`A`的结构特征。 528 | 529 | 根据“结构类型”原则,TypeScript 检查某个值是否符合指定类型时,并不是检查这个值的类型名(即“名义类型”),而是检查这个值的结构是否符合要求(即“结构类型”)。 530 | 531 | TypeScript 之所以这样设计,是为了符合 JavaScript 的行为。JavaScript 并不关心对象是否严格相似,只要某个对象具有所要求的属性,就可以正确运行。 532 | 533 | 如果类型 B 可以赋值给类型 A,TypeScript 就认为 B 是 A 的子类型(subtyping),A 是 B 的父类型。子类型满足父类型的所有结构特征,同时还具有自己的特征。凡是可以使用父类型的地方,都可以使用子类型,即子类型兼容父类型。 534 | 535 | 这种设计有时会导致令人惊讶的结果。 536 | 537 | ```typescript 538 | type myObj = { 539 | x: number, 540 | y: number, 541 | }; 542 | 543 | function getSum(obj:myObj) { 544 | let sum = 0; 545 | 546 | for (const n of Object.keys(obj)) { 547 | const v = obj[n]; // 报错 548 | sum += Math.abs(v); 549 | } 550 | 551 | return sum; 552 | } 553 | ``` 554 | 555 | 上面示例中,函数`getSum()`要求传入参数的类型是`myObj`,但是实际上所有与`myObj`兼容的对象都可以传入。这会导致`const v = obj[n]`这一行报错,原因是`obj[n]`取出的属性值不一定是数值(`number`),使得变量`v`的类型被推断为`any`。如果项目设置为不允许变量类型推断为`any`,代码就会报错。写成下面这样,就不会报错。 556 | 557 | ```typescript 558 | type MyObj = { 559 | x: number, 560 | y: number, 561 | }; 562 | 563 | function getSum(obj:MyObj) { 564 | return Math.abs(obj.x) + Math.abs(obj.y); 565 | } 566 | ``` 567 | 568 | 上面示例就不会报错,因为函数体内部只使用了属性`x`和`y`,这两个属性有明确的类型声明,保证`obj.x`和`obj.y`肯定是数值。虽然与`MyObj`兼容的任何对象都可以传入函数`getSum()`,但是只要不使用其他属性,就不会有类型报错。 569 | 570 | ## 严格字面量检查 571 | 572 | 如果对象使用字面量表示,会触发 TypeScript 的严格字面量检查(strict object literal checking)。如果字面量的结构跟类型定义的不一样(比如多出了未定义的属性),就会报错。 573 | 574 | ```typescript 575 | const point:{ 576 | x:number; 577 | y:number; 578 | } = { 579 | x: 1, 580 | y: 1, 581 | z: 1 // 报错 582 | }; 583 | ``` 584 | 585 | 上面示例中,等号右边是一个对象的字面量,这时会触发严格字面量检查。只要有类型声明中不存在的属性(本例是`z`),就会导致报错。 586 | 587 | 如果等号右边不是字面量,而是一个变量,根据结构类型原则,是不会报错的。 588 | 589 | ```typescript 590 | const myPoint = { 591 | x: 1, 592 | y: 1, 593 | z: 1 594 | }; 595 | 596 | const point:{ 597 | x:number; 598 | y:number; 599 | } = myPoint; // 正确 600 | ``` 601 | 602 | 上面示例中,等号右边是一个变量,就不会触发严格字面量检查,从而不报错。 603 | 604 | TypeScript 对字面量进行严格检查的目的,主要是防止拼写错误。一般来说,字面量大多数来自手写,容易出现拼写错误,或者误用 API。 605 | 606 | ```typescript 607 | type Options = { 608 | title:string; 609 | darkMode?:boolean; 610 | }; 611 | 612 | const obj:Options = { 613 | title: '我的网页', 614 | darkmode: true, // 报错 615 | }; 616 | ``` 617 | 618 | 上面示例中,属性`darkMode`拼写错了,成了`darkmode`。如果没有严格字面量规则,就不会报错,因为`darkMode`是可选属性,根据结构类型原则,任何对象只要有`title`属性,都认为符合`Options`类型。 619 | 620 | 规避严格字面量检查,可以使用中间变量。 621 | 622 | ```typescript 623 | let myOptions = { 624 | title: '我的网页', 625 | darkmode: true, 626 | }; 627 | 628 | const obj:Options = myOptions; 629 | ``` 630 | 631 | 上面示例中,创建了一个中间变量`myOptions`,就不会触发严格字面量规则,因为这时变量`obj`的赋值,不属于直接字面量赋值。 632 | 633 | 如果你确认字面量没有错误,也可以使用类型断言规避严格字面量检查。 634 | 635 | ```typescript 636 | const obj:Options = { 637 | title: '我的网页', 638 | darkmode: true, 639 | } as Options; 640 | ``` 641 | 642 | 上面示例使用类型断言`as Options`,告诉编译器,字面量符合 Options 类型,就能规避这条规则。 643 | 644 | 如果允许字面量有多余属性,可以像下面这样在类型里面定义一个通用属性。 645 | 646 | ```typescript 647 | let x: { 648 | foo: number, 649 | [x: string]: any 650 | }; 651 | 652 | x = { foo: 1, baz: 2 }; // Ok 653 | ``` 654 | 655 | 上面示例中,变量`x`的类型声明里面,有一个属性的字符串索引(`[x: string]`),导致任何字符串属性名都是合法的。 656 | 657 | 由于严格字面量检查,字面量对象传入函数必须很小心,不能有多余的属性。 658 | 659 | ```typescript 660 | interface Point { 661 | x: number; 662 | y: number; 663 | } 664 | 665 | function computeDistance(point: Point) { /*...*/ } 666 | 667 | computeDistance({ x: 1, y: 2, z: 3 }); // 报错 668 | computeDistance({x: 1, y: 2}); // 正确 669 | ``` 670 | 671 | 上面示例中,对象字面量传入函数`computeDistance()`时,不能有多余的属性,否则就通不过严格字面量检查。 672 | 673 | 编译器选项`suppressExcessPropertyErrors`,可以关闭多余属性检查。下面是它在 tsconfig.json 文件里面的写法。 674 | 675 | ```typescript 676 | { 677 | "compilerOptions": { 678 | "suppressExcessPropertyErrors": true 679 | } 680 | } 681 | ``` 682 | 683 | ## 最小可选属性规则 684 | 685 | 根据“结构类型”原则,如果一个对象的所有属性都是可选的,那么其他对象跟它都是结构类似的。 686 | 687 | ```typescript 688 | type Options = { 689 | a?:number; 690 | b?:number; 691 | c?:number; 692 | }; 693 | ``` 694 | 695 | 上面示例中,类型`Options`的所有属性都是可选的,所以它可以是一个空对象,也就意味着任意对象都满足`Options`的结构。 696 | 697 | 为了避免这种情况,TypeScript 2.4 引入了一个“最小可选属性规则”,也称为[“弱类型检测”](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-2-4.html#weak-type-detection)(weak type detection)。 698 | 699 | ```typescript 700 | type Options = { 701 | a?:number; 702 | b?:number; 703 | c?:number; 704 | }; 705 | 706 | const opts = { d: 123 }; 707 | 708 | const obj:Options = opts; // 报错 709 | ``` 710 | 711 | 上面示例中,对象`opts`与类型`Options`没有共同属性,赋值给该类型的变量就会报错。 712 | 713 | 报错原因是,如果某个类型的所有属性都是可选的,那么该类型的对象必须至少存在一个可选属性,不能所有可选属性都不存在。这就叫做“最小可选属性规则”。 714 | 715 | 如果想规避这条规则,要么在类型里面增加一条索引属性(`[propName: string]: someType`),要么使用类型断言(`opts as Options`)。 716 | 717 | ## 空对象 718 | 719 | 空对象是 TypeScript 的一种特殊值,也是一种特殊类型。 720 | 721 | ```typescript 722 | const obj = {}; 723 | obj.prop = 123; // 报错 724 | ``` 725 | 726 | 上面示例中,变量`obj`的值是一个空对象,然后对`obj.prop`赋值就会报错。 727 | 728 | 原因是这时 TypeScript 会推断变量`obj`的类型为空对象,实际执行的是下面的代码。 729 | 730 | ```typescript 731 | const obj:{} = {}; 732 | ``` 733 | 734 | 空对象没有自定义属性,所以对自定义属性赋值就会报错。空对象只能使用继承的属性,即继承自原型对象`Object.prototype`的属性。 735 | 736 | ```typescript 737 | obj.toString() // 正确 738 | ``` 739 | 740 | 上面示例中,`toString()`方法是一个继承自原型对象的方法,TypeScript 允许在空对象上使用。 741 | 742 | 回到本节开始的例子,这种写法其实在 JavaScript 很常见:先声明一个空对象,然后向空对象添加属性。但是,TypeScript 不允许动态添加属性,所以对象不能分步生成,必须生成时一次性声明所有属性。 743 | 744 | ```typescript 745 | // 错误 746 | const pt = {}; 747 | pt.x = 3; 748 | pt.y = 4; 749 | 750 | // 正确 751 | const pt = { 752 | x: 3, 753 | y: 4 754 | }; 755 | ``` 756 | 757 | 如果确实需要分步声明,一个比较好的方法是,使用扩展运算符(`...`)合成一个新对象。 758 | 759 | ```typescript 760 | const pt0 = {}; 761 | const pt1 = { x: 3 }; 762 | const pt2 = { y: 4 }; 763 | 764 | const pt = { 765 | ...pt0, ...pt1, ...pt2 766 | }; 767 | ``` 768 | 769 | 上面示例中,对象`pt`是三个部分合成的,这样既可以分步声明,也符合 TypeScript 静态声明的要求。 770 | 771 | 空对象作为类型,其实是`Object`类型的简写形式。 772 | 773 | ```typescript 774 | let d:{}; 775 | // 等同于 776 | // let d:Object; 777 | 778 | d = {}; 779 | d = { x: 1 }; 780 | d = 'hello'; 781 | d = 2; 782 | ``` 783 | 784 | 上面示例中,各种类型的值(除了`null`和`undefined`)都可以赋值给空对象类型,跟`Object`类型的行为是一样的。 785 | 786 | 因为`Object`可以接受各种类型的值,而空对象是`Object`类型的简写,所以它不会有严格字面量检查,赋值时总是允许多余的属性,只是不能读取这些属性。 787 | 788 | ```typescript 789 | interface Empty { } 790 | const b:Empty = {myProp: 1, anotherProp: 2}; // 正确 791 | b.myProp // 报错 792 | ``` 793 | 794 | 上面示例中,变量`b`的类型是空对象,视同`Object`类型,不会有严格字面量检查,但是读取多余的属性会报错。 795 | 796 | 如果想强制使用没有任何属性的对象,可以采用下面的写法。 797 | 798 | ```typescript 799 | interface WithoutProperties { 800 | [key: string]: never; 801 | } 802 | 803 | // 报错 804 | const a:WithoutProperties = { prop: 1 }; 805 | ``` 806 | 807 | 上面的示例中,`[key: string]: never`表示字符串属性名是不存在的,因此其他对象进行赋值时就会报错。 808 | 809 | -------------------------------------------------------------------------------- /docs/operator.md: -------------------------------------------------------------------------------- 1 | # TypeScript 类型运算符 2 | 3 | TypeScript 提供强大的类型运算能力,可以使用各种类型运算符,对已有的类型进行计算,得到新类型。 4 | 5 | ## keyof 运算符 6 | 7 | ### 简介 8 | 9 | keyof 是一个单目运算符,接受一个对象类型作为参数,返回该对象的所有键名组成的联合类型。 10 | 11 | ```typescript 12 | type MyObj = { 13 | foo: number, 14 | bar: string, 15 | }; 16 | 17 | type Keys = keyof MyObj; // 'foo'|'bar' 18 | ``` 19 | 20 | 上面示例中,`keyof MyObj`返回`MyObj`的所有键名组成的联合类型,即`'foo'|'bar'`。 21 | 22 | 下面是另一个例子。 23 | 24 | ```typescript 25 | interface T { 26 | 0: boolean; 27 | a: string; 28 | b(): void; 29 | } 30 | 31 | type KeyT = keyof T; // 0 | 'a' | 'b' 32 | ``` 33 | 34 | 由于 JavaScript 对象的键名只有三种类型,所以对于任意对象的键名的联合类型就是`string|number|symbol`。 35 | 36 | ```typescript 37 | // string | number | symbol 38 | type KeyT = keyof any; 39 | ``` 40 | 41 | 对于没有自定义键名的类型使用 keyof 运算符,返回`never`类型,表示不可能有这样类型的键名。 42 | 43 | ```typescript 44 | type KeyT = keyof object; // never 45 | ``` 46 | 47 | 上面示例中,由于`object`类型没有自身的属性,也就没有键名,所以`keyof object`返回`never`类型。 48 | 49 | 由于 keyof 返回的类型是`string|number|symbol`,如果有些场合只需要其中的一种类型,那么可以采用交叉类型的写法。 50 | 51 | ```typescript 52 | type Capital = Capitalize; 53 | 54 | type MyKeys = Capital; // 报错 55 | ``` 56 | 57 | 上面示例中,类型`Capital`只接受字符串作为类型参数,传入`keyof Obj`会报错,原因是这时的类型参数是`string|number|symbol`,跟字符串不兼容。采用下面的交叉类型写法,就不会报错。 58 | 59 | ```typescript 60 | type MyKeys = Capital; 61 | ``` 62 | 63 | 上面示例中,`string & keyof Obj`等同于`string & string|number|symbol`进行交集运算,最后返回`string`,因此`Capital`就不会报错了。 64 | 65 | 如果对象属性名采用索引形式,keyof 会返回属性名的索引类型。 66 | 67 | ```typescript 68 | // 示例一 69 | interface T { 70 | [prop: number]: number; 71 | } 72 | 73 | // number 74 | type KeyT = keyof T; 75 | 76 | // 示例二 77 | interface T { 78 | [prop: string]: number; 79 | } 80 | 81 | // string|number 82 | type KeyT = keyof T; 83 | ``` 84 | 85 | 上面的示例二,`keyof T`返回的类型是`string|number`,原因是 JavaScript 属性名为字符串时,包含了属性名为数值的情况,因为数值属性名会自动转为字符串。 86 | 87 | 如果 keyof 运算符用于数组或元组类型,得到的结果可能出人意料。 88 | 89 | ```typescript 90 | type Result = keyof ['a', 'b', 'c']; 91 | // 返回 number | "0" | "1" | "2" 92 | // | "length" | "pop" | "push" | ··· 93 | ``` 94 | 95 | 上面示例中,keyof 会返回数组的所有键名,包括数字键名和继承的键名。 96 | 97 | 对于联合类型,keyof 返回成员共有的键名。 98 | 99 | ```typescript 100 | type A = { a: string; z: boolean }; 101 | type B = { b: string; z: boolean }; 102 | 103 | // 返回 'z' 104 | type KeyT = keyof (A | B); 105 | ``` 106 | 107 | 对于交叉类型,keyof 返回所有键名。 108 | 109 | ```typescript 110 | type A = { a: string; x: boolean }; 111 | type B = { b: string; y: number }; 112 | 113 | // 返回 'a' | 'x' | 'b' | 'y' 114 | type KeyT = keyof (A & B); 115 | 116 | // 相当于 117 | keyof (A & B) ≡ keyof A | keyof B 118 | ``` 119 | 120 | keyof 取出的是键名组成的联合类型,如果想取出键值组成的联合类型,可以像下面这样写。 121 | 122 | ```typescript 123 | type MyObj = { 124 | foo: number, 125 | bar: string, 126 | }; 127 | 128 | type Keys = keyof MyObj; 129 | 130 | type Values = MyObj[Keys]; // number|string 131 | ``` 132 | 133 | 上面示例中,`Keys`是键名组成的联合类型,而`MyObj[Keys]`会取出每个键名对应的键值类型,组成一个新的联合类型,即`number|string`。 134 | 135 | ### keyof 运算符的用途 136 | 137 | keyof 运算符往往用于精确表达对象的属性类型。 138 | 139 | 举例来说,取出对象的某个指定属性的值,JavaScript 版本可以写成下面这样。 140 | 141 | ```typescript 142 | function prop(obj, key) { 143 | return obj[key]; 144 | } 145 | ``` 146 | 147 | 上面这个函数添加类型,只能写成下面这样。 148 | 149 | ```javascript 150 | function prop( 151 | obj: { [p:string]: any }, 152 | key: string 153 | ):any { 154 | return obj[key]; 155 | } 156 | ``` 157 | 158 | 上面的类型声明有两个问题,一是无法表示参数`key`与参数`obj`之间的关系,二是返回值类型只能写成`any`。 159 | 160 | 有了 keyof 以后,就可以解决这两个问题,精确表达返回值类型。 161 | 162 | ```javascript 163 | function prop( 164 | obj:Obj, key:K 165 | ):Obj[K] { 166 | return obj[key]; 167 | } 168 | ``` 169 | 170 | 上面示例中,`K extends keyof Obj`表示`K`是`Obj`的一个属性名,传入其他字符串会报错。返回值类型`Obj[K]`就表示`K`这个属性值的类型。 171 | 172 | keyof 的另一个用途是用于属性映射,即将一个类型的所有属性逐一映射成其他值。 173 | 174 | ```typescript 175 | type NewProps = { 176 | [Prop in keyof Obj]: boolean; 177 | }; 178 | 179 | // 用法 180 | type MyObj = { foo: number; }; 181 | 182 | // 等于 { foo: boolean; } 183 | type NewObj = NewProps; 184 | ``` 185 | 186 | 上面示例中,类型`NewProps`是类型`Obj`的映射类型,前者继承了后者的所有属性,但是把所有属性值类型都改成了`boolean`。 187 | 188 | 下面的例子是去掉 readonly 修饰符。 189 | 190 | ```typescript 191 | type Mutable = { 192 | -readonly [Prop in keyof Obj]: Obj[Prop]; 193 | }; 194 | 195 | // 用法 196 | type MyObj = { 197 | readonly foo: number; 198 | } 199 | 200 | // 等于 { foo: number; } 201 | type NewObj = Mutable; 202 | ``` 203 | 204 | 上面示例中,`[Prop in keyof Obj]`是`Obj`类型的所有属性名,`-readonly`表示去除这些属性的只读特性。对应地,还有`+readonly`的写法,表示添加只读属性设置。 205 | 206 | 下面的例子是让可选属性变成必有的属性。 207 | 208 | ```typescript 209 | type Concrete = { 210 | [Prop in keyof Obj]-?: Obj[Prop]; 211 | }; 212 | 213 | // 用法 214 | type MyObj = { 215 | foo?: number; 216 | } 217 | 218 | // 等于 { foo: number; } 219 | type NewObj = Concrete; 220 | ``` 221 | 222 | 上面示例中,`[Prop in keyof Obj]`后面的`-?`表示去除可选属性设置。对应地,还有`+?`的写法,表示添加可选属性设置。 223 | 224 | ## in 运算符 225 | 226 | JavaScript 语言中,`in`运算符用来确定对象是否包含某个属性名。 227 | 228 | ```javascript 229 | const obj = { a: 123 }; 230 | 231 | if ('a' in obj) 232 | console.log('found a'); 233 | ``` 234 | 235 | 上面示例中,`in`运算符用来判断对象`obj`是否包含属性`a`。 236 | 237 | `in`运算符的左侧是一个字符串,表示属性名,右侧是一个对象。它的返回值是一个布尔值。 238 | 239 | TypeScript 语言的类型运算中,`in`运算符有不同的用法,用来取出(遍历)联合类型的每一个成员类型。 240 | 241 | ```typescript 242 | type U = 'a'|'b'|'c'; 243 | 244 | type Foo = { 245 | [Prop in U]: number; 246 | }; 247 | // 等同于 248 | type Foo = { 249 | a: number, 250 | b: number, 251 | c: number 252 | }; 253 | ``` 254 | 255 | 上面示例中,`[Prop in U]`表示依次取出联合类型`U`的每一个成员。 256 | 257 | 上一小节的例子也提到,`[Prop in keyof Obj]`表示取出对象`Obj`的每一个键名。 258 | 259 | ## 方括号运算符 260 | 261 | 方括号运算符(`[]`)用于取出对象的键值类型,比如`T[K]`会返回对象`T`的属性`K`的类型。 262 | 263 | ```typescript 264 | type Person = { 265 | age: number; 266 | name: string; 267 | alive: boolean; 268 | }; 269 | 270 | // Age 的类型是 number 271 | type Age = Person['age']; 272 | ``` 273 | 274 | 上面示例中,`Person['age']`返回属性`age`的类型,本例是`number`。 275 | 276 | 方括号的参数如果是联合类型,那么返回的也是联合类型。 277 | 278 | ```typescript 279 | type Person = { 280 | age: number; 281 | name: string; 282 | alive: boolean; 283 | }; 284 | 285 | // number|string 286 | type T = Person['age'|'name']; 287 | 288 | // number|string|boolean 289 | type A = Person[keyof Person]; 290 | ``` 291 | 292 | 上面示例中,方括号里面是属性名的联合类型,所以返回的也是对应的属性值的联合类型。 293 | 294 | 如果访问不存在的属性,会报错。 295 | 296 | ```typescript 297 | type T = Person['notExisted']; // 报错 298 | ``` 299 | 300 | 方括号运算符的参数也可以是属性名的索引类型。 301 | 302 | ```typescript 303 | type Obj = { 304 | [key:string]: number, 305 | }; 306 | 307 | // number 308 | type T = Obj[string]; 309 | ``` 310 | 311 | 上面示例中,`Obj`的属性名是字符串的索引类型,所以可以写成`Obj[string]`,代表所有字符串属性名,返回的就是它们的类型`number`。 312 | 313 | 这个语法对于数组也适用,可以使用`number`作为方括号的参数。 314 | 315 | ```typescript 316 | // MyArray 的类型是 { [key:number]: string } 317 | const MyArray = ['a','b','c']; 318 | 319 | // 等同于 (typeof MyArray)[number] 320 | // 返回 string 321 | type Person = typeof MyArray[number]; 322 | ``` 323 | 324 | 上面示例中,`MyArray`是一个数组,它的类型实际上是属性名的数值索引,而`typeof MyArray[number]`的`typeof`运算优先级高于方括号,所以返回的是所有数值键名的键值类型`string`。 325 | 326 | 注意,方括号里面不能有值的运算。 327 | 328 | ```typescript 329 | // 示例一 330 | const key = 'age'; 331 | type Age = Person[key]; // 报错 332 | 333 | // 示例二 334 | type Age = Person['a' + 'g' + 'e']; // 报错 335 | ``` 336 | 337 | 上面两个示例,方括号里面都涉及值的运算,编译时不会进行这种运算,所以会报错。 338 | 339 | ## extends...?: 条件运算符 340 | 341 | TypeScript 提供类似 JavaScript 的`?:`运算符这样的三元运算符,但多出了一个`extends`关键字。 342 | 343 | 条件运算符`extends...?:`可以根据当前类型是否符合某种条件,返回不同的类型。 344 | 345 | ```typescript 346 | T extends U ? X : Y 347 | ``` 348 | 349 | 上面式子中的`extends`用来判断,类型`T`是否可以赋值给类型`U`,即`T`是否为`U`的子类型,这里的`T`和`U`可以是任意类型。 350 | 351 | 如果`T`能够赋值给类型`U`,表达式的结果为类型`X`,否则结果为类型`Y`。 352 | 353 | ```typescript 354 | // true 355 | type T = 1 extends number ? true : false; 356 | ``` 357 | 358 | 上面示例中,`1`是`number`的子类型,所以返回`true`。 359 | 360 | 下面是另外一个例子。 361 | 362 | ```typescript 363 | interface Animal { 364 | live(): void; 365 | } 366 | interface Dog extends Animal { 367 | woof(): void; 368 | } 369 | 370 | // number 371 | type T1 = Dog extends Animal ? number : string; 372 | 373 | // string 374 | type T2 = RegExp extends Animal ? number : string; 375 | ``` 376 | 377 | 上面示例中,`Dog`是`Animal`的子类型,所以`T1`的类型是`number`。`RegExp`不是`Animal`的子类型,所以`T2`的类型是`string`。 378 | 379 | 一般来说,调换`extends`两侧类型,会返回相反的结果。举例来说,有两个类`Cat`和`Animal`,前者是后者的子类型,那么`Cat extends Animal`就为真,而`Animal extends Cat`就为伪。 380 | 381 | 如果对泛型使用 extends 条件运算,有一个地方需要注意。当泛型的类型参数是一个联合类型时,那么条件运算符会展开这个类型参数,即`T = T | T`,所以 extends 对类型参数的每个部分是分别计算的。 382 | 383 | ```typescript 384 | type Cond = T extends U ? X : Y; 385 | 386 | type MyType = Cond; 387 | // 等同于 Cond | Cond 388 | // 等同于 (A extends U ? X : Y) | (B extends U ? X : Y) 389 | ``` 390 | 391 | 上面示例中,泛型`Cond`的类型参数`A|B`是一个联合类型,进行条件运算时,相当于`A`和`B`分别进行条件运算,返回结果组成一个联合类型。也就是说,如果类型参数是联合类型,条件运算的返回结果依然是一个联合类型。 392 | 393 | 如果不希望联合类型被条件运算符展开,可以把`extends`两侧的操作数都放在方括号里面。 394 | 395 | ```typescript 396 | // 示例一 397 | type ToArray = 398 | Type extends any ? Type[] : never; 399 | 400 | // 返回结果 string[]|number[] 401 | type T = ToArray; 402 | 403 | // 示例二 404 | type ToArray = 405 | [Type] extends [any] ? Type[] : never; 406 | 407 | // 返回结果 (string | number)[] 408 | type T = ToArray; 409 | ``` 410 | 411 | 上面的示例一,泛型`ToArray`的类型参数`string|number`是一个联合类型,所以会被展开,返回的也是联合类型`string[]|number[]`。示例二是`extends`两侧的运算数都放在方括号里面,左侧是`[Type]`,右侧是`[any]`,这时传入的联合类型不会展开,返回的是一个数组`(string|number)[]`。 412 | 413 | 条件运算符还可以嵌套使用。 414 | 415 | ```typescript 416 | type LiteralTypeName = 417 | T extends undefined ? "undefined" : 418 | T extends null ? "null" : 419 | T extends boolean ? "boolean" : 420 | T extends number ? "number" : 421 | T extends bigint ? "bigint" : 422 | T extends string ? "string" : 423 | never; 424 | ``` 425 | 426 | 上面示例是一个多重判断,返回一个字符串的值类型,对应当前类型。下面是它的用法。 427 | 428 | ```typescript 429 | // "bigint" 430 | type Result1 = LiteralTypeName<123n>; 431 | 432 | // "string" | "number" | "boolean" 433 | type Result2 = LiteralTypeName; 434 | ``` 435 | 436 | ## infer 关键字 437 | 438 | `infer`关键字用来定义泛型里面推断出来的类型参数,而不是外部传入的类型参数。 439 | 440 | 它通常跟条件运算符一起使用,用在`extends`关键字后面的父类型之中。 441 | 442 | ```typescript 443 | type Flatten = 444 | Type extends Array ? Item : Type; 445 | ``` 446 | 447 | 上面示例中,`infer Item`表示`Item`这个参数是 TypeScript 自己推断出来的,不用显式传入,而`Flatten`则表示`Type`这个类型参数是外部传入的。`Type extends Array`则表示,如果参数`Type`是一个数组,那么就将该数组的成员类型推断为`Item`,即`Item`是从`Type`推断出来的。 448 | 449 | 一旦使用`Infer Item`定义了`Item`,后面的代码就可以直接调用`Item`了。下面是上例的泛型`Flatten`的用法。 450 | 451 | ```typescript 452 | // string 453 | type Str = Flatten; 454 | 455 | // number 456 | type Num = Flatten; 457 | ``` 458 | 459 | 上面示例中,第一个例子`Flatten`传入的类型参数是`string[]`,可以推断出`Item`的类型是`string`,所以返回的是`string`。第二个例子`Flatten`传入的类型参数是`number`,它不是数组,所以直接返回自身。 460 | 461 | 如果不用`infer`定义类型参数,那么就要传入两个类型参数。 462 | 463 | ```typescript 464 | type Flatten = 465 | Type extends Array ? Item : Type; 466 | ``` 467 | 468 | 上面是不使用`infer`的写法,每次调用`Flatten`的时候,都要传入两个参数,就比较麻烦。 469 | 470 | 下面的例子使用`infer`,推断函数的参数类型和返回值类型。 471 | 472 | ```typescript 473 | type ReturnPromise = 474 | T extends (...args: infer A) => infer R 475 | ? (...args: A) => Promise 476 | : T; 477 | ``` 478 | 479 | 上面示例中,如果`T`是函数,就返回这个函数的 Promise 版本,否则原样返回。`infer A`表示该函数的参数类型为`A`,`infer R`表示该函数的返回值类型为`R`。 480 | 481 | 如果不使用`infer`,就不得不把`ReturnPromise`写成`ReturnPromise`,这样就很麻烦,相当于开发者必须人肉推断编译器可以完成的工作。 482 | 483 | 下面是`infer`提取对象指定属性的例子。 484 | 485 | ```typescript 486 | type MyType = 487 | T extends { 488 | a: infer M, 489 | b: infer N 490 | } ? [M, N] : never; 491 | 492 | // 用法示例 493 | type T = MyType<{ a: string; b: number }>; 494 | // [string, number] 495 | ``` 496 | 497 | 上面示例中,`infer`提取了参数对象的属性`a`和属性`b`的类型。 498 | 499 | 下面是`infer`通过正则匹配提取类型参数的例子。 500 | 501 | ```typescript 502 | type Str = 'foo-bar'; 503 | 504 | type Bar = Str extends `foo-${infer rest}` ? rest : never // 'bar' 505 | ``` 506 | 507 | 上面示例中,`rest`是从模板字符串提取的类型参数。 508 | 509 | ## is 运算符 510 | 511 | 函数返回布尔值的时候,可以使用`is`运算符,限定返回值与参数之间的关系。 512 | 513 | `is`运算符用来描述返回值属于`true`还是`false`。 514 | 515 | ```typescript 516 | function isFish( 517 | pet: Fish|Bird 518 | ):pet is Fish { 519 | return (pet as Fish).swim !== undefined; 520 | } 521 | ``` 522 | 523 | 上面示例中,函数`isFish()`的返回值类型为`pet is Fish`,表示如果参数`pet`类型为`Fish`,则返回`true`,否则返回`false`。 524 | 525 | `is`运算符总是用于描述函数的返回值类型,写法采用`parameterName is Type`的形式,即左侧为当前函数的参数名,右侧为某一种类型。它返回一个布尔值,表示左侧参数是否属于右侧的类型。 526 | 527 | ```typescript 528 | type A = { a: string }; 529 | type B = { b: string }; 530 | 531 | function isTypeA(x: A|B): x is A { 532 | if ('a' in x) return true; 533 | return false; 534 | } 535 | ``` 536 | 537 | 上面示例中,返回值类型`x is A`可以准确描述函数体内部的运算逻辑。 538 | 539 | `is`运算符可以用于类型保护。 540 | 541 | ```typescript 542 | function isCat(a:any): a is Cat { 543 | return a.name === 'kitty'; 544 | } 545 | 546 | let x:Cat|Dog; 547 | 548 | if (isCat(x)) { 549 | x.meow(); // 正确,因为 x 肯定是 Cat 类型 550 | } 551 | ``` 552 | 553 | 上面示例中,函数`isCat()`的返回类型是`a is Cat`,它是一个布尔值。后面的`if`语句就用这个返回值进行判断,从而起到类型保护的作用,确保`x`是 Cat 类型,从而`x.meow()`不会报错(假定`Cat`类型拥有`meow()`方法)。 554 | 555 | `is`运算符还有一种特殊用法,就是用在类(class)的内部,描述类的方法的返回值。 556 | 557 | ```typescript 558 | class Teacher { 559 | isStudent():this is Student { 560 | return false; 561 | } 562 | } 563 | 564 | class Student { 565 | isStudent():this is Student { 566 | return true; 567 | } 568 | } 569 | ``` 570 | 571 | 上面示例中,`isStudent()`方法的返回值类型,取决于该方法内部的`this`是否为`Student`对象。如果是的,就返回布尔值`true`,否则返回`false`。 572 | 573 | 注意,`this is T`这种写法,只能用来描述方法的返回值类型,而不能用来描述属性的类型。 574 | 575 | ## 模板字符串 576 | 577 | TypeScript 允许使用模板字符串,构建类型。 578 | 579 | 模板字符串的最大特点,就是内部可以引用其他类型。 580 | 581 | ```typescript 582 | type World = "world"; 583 | 584 | // "hello world" 585 | type Greeting = `hello ${World}`; 586 | ``` 587 | 588 | 上面示例中,类型`Greeting`是一个模板字符串,里面引用了另一个字符串类型`world`,因此`Greeting`实际上是字符串`hello world`。 589 | 590 | 注意,模板字符串可以引用的类型一共7种,分别是 string、number、bigint、boolean、null、undefined、Enum。引用这7种以外的类型会报错。 591 | 592 | ```typescript 593 | type Num = 123; 594 | type Obj = { n : 123 }; 595 | 596 | type T1 = `${Num} received`; // 正确 597 | type T2 = `${Obj} received`; // 报错 598 | ``` 599 | 600 | 上面示例中,模板字符串引用数值类型的别名`Num`是可以的,但是引用对象类型的别名`Obj`就会报错。 601 | 602 | 模板字符串里面引用的类型,如果是一个联合类型,那么它返回的也是一个联合类型,即模板字符串可以展开联合类型。 603 | 604 | ```typescript 605 | type T = 'A'|'B'; 606 | 607 | // "A_id"|"B_id" 608 | type U = `${T}_id`; 609 | ``` 610 | 611 | 上面示例中,类型`U`是一个模板字符串,里面引用了一个联合类型`T`,导致最后得到的也是一个联合类型。 612 | 613 | 如果模板字符串引用两个联合类型,它会交叉展开这两个类型。 614 | 615 | ```typescript 616 | type T = 'A'|'B'; 617 | 618 | type U = '1'|'2'; 619 | 620 | // 'A1'|'A2'|'B1'|'B2' 621 | type V = `${T}${U}`; 622 | ``` 623 | 624 | 上面示例中,`T`和`U`都是联合类型,各自有两个成员,模板字符串里面引用了这两个类型,最后得到的就是一个4个成员的联合类型。 625 | 626 | ## satisfies 运算符 627 | 628 | `satisfies`运算符用来检测某个值是否符合指定类型。有时候,不方便将某个值指定为某种类型,但是希望这个值符合类型条件,这时候就可以用`satisfies`运算符对其进行检测。[TypeScript 4.9](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator)添加了这个运算符。 629 | 630 | 举例来说,有一个对象的属性名拼写错误。 631 | 632 | ```typescript 633 | const palette = { 634 | red: [255, 0, 0], 635 | green: "#00ff00", 636 | bleu: [0, 0, 255] // 属性名拼写错误 637 | }; 638 | ``` 639 | 640 | 上面示例中,对象`palette`的属性名拼写错了,将`blue`拼成了`bleu`,我们希望通过指定类型,发现这个错误。 641 | 642 | ```typescript 643 | type Colors = "red" | "green" | "blue"; 644 | type RGB = [number, number, number]; 645 | 646 | const palette: Record = { 647 | red: [255, 0, 0], 648 | green: "#00ff00", 649 | bleu: [0, 0, 255] // 报错 650 | }; 651 | ``` 652 | 653 | 上面示例中,变量`palette`的类型被指定为`Record`,这是一个类型工具,用来返回一个对象,详细介绍见《类型工具》一章。简单说,它的第一个类型参数指定对象的属性名,第二个类型参数指定对象的属性值。 654 | 655 | 本例的`Record`,就表示变量`palette`的属性名应该符合类型`Colors`,属性值应该符合类型`string|RGB`,要么是字符串,要么是元组`RGB`。属性名`bleu`不符合类型`Colors`,所以就报错了。 656 | 657 | 这样的写法,虽然可以发现属性名的拼写错误,但是带来了新的问题。 658 | 659 | ```typescript 660 | const greenComponent = palette.green.substring(1, 6); // 报错 661 | ``` 662 | 663 | 上面示例中,`palette.green`属性调用`substring()`方法会报错,原因是这个方法只有字符串才有,而`palette.green`的类型是`string|RGB`,除了字符串,还可能是元组`RGB`,而元组并不存在`substring()`方法,所以报错了。 664 | 665 | 如果要避免报错,要么精确给出变量`palette`每个属性的类型,要么对`palette.green`的值进行类型缩小。两种做法都比较麻烦,也不是很有必要。 666 | 667 | 这时就可以使用`satisfies`运算符,对`palette`进行类型检测,但是不改变 TypeScript 对`palette`的类型推断。 668 | 669 | ```typescript 670 | type Colors = "red" | "green" | "blue"; 671 | type RGB = [number, number, number]; 672 | 673 | const palette = { 674 | red: [255, 0, 0], 675 | green: "#00ff00", 676 | bleu: [0, 0, 255] // 报错 677 | } satisfies Record; 678 | 679 | const greenComponent = palette.green.substring(1); // 不报错 680 | ``` 681 | 682 | 上面示例中,变量`palette`的值后面增加了`satisfies Record`,表示该值必须满足`Record`这个条件,所以能够检测出属性名`bleu`的拼写错误。同时,它不会改变`palette`的类型推断,所以,TypeScript 知道`palette.green`是一个字符串,对其调用`substring()`方法就不会报错。 683 | 684 | `satisfies`也可以检测属性值。 685 | 686 | ```typescript 687 | const palette = { 688 | red: [255, 0, 0], 689 | green: "#00ff00", 690 | blue: [0, 0] // 报错 691 | } satisfies Record; 692 | ``` 693 | 694 | 上面示例中,属性`blue`的值只有两个成员,不符合元组`RGB`必须有三个成员的条件,从而报错了。 695 | 696 | -------------------------------------------------------------------------------- /docs/react.md: -------------------------------------------------------------------------------- 1 | # TypeScript 的 React 支持 2 | 3 | ## JSX 语法 4 | 5 | JSX 是 React 库引入的一种语法,可以在 JavaScript 脚本中直接书写 HTML 风格的标签。 6 | 7 | TypeScript 支持 JSX 语法,但是必须将脚本后缀名改成`.tsx`。 8 | 9 | `.tsx`文件中,类型断言一律使用`as`形式,因为尖括号的写法会与 JSX 冲突。 10 | 11 | ```typescript 12 | // 使用 13 | var x = foo as any; 14 | 15 | // 不使用 16 | var x = foo; 17 | ``` 18 | 19 | 上面示例中,变量`foo`被断言为类型`any`,在`.tsx`文件中只能使用第一种写法,不使用第二种写法。 20 | 21 | ## React 库 22 | 23 | TypeScript 使用 React 库必须引入 React 的类型定义。 24 | 25 | ```typescript 26 | /// 27 | interface Props { 28 | name: string; 29 | } 30 | class MyComponent extends React.Component { 31 | render() { 32 | return {this.props.name}; 33 | } 34 | } 35 | ; // OK 36 | ; // error, `name` is not a number 37 | ``` 38 | 39 | ## 内置元素 40 | 41 | 内置元素使用`JSX.IntrinsicElements`接口。默认情况下,内置元素不进行类型检查。但是,如果给出了接口定义,就会进行类型检查。 42 | 43 | ```typescript 44 | declare namespace JSX { 45 | interface IntrinsicElements { 46 | foo: any; 47 | } 48 | } 49 | ; // ok 50 | ; // error 51 | ``` 52 | 53 | 上面示例中,``不符合接口定义,所以报错。 54 | 55 | 一种解决办法就是,在接口中定义一个通用元素。 56 | 57 | ```typescript 58 | declare namespace JSX { 59 | interface IntrinsicElements { 60 | [elemName: string]: any; 61 | } 62 | } 63 | ``` 64 | 65 | 上面示例中, 元素名可以是任意字符串。 66 | 67 | ## 组件的写法 68 | 69 | ```typescript 70 | interface FooProp { 71 | name: string; 72 | X: number; 73 | Y: number; 74 | } 75 | declare function AnotherComponent(prop: { name: string }); 76 | function ComponentFoo(prop: FooProp) { 77 | return ; 78 | } 79 | const Button = (prop: { value: string }, context: { color: string }) => ( 80 |