万万没想到会来到第七篇,第七篇主要对TypeScript的应用作一些说明和示例,算进阶篇。
工具泛型
Key/Keyof
keyof
可以用来取得一个对象接口的所有 key
值.in 则可以遍历枚举类型
1 | interface Foo { |
keyof
产生联合类型, in
则可以遍历枚举类型, 所以他们经常一起使用
keyof配合泛型使用
1 | interface IProps<T> { |
keyof配合typeof使用
1 | const defaultProps = { |
partial
Partial 作用是将传入的属性变为可选项.
首先我们需要理解两个关键字 keyof
和 in
, keyof
可以用来取得一个对象接口的所有 key
值.
1 | type Partial<T> = { [P in keyof T]?: T[P] }; |
required
Required 的作用是将传入的属性变为必选项, 源码如下
1 | type Required<T> = { [P in keyof T]-?: T[P] }; |
readonly(只读)
typescript类型系统允许在一个接口中使用readonly来标记属性,也就是只读的方式,不可预期的改变是很糟糕的。
可以在接口、类中用此方法定义
1 | type Readonly<T> = { readonly [P in keyof T]: T[P] }; |
Mutable
将 T 的所有属性的 readonly 移除,
1 | type Mutable<T> = { |
record
将 K 中所有的属性的值转化为 T 类型
1 | type Record<K extends keyof any, T> = { [P in K]: T }; |
pick
从 T 中取出 一系列 K 的属性
1 | type Pick<T, K extends keyof T> = { [P in K]: T[P] }; |
omit
用之前的 Pick 和 Exclude 进行组合, 实现忽略对象某些属性功能,
1 | type Omit<T, K> = Pick<T, Exclude<keyof T, K>> |
exclude
Exclude 的作用是从 T 中找出 U 中没有的元素, 换种更加贴近语义的说法其实就是从T 中排除 U
1 | type T = Exclude<1 | 2, 1 | 3> // -> 2 |
extract
Extract 的作用是提取出 T 包含在 U 中的元素, 换种更加贴近语义的说法就是从 T 中提取出 U
1 | type Extract<T, U> = T extends U ? T : never; |
NonNullable
排除T为null或者undefined的情况
1 | type T = NonNullable<string | string[] | null | undefined>; //string | string[] |
infer关键字与Returntype
官方类型库中提供了ReturnType可以获取方法的返回类型,实例
1 | type stringPromiseReturnType = ReturnType<typeof stringPromise>; |
Returntype的定义如下
1 | type ReturnType<T extends (...args:any) => any >= T extends(...args:any)=> infer R?R:any; |
利用infer反解promise中的泛型
1 | type PromiseType<T> = (args:any[]) => Promise<T>; |
也可以解析函数入参的类型
1 | type VariadicFn<A extends |
1 | type FunctionReturnType<T> = T extends (...args: any[]) => infer R ? R : T; |
函数重载与方法重载
js 因为是动态类型,本身不需要支持重载,直接对参数进行类型判断即可,但是ts为了保证类型安全,支持了函数签名的类型重载
如在JavaScript中:
1 | function add(x, y) { |
由于 TypeScript 是 JavaScript 的超集,因此以上的代码可以直接在 TypeScript 中使用,但当 TypeScript 编译器开启 noImplicitAny
的配置项时,以上代码会提示以下错误信息:
1 | Parameter 'x' implicitly has an 'any' type. |
该信息告诉我们参数 x 和参数 y 隐式具有 any
类型。为了解决这个问题,我们可以为参数设置一个类型。因为我们希望 add
函数同时支持 string 和 number 类型,因此我们可以定义一个 string | number
联合类型,然后在函数中使用
1 | type Combinable = string | number; |
但是此时如果在结果中使用字符串函数会报错
1 | const result = add('semlinker', ' kakuqo'); |
Combinable
和 number
类型的对象上并不存在 split
属性。这时我们就可以利用 TypeScript 提供的函数重载。
函数重载或方法重载是使用相同名称和不同参数数量或类型创建多个方法的一种能力。
1 | function add(a: number, b: number): number; |
方法重载是指在同一个类中方法同名,参数不同(参数类型不同、参数个数不同或参数个数相同时参数的先后顺序不同),调用时根据实参的形式,选择与它匹配的方法执行操作的一种技术。所以类中成员方法满足重载的条件是:在同一个类中,方法名相同且参数列表不同。
1 | class Calculator { |
当 TypeScript 编译器处理函数重载时,它会查找重载列表,尝试使用第一个重载定义。 如果匹配的话就使用这个。 因此,在定义重载的时候,一定要把最精确的定义放在最前面。另外在 Calculator 类中,add(a: Combinable, b: Combinable){ }
并不是重载列表的一部分,因此对于 add 成员方法来说,我们只定义了四个重载方法。
声明语句与声明文件、声明合并
假如我们想使用第三方库 jQuery,一种常见的方式是在 html 中通过 <script>
标签引入 jQuery,然后就可以使用全局变量 $
或 jQuery
了。
但是在 ts 中,编译器并不知道 $
或 jQuery
是什么东西1:
这时,我们需要使用 declare var
来定义它的类型
通常我们会把声明语句放到一个单独的文件(jQuery.d.ts
)中,这就是声明文件。声明文件必需以 .d.ts
为后缀。一般来说,ts 会解析项目中所有的 *.ts
文件,当然也包含以 .d.ts
结尾的文件。所以当我们将 jQuery.d.ts
放到项目中时,其他所有 *.ts
文件就都可以获得 jQuery
的类型定义了。
假如仍然无法解析,那么可以检查下 tsconfig.json
中的 files
、include
和 exclude
配置,确保其包含了 jQuery.d.ts
文件。
TS可以在编译时自动生成.d.ts文件,只需要在tsconfig.json配置文件中开启即可
1 | { |
一般只有三种情况需要手动定义声明文件:
1.通过script标签引入第三方库
2.使用的第三方npm包没有提供声明文件
3.自己团队内比较优秀的js库或者插件,为了提升开发体验
声明文件只是对类型的定义,不能赋值
声明文件有全局的类型声明和局部的类型声明两种。
.d.ts
里面,没有使用 import
、export
,默认是全局的。全局的类型声明在项目的任何地方都可以直接使用,无需引入。但是要特别注意类型命名冲突。在 .d.ts
文件中,只要有一个类型定义使用了 export
,那这个声明文件就会变成模块化的。想要使用里面的类型定义,需要先通过 import
的方式将其引入才行。
以react的ts声明文件为例
1 | // @types/react/index.d.ts |
导出的都是以一个以原库同名的命名空间。引用库时相当于也把它的类型声明也引进来了,当然在使用的时候,会自动提示
对于没有提供声明文件的npm包,可以创建一个types目录,来管理自己写的声明文件,同时在配置文件tsconfig.json中的paths和baseUrl配置
1 | { |
npm包的声明文件主要有以下几种语法
1 | export const/let |
复用公共的接口/类型
对于那些同一个类型,可能会在项目中的其它地方用到的,复用类型是一个不错的选择
全局的类型:直接放在最外层的 global.d.ts
或者 typing.d.ts
中,不使用 export
导出
模块级的类型。在每个功能模块下,定义一个 index.d.ts
文件。在这个文件中写需要复用的类型定义。再通过 export
的方式将其导出。在需要使用类型的地方,再通过 import
导入使用。
antd
在每个独立的模块文件夹下面多了一个index.d.ts
,见node_modules/antd/lib
下面react-bulma-components 1.1k
在每个独立的模块文件夹下面多了一个index.d.ts
swiper
-27.6k star
,公共的单独放于types
文件夹里面,其它的和文件同级,添加文件名.d.ts
文件
1 | // typing.d.ts 全局的 |
命名空间
在 JavaScript 使用命名空间时, 这有一个常用的、方便的语法:
1 | (function(something) { |
在确保创建的变量不会泄漏至全局命名空间时,这种方式在 JavaScript 中很常见。当基于文件模块使用时,你无须担心这点,但是该模式仍然适用于一组函数的逻辑分组。因此 TypeScript 提供了 namespace
关键字来描述这种分组,
1 | namespace Utility { |
值得注意的一点是,命名空间是支持嵌套的。因此,你可以做一些类似于在 Utility
命名空间下嵌套一个命名空间 Messaging
的事情。
一些特殊用法
typeof与类型别名混用
1 | const defaultProps = { |
promise类型
在异步操作时常常会使用async函数,函数调用时会return一个promise对象,可以使用then方法添加回调函数
1 | interface IResponse<T> { |
动态分配属性
在 JavaScript 中,我们可以很容易地为对象动态分配属性,但是在typescript中直接给对象添加属性会报错,这个时候需要使用一种宽松的属性对象
1 | let developer = {}; |
索引签名
JavaScript 在一个对象类型的索引签名上会隐式调用 toString
方法,而在 TypeScript 中,为防止初学者砸伤自己的脚(我总是看到 stackoverflow 上有很多 JavaScript 使用者都会这样。),它将会抛出一个错误。
1 | const obj = { |
声明索引签名
1 | const foo: { |
当你声明一个索引签名时,所有明确的成员都必须符合索引签名
这可以给你提供安全性,任何以字符串的访问都能得到相同结果。
1 | // ok |
在 JavaScript 社区你将会见到很多滥用索引签名的 API。如 JavaScript 库中使用 CSS 的常见模式
1 | interface NestedCSS { |
可以用索引签名的嵌套避免这种滥用,我们把索引签名分离到自己的属性里,如命名为 nest
(或者 children
、subnodes
等)
1 | interface NestedCSS { |
你需要把属性合并至索引签名,可以使用交叉类型
1 | type FieldState = { |
空值合并运算符
??
非空断言操作符
非空断言操作符会从变量中移除 undefined 和 null,在变量后面添加一个 ! 就会忽略 undefined 和 null
1 | function simpleExample(a: number | undefined) { |
这种操作符在传递可选props、后端加载数据或者ref取dom时会使用比较频繁,因为这三种情况需要等浏览器加载dom或者组件,值可能为空,如果不使用非空断言操作符,这些情况需要手动添加undefined|null类型或者使用if/三目运算符进行判断,比较麻烦
1 | const ScrolledInput = () => { |
代码检查
Es-lint
安装es-lint
1 | npm install --save-dev eslint |
由于 ESLint 默认使用 Espree 进行语法解析,无法识别 TypeScript 的一些语法,故我们需要安装 @typescript-eslint/parser
,替代掉默认的解析器,别忘了同时安装 typescript
:
1 | npm install --save-dev typescript @typescript-eslint/parser |
接下来需要安装对应的插件 @typescript-eslint/eslint-plugin 它作为 eslint 默认规则的补充,提供了一些额外的适用于 ts 语法的规则。
1 | npm install --save-dev @typescript-eslint/eslint-plugin |
创建自己的规则
ESLint 需要一个配置文件来决定对哪些规则进行检查,配置文件的名称一般是 .eslintrc.js
或 .eslintrc.json
。
当运行 ESLint 的时候检查一个文件的时候,它会首先尝试读取该文件的目录下的配置文件,然后再一级一级往上查找,将所找到的配置合并起来,作为当前被检查文件的配置。
1 | module.exports = { |
执行检查
我们的项目源文件一般是放在 src
目录下,所以需要将 package.json
中的 eslint
脚本改为对一个目录进行检查。由于 eslint
默认不会检查 .ts
后缀的文件,所以需要加上参数 --ext .ts
:
1 | { |
此时执行 npm run eslint
即会检查 src
目录下的所有 .ts
后缀的文件。
在 VSCode 中集成 ESLint 检查§
在编辑器中集成 ESLint 检查,可以在开发过程中就发现错误,甚至可以在保存时自动修复错误,极大的增加了开发效率。
要在 VSCode 中集成 ESLint 检查,我们需要先安装 ESLint 插件,点击「扩展」按钮,搜索 ESLint,然后安装即可。
VSCode 中的 ESLint 插件默认是不会检查 .ts
后缀的,需要在「文件 => 首选项 => 设置 => 工作区」中(也可以在项目根目录下创建一个配置文件 .vscode/settings.json
),添加以下配置:
1 | { |
此时打开ts文件,在错误处就会有提示
Prettier
ESLint 包含了一些代码格式的检查,比如空格、分号等。但前端社区中有一个更先进的工具可以用来格式化代码,那就是 Prettier。
Prettier 聚焦于代码的格式化,通过语法分析,重新整理代码的格式,让所有人的代码都保持同样的风格。
安装Prettier
1 | npm install --save-dev prettier |
然后创建一个 prettier.config.js
文件,里面包含 Prettier 的配置项。Prettier 的配置项很少,这里我推荐大家一个配置规则,作为参考:
1 | // prettier.config.js or .prettierrc.js |
Es-lint支持tsx
如果需要同时支持对 tsx 文件的检查,则需要对以上步骤做一些调整:
安装 eslint-plugin-react
1 | npm install --save-dev eslint-plugin-react |
在package.json和vscode的插件中添加配置
1 | { |
1 | { |
style-lint
对Node的支持
想用typescript写nodejs,需要引入第三方声明文件
1 | npm install @type/node --save |
https://ts.xcatliu.com/basics/type-of-function.html
TS包
DefinitelyTyped
ts简单辨析
never与void、any、unknown的区别:
任意未明确声明类型并切无法推导出类型的值都默认为any类型,any是检测弱,兼容性问题解决方案。
当一个函数返回空值时,它的返回值为 void 类型,但是,当一个函数永不返回时(或者总是抛出错误),它的返回值为 never 类型。
void 类型可以被赋值(在 strictNullChecking 为 false 时),但是除了 never 本身以外,其他任何类型不能赋值给 never。
unknown相对于any,任意类型都可以赋值给unknow,但是不可对其进行任何访问操作(仅仅为类型安全,any操作访问也安全)
1 | let val:any |
但是unkown可以通过别的方式来缩小类型
1 | declare const maybe: unknown; |
数字枚举与字符串枚举的区别
我们可以使用字符串枚举或者数值枚举,
1 | enum NoYes { |
type与interface的区别
type与interface都用于描述一个对象或函数, 两者都可以实现继承
interface 可以 extends, 但 type 是不允许 extends 和 implement 的,但是 type 可以通过交叉类型 实现 interface 的 extend 行为,并且两者并不是相互独立的,也就是说 interface 可以 extends type, type 也可以 与 interface 类型交叉 。
1 | //interface使用extends继承,type可以通过交叉类型继承 |
不同点:
interface可以声明合并,type不行
1 | interface User { |
对象、函数两者都适用,type可以声明基本类型别名,联合类型,元组等类型,还可以使用 typeof 获取实例的类型进行赋值,interface不行
1 | // 基本类型别名 |
type 支持计算属性,生成映射类型,;interface 不支持。
type 能使用 in 关键字生成映射类型, 内部使用了 for .. in。 具有三个部分:类型变量 K,它会依次绑定到每个属性。
字符串字面量联合的 Keys,它包含了要迭代的属性名的集合。
属性的结果类型。
1 | type Keys = "firstname" | "surname" |
一般来说,如果不清楚什么时候用interface/type,能用 interface 实现,就用 interface , 如果不能就用 type 。
元组与数组的区别
数组的类型在[]前面, 元组的类型在[]内部。数组的类型规定数组全部的类型,而元组内部的类型是逐个指定的,也就是元组需要规定元素数量
1 | let arr:(number | string)[] = ['s',3,'a']; |
索引签名和工具类型Record的区别
其实Record工具类型的本质就是索引签名,不同之处只是用法,仅仅需要继承就可以了,不需要再写一遍
1 | interface inf{ |
npm run tsc
WebAssembly-AssemblyScript
AssemblyScript定义了一个TypeScript的子集,意在帮助TS背景的同学,通过标准的JavaScript API来完成到wasm的编译,从而消除语言的差异,让程序猿可以快乐的编码。
AssemblyScript项目主要分为三个子项目:
- AssemblyScript:将TypeScript转化为wasm的主程序
- binaryen.js:AssemblyScript主程序转化为wasm的底层实现,依托于binaryen库,是对binaryen的TypeScript封装。
- wast.js:AssemblyScript主程序转化为wasm的底层实现,依托于wast库,是对wast的TypeScript封装。
首先安装assemblyScript
1 | git clone https://github.com/AssemblyScript/assemblyscript.git |
在node的项目中添加wasm命令
1 | "scripts": { |
1 | npm install --save @assemblyscript/loader |
初始化node-modules
1 | npx asinit . |
构建
1 | npm run asbuild |
js调用wasm
对于JavaScript调用wasm,一般采用如下步骤:
- 加载wasm的字节码。
- 将获取到字节码后转换成 ArrayBuffer,只有这种结构才能被正确编译。编译时会对上述ArrayBuffer进行验证。验证通过方可编译。编译后会通过Promise resolve一个 WebAssembly.Module。
- 在获取到 module 后需要通过 WebAssembly.Instance API 去同步的实例化 module。
- 上述第2、3步骤可以用instaniate 异步API等价代替。
- 之后就可以和像使用JavaScript模块一样调用了。
学习资源
typescript手册:https://www.typescriptlang.org/docs/handbook/
深入理解typescript:https://jkchao.github.io/typescript-book-chinese/
typescript入门教程:https://ts.xcatliu.com/basics/type-of-function.html