前言
俗话说的好:“温故而知新”,以前学习 typescript
只是因为工作需要去了解,然后临时磨刀学了些皮毛,但缺没有对 typescript
有更深入的理解,因此今天趁工作之余,系统了解一下 typescript
的相关知识,为后续的学习做好铺垫。
注意:文章大部分为引用内容,且多为个人认为重要和难以记住的地方
了解 tsconfig 配置
做 ts 项目的时候,经常会发现项目内有个 tsconfig.json 的配置文件,那么这些配置是从哪里来的呢,它具体有那些配置属性呢,这里就一点点列举:
生成配置属性
1 | # 自己进入一个空项目 |
配置属性内容
1 | { |
### 基础语法
数组
数组有两张写法,个人推荐第一种,一目了然。
1 | // 写法一 |
元组
元组多用于记录确定数量和类型的数组。
1 | let x: [string, number]; |
当访问一个越界的元素,会使用联合类型替代:
1 | x[3] = 'world'; // OK, 字符串可以赋值给(string | number)类型 |
枚举
使用枚举可以定义一些有名字的数字常量
1 | enum Color { |
Any
处理不确定的内容:比如没有 ts 声明的第三方库/用户自定义库
1 | let notSure: any = 4; |
注:能不用就尽量不用,因为使用此和不用 ts 没什么区别
Void
表示没有任何类型,常用于无返回值的函数。
1 | function warnUser(): void { |
注:声明一个 void 类型的变量没有什么大用,因为你只能为它赋予
undefined
和null
。
Never
never
类型表示的是那些永不存在的值的类型。一般用于报错函数或者无终止条件的函数。
never
类型是任何类型的子类型,也可以赋值给任何类型;然而,没有类型是 never
的子类型或可以赋值给 never
类型(除了 never
本身之外)。 即使 any
也不可以赋值给 never
。
1 | // 返回 never 的函数必须存在无法达到的终点 |
Object
object
表示非原始类型,也就是除 number
,string
,boolean
,symbol
,null
或 undefined
之外的类型。
类型断言
类型断言有两种形式。 其一是“尖括号”语法:
1 | let someValue: any = 'this is a string'; |
另一个为 as
语法:
1 | let someValue: any = 'this is a string'; |
当在 TypeScript 里使用 JSX 时,只有 as 语法断言是被允许的。
注:类型断言会影响 ts 的类型校验,对于十分确定的情况可以使用断言来减少一些转换问题,但不要滥用。
接口
基本写法
1 | interface LabelledValue { |
注意:可选属性如果出现报红提示时,需要考虑传入的变量类型为 undefined 的情况,因为 undefined 和 null 是所有基础类型的子集
只读属性
一些对象属性只能在对象刚刚创建的时候修改其值。你可以在属性名前用 readonly
来指定只读属性:
1 | interface Point { |
TypeScript 具有 ReadonlyArray<T>
类型,它与 Array<T>
相似,只是把所有可变方法去掉了,因此可以确保数组创建后再也不能被修改。
1 | let a: number[] = [1, 2, 3, 4]; |
⚠️:上面代码的最后一行,可以看到就算把整个 ReadonlyArray 赋值到一个普通数组也是不可以的。 但是你可以用类型断言重写:
1 | a = ro as number[]; |
最简单判断该用
readonly
还是const
的方法是看要把它做为变量使用还是做为一个属性。做为变量使用的话用const
,若做为属性则使用readonly
。
额外的属性检查
对象字面量会被特殊对待而且会经过额外属性检查,当将它们赋值给变量或作为参数传递的时候。如果一个对象字面量存在任何“目标类型”不包含的属性时,你会得到一个错误。
1 | interface SquareConfig { |
绕开这些检查非常简单。 最简便的方法是使用类型断言:
1 | let mySquare = createSquare({ width: 100, opacity: 0.5 } as SquareConfig); |
然而,最佳的方式是能够添加一个字符串索引签名,前提是你能够确定这个对象可能具有某些做为特殊用途使用的额外属性。
1 | interface SquareConfig { |
⚠️ 注意
还有最后一种跳过这些检查的方式,这可能会让你感到惊讶,它就是将这个对象赋值给一个另一个变量:因为 squareOptions 不会经过额外属性检查,所以编译器不会报错。
1 | let squareOptions = { colour: 'red', width: 100 }; |
函数类型
为了使用接口表示函数类型,我们需要给接口定义一个调用签名。 它就像是一个只有参数列表和返回值类型的函数定义。参数列表里的每个参数都需要名字和类型。
1 | interface SearchFunc { |
对于函数类型的类型检查来说,函数的参数名不需要与接口里定义的名字相匹配,函数的参数会逐个进行检查,要求对应位置上的参数类型是兼容的。比如,我们使用下面的代码重写上面的例子:
1 | // 不对应变量名 |
可索引的类型
使用接口的方式来为数组进行类型声明
1 | interface StringArray { |
字符串索引签名能够很好的描述 dictionary 模式,并且它们也会确保所有属性与其返回值类型相匹配。 因为字符串索引声明了 obj.property
和 obj["property"]
两种形式都可以。下面的例子里,name
的类型与字符串索引类型不匹配,所以类型检查器给出一个错误提示:
1 | interface NumberDictionary { |
函数
剩余参数
剩余参数会被当做个数不限的可选参数。 可以一个都没有,同样也可以有任意个。
1 | function buildName(firstName: string, ...restOfName: string[]) { |
声明文件
UMD
UMD 模块是指那些既可以作为模块使用(通过导入)又可以作为全局(在没有模块加载器的环境里)使用的模块。 许多流行的库,比如 Moment.js,就是这样的形式。 比如,在 Node.js 或 RequireJs 里,你可以这样写:
1 | import moment = require('moment'); |
namespace 和 module
namespace
TS 里的 namespace
主要是解决命名冲突的问题,会在全局生成一个对象,定义在 namespace
内部的类都要通过这个对象的属性访问。对于内部模块来说,尽量使用 namespace
替代 module
,可参考官方文档。例如:
1 | namespace Test { |
注意:import xx = require(‘xx’) 为加载模块的写法,不要与取别名的写法混淆。
默认全局环境的 namespace 为 global
module
模块可理解成 Vue 中的单个 vue 文件,它是以功能为单位进行划分的,一个模块负责一个功能。其与 namespace
的最大区别在于:namespace
是跨文件的,module
是以文件为单位的,一个文件对应一个 module
。类比 Java,namespace
就好比 Java 中的包,而 module
则相当于文件。