写点什么

【网易云商】TypeScript 进阶指南,突破基本类型

作者:网易智企
  • 2022 年 5 月 27 日
  • 本文字数:12042 字

    阅读完需:约 40 分钟

【网易云商】TypeScript 进阶指南,突破基本类型

前言


TypeScript 开始在国内流行(17 年-18 年)时,大家对它的使用态度还是有所保留的。TypeScirpt 在当时的存在的一些问题:


  • npm 上第三方库对于 TypeScript 的支持度不足

  • 让 JavaScript 失去了原本动态灵活的脚本语言特点,写起来可能没那么爽了

  • 代码中的类型声明变相的增加了开发者的负担

  • 工程环境对 TS 的支持存在差异,例如在 vue 的早期版本想要用 TypeScript 开发,环境非常难配置

  • 存在一定的学习成本


笔者个人的观点:


  • 支持在组件或者第三方工具包开发的时候使用 TypeScript

  • 在业务项目中保留观望的态度


随着社区及生态的快速发展,我们可以发现,大量的第三方库基本都是使用 TypeScript 开发。同时 TypeScript 开发的三方库让使用者感受到安全感。


综合考虑以下几点:

  • 配合编程工具 vscode 等,智能化提示,提高开发效率

  • 社区活跃,第三方库 TypeScript 的普及度提高

  • 静态类型,在编译期发现隐藏问题,提高代码质量

  • 提高代码可维护性,特别是中大型项目


网易云商团队在 2020 年开始将 TypeScript 从原先只在库开发中使用,扩展到了业务中。然而在迁移过程中发现,大部分同学依然停留来基本类型的声明,对于碰到一些不会的类型或异常提示,通常采用 any 大法。但是 TypeScirpt 的落地不仅仅是 .js => .ts,因此希望通过本文,大家能对 TypeScript 有更加深入的了解。


在我初次使用 TypeScirpt 的时候,看到 .d.ts 文件,是比较茫然的,以 React 项目为例,当我通过开发工具点击到类型声明文件时,映入眼帘是这样子的:



各种语法组合,如果没有熟读文档及理解,很难明白它们所描述的类型。本文也意在让我们看到这些类型,能够很快清楚它所要描述的具体内容,并且能够举一反三,自己去扩展一些高级类型应用到日常开发中。


文章主要分为以下几个部分来讲解如何进阶 TypeScript

  • 基础介绍

  • 泛型介绍

  • 操作符介绍

  • 内置工具类型实现原理

  • 自定义工具类型

  • 业务实践


基础部分


对于基础的类型,大家可以阅读官方文档手册。当然如果你对于 TypeScript 相当熟悉,不妨试试 ts 版本 LeetCode。


TypeScript 类型体操姿势合集:

https://github.com/type-challenges/type-challenges


泛型 generic


描述定义:泛型程序设计(generic programming)是程序设计语言的一种风格或范式。泛型允许程序员在强类型程序设计语言中编写代码时使用一些以后才指定的类型,在实例化时作为参数指明这些类型。通俗点的理解,泛型好比是用来产生类型的函数,目的就是复用,如果你玩的转泛型,进阶 TypeScript 就是水到渠成的事情啦。


举例:实现一个返回参数值的函数,如果不用泛型实现,我们可能会这么做:


function identity(arg: string): string;function identity(arg: number): number;function identity(arg: number | string): number | string {    return arg;}
identity('Jack'); // okidentity(10000); // okidentity(true); // error
复制代码


显然我们不可能去穷举所有参数的类型,最终会变成 any 大法,然而这也直接失去了类型语言的优势,函数的参数类型及返回类型都变成了宽泛的 any 类型。


function identity(arg: any): any {    return arg;}
复制代码


幸运的是,在 TypeScript 中,泛型帮我们解决了这种窘境。


泛型函数


上例中,提到的函数我们可以通过泛型实现,代码如下:


function identity<T>(arg: T): T {    return arg;}
const getString = identity<string>('Jack')const getNumber = identity<number>(5)
复制代码


泛型接口


interface VO<T> {  value: T;}
const numberVo: VO<number> = { value: 123 }const stringVo: VO<string> = { value: 'Jack' }
复制代码


泛型类


class ValueContoller<T> {  value: T;
constructor(val: T) { this.value = val } getValue(): T { return this.value }}
const numberContoller = new ValueContoller<number>(20);const numberVal = numberContoller.getValue(); // numberVal: number
const stringContoller = new ValueContoller<string>('Jack');const stringVal = stringContoller.getValue(); // numberVal: string
复制代码


操作符及映射类型


typeof 操作符 


用于在类型上下文中获取 变量 或者 属性 的类型。


interface Person {  name: string;  age: number;}const sem: Person = { name: "semlinker", age: 30 };type Sem = typeof sem; // type Sem = Persontype SemName = typeof sem.name; // type SemName = stringconst plusNum = (base: number): number => {  return base + 1;}
type PlusNum = typeof plusNum; // type PlusNum = (base: number) => number
复制代码


注意:此处是 TypeScript 中的 typeof,与上面类型守卫中的 typeof 是有区别的,上面类型守卫中的 typeof 实际上是 JavaScript 中的 typeof,配合逻辑条件语句块:if、else、else if,会被 TypeScript 识别为类型保护推断出合适的类型。


keyof 操作符 


可以用于获取某种类型的所有键,其返回类型是联合类型。


interface Person {  name: string;  age: number;}
type K1 = keyof Person; // "name" | "age"type K2 = keyof Person[]; // "length" | "toString" | "pop" | "push" | "concat" | "join" type K3 = keyof { [x: string]: Person }; // string | number
复制代码


在 TypeScript 中支持两种索引签名,数字索引和字符串索引。


interface StringObject {  // 字符串索引 -> keyof StringObject => string | number  [index: string]: string; }
interface NumberObject { // 数字索引 -> keyof NumberObject => number [index: number]: string;}
复制代码


为了同时支持两种索引类型,就得要求数字索引的返回值必须是字符串索引返回值的子类。其中的原因就是当使用数值索引时,JavaScript 在执行索引操作时,会先把数值索引先转换为字符串索引。


所以 keyof { [x: string]: Person } 的结果会返回 string | number


in 操作符


in 用来遍历联合类型。


type Keys = "a" | "b" | "c"
// -> { a: string, b: string, c: string }type Obj = { [P in Keys]: string}
复制代码


[P in Keys]: string:遍历联合类型 Keys,并定义变量 P 来接收,其每次遍历返回的类型值为 string


extends 操作符


  • 用作类型继承


interface Animal {    name: string;}
interface Dog extends Animal { breed: string;}
复制代码


  • 用作类型约束


// 将getValue的参数限制为string,numberfunction getValue<T extends (string | number)>(value: T) {  return value}
getValue('Jack'); // okgetValue(123); // okgetValue(true); // error
复制代码


  • 用作条件类型,条件类型表达式,类似于三元运算符。


// 如果T包含的类型是U包含的类型的 ‘子集’,那么取类型X,否则取类型YT extends U ? X : Y
复制代码


infer 操作符 


必须和 extends 结合使用,用来声明一个待推断的类型变量。


举例:获取函数的返回值类型


type MyReturnType<T> = T extends (...args: any[]) => infer R ? R : T;
type T0 = MyReturnType<() => string>; // stringtype T1 = MyReturnType<() => void>; // void
复制代码



类型拆解:

  • (...args: any[]) => infer R,声明一个函数类型,通过变量 R 表示待推断的函数返回类型

  • T extends (...args: any[]) => infer R 这里的  extends 用来约束泛型 T 必须满足 (...args: any[]) => infer R

  • ? R : any 如果条件满足,则从类型 T 中推断函数返回类型,并赋值给 R 并返回,否则返回 any


其他示例:


// 推断数组类型type GetItemType<T> = T extends (infer R)[] ? R : T;type T0 = GetItemType<string[]>; // string
// 推断Promise返回值类型type GetPromiseType<T> = T extends Promise<infer U> ? U : T;type T1 = GetPromiseType<Promise<number>>; // number
复制代码


需要特别注意两点:

  • 仅在 extends 作为条件类型时,紧跟着 extends 后使用

  • infer 声明的待推断类型变量 T 只能在 true 分支中使用


 T[K] 索引访问类型 


我们可以使用索引访问类型来查询另一个类型上的特定属性。


interface Person {   age: number;  name: string;  sex: string;};
// stringtype T1 = Person["age"];
// string | numbertype T2 = Person["age" | "name"];
type PersonList = Person[]; // Persontype T3 = PersonList[number];
// numbertype T4 =PersonList[number]["age"];
复制代码


映射类型


TypeScript 映射类型是将一种类型转换成另外一种类型,并减少一些不必要的重复工作。


在数学领域中,我们经常通过函数实现集合之间的数据映射转换,如下图:



在 TypeScirpt 中,我们则通过 映射类型 来实现类型之间的转换。


举例:在部分业务场景中,我们可能需要把一个类型的属性变成可选的。


interface InnerConfig {  type: string;  size: string;  color: string;}
复制代码


如果不使用映射类型,我们需要重新声明一个类型,并把类型属性改成可选的,类型比较复杂的时,这种手工转换的方式变得费时费力。


interface OutConfig {  type?: string;  size?: string;  color?: string;}
复制代码


当然 TypeScript 为我们提供了内置映射类型,Partial 来实现类型转换,方法如下:


type OutConfig = Partial<InnerConfig>
复制代码


我们看到使用映射类型极大的简化了我们的工作,就像调用一个普通函数一样,实现了类型的转换。



那么 Partial 是怎么实现的呢?


type Partial<T> = {  [P in keyof T]?: T[P];};
复制代码


我们拆解一下这个映射类型的实现:

  • type Partial:定义一个类型别名 Partial 和 泛型 T

  • keyof T:获取泛型 T 中的所有 key 组成的联合类型

  • in:遍历泛型 T 的所有属性名

  • T[P]:索引访问类型,获取泛型 T 上,属性名为 P 的类型

  • ?:映射修饰符,将类型值设置为可选的


内置类型 Partial 的原理讲述完毕。


小结:通过泛型 + 操作符基本上能实现大部分高级类型,所以想要要进阶 TypeScript,那么必须熟悉这两部分知识。


内置工具类型实现原理


通过上文三、四章节介绍,我们对 TypeScirpt 类型系统中的 泛型 操作符 有了一定的认识了,那么接下来通过一些内置工具类型,加深这两部分知识的理解。


Partial


构建一个类型,将类型的所有属性设置为可选,参考上文实现。


Pick<Type, Keys>


通过从 Type 中选取属性集合 Keys 来构造一个类型。


interface Todo {  title: string;  description: string;  completed: boolean;}
// type TodoPreview = { title: string; completed: boolean; }type TodoPreview = Pick<Todo, "title" | "completed">;// 实现原理type Pick<T, K extends keyof T> = { [P in K]: T[P];};
复制代码


类型拆解:

  • 定义一个类型别名 Pick,并定义泛型 T 和 K

  • K extends keyof T 约束泛型 K 必须满足由泛型 T 的属性名组成的联合类型

  • 通过 in 操作符遍历泛型 K,通过 P 存储遍历的属性名称,并将返回值设置为 T[P],即类型 T 的属性 P 的类型值

  • 以上就是内置类型 Pick 的实现原理


用伪代码表示:


type TodoPreview = Pick<Todo, "title" | "completed">;
// 等价于type TodoPreview = { [P in "title" | "completed"]: Todo[P];};
// 第一次遍历type TodoPreview = { "title": Todo["title"];};
// 第二次遍历type TodoPreview = { "title": Todo["title"]; "completed": Todo["completed"];};
// 最终结果type TodoPreview = { "title": string; "completed": boolean;}
复制代码


Omit<Type, Keys>


通过从 Type 中选取所有属性,然后删除 Keys 来构造一个类型。


interface Todo {  title: string;  description: string;  completed: boolean;}
// type TodoPreview = { description: string; }type TodoPreview = Omit<Todo, "title" | "completed">;// 实现原理type Exclude<T, U> = T extends U ? never : T;type Omit<T, K extends keyof any> = Pick<T, Exclude<keyof T, K>>;
复制代码


类型拆解

  • 要实现 Omit,其实就相当于 Omit = Pick<T, 需要的联合类型> 需要的联合类型>

  • 需要的联合类型 = 从 T 的属性联合类型中,排除联合类型 K 中存在的类型

  • 也就是 Exclude<keyof T, K>

  • 这里需要注意 Exclude 的实现,Exclude 两个泛型在此处都表示联合类型,当联合类型碰到 in 、extends 等操作符时,会有迭代效果,我们通过下面的过程表示:


type Exclude<T, U> = T extends U ? never : T;
type NameKey = Exclude<'name' | 'sex' | 'age', 'name' | 'sex'>;
// 等价于type NameKey = ('name' extends 'name' | 'sex' ? never : 'name')|('sex' extends 'name' | 'sex' ? never : 'sex')| ('age' extends 'name' | 'sex' ? never : 'age')
// 等价于type NameKey = never | never | 'age'
// 等价于type NameKey = 'age'
复制代码


点击查看其他官方实用程序类型 


自定义工具类型


在明白了一些内置工具类型实现之后,我们尝试实现一些自定义高级类型

PartOptional


指定类型中部分属性变成可选属性.


业务场景:在日常开发中,我们经常需要封装一些三方组件,并默认设置一些参数,但是这些参数在原组件的参数类型中是必选的,举例如下:


type SelectProps = {  value: string;  options: string[];}const Select: React.FC<SelectProps> = (props) => { // ...}
type SelectProps = PartOptional<SelectProps, 'options'>const SelectProp: React.FC<SelectProps> = (props) => { const [ options = ['选项A', '选项B'] ] = props; // ...}
复制代码


上述事例中,我们有一个组件 Select,其参数 value 和 options 是必选的,基于一些业务需求,我们需要扩展一些 Select 的功能,同时扩展组件中内置选项参数,所以我们新组件中需要将原组件参数中的 options 变成可选的,否则会出现 TypeScript 异常提示。


接下来我们看下 PartOptional 实现逻辑:


type PartOptional<T, K extends keyof T> = {  [P in keyof Omit<T, K>]: T[P];} & {  [P in K]?: T[P];};
interface Person { name: string; age: number; sex: string;};
type PersonSimple = PartOptional<Person, 'sex' | 'age'>;
复制代码


  • PartOptional 泛型有两个类型变量 T 和 K 分别代表的是输入类型和需要转成可选的属性名称的联合类型

  • 首先我们通过 keyof T 获取到了类型 T 上所有属性名称组成的联合类型

  • 然后我们通过 K extends XX 约束了 K 中的属性必须是类型 T 上的属性

  • 非指定部分,保持原样子,这里我们通过 Omit<T, K> 获取剩余的类型属性,之后通过映射类型遍历实现

  • 指定可选部分,我们通过映射类型和映射操作符 [P in K]?: T[P] 将指定的属性变成可选的

-[P in K] 通过 in 操作符遍历联合类型 K 并将遍历值存储于变量 P

? 映射修饰符,设置属性为可选

-T[P] 设置类型为目标类型


  • 至此映射类型 PartOptional 实现了

PickOptional


选取类型中可选的类型,并生成新的类型。


业务场景:在组件封装过程中,我们经常会做上层参数的转换,例如原组件的参数。


type Config = {  value: string;  onChange: () => void;  sex?: string;  likes?: string[];}
复制代码


高阶组件中,我们将原组件的可选参数作为一个单独的配置开放。


// { sex: string; likes: string[] }type OptionConfig = PickOptional<Config>;
复制代码


实现原理


type OptionalKeys<T> = {  [P in keyof T]: {} extends Pick<T, P> ? P : never}[keyof T];
type PickOptional<T> = { [P in keyof Pick<T, OptionalKeys<T>>]-?: T[P]}
复制代码


类型拆解:

  • 首先我们定义一个映射类型 OptionalKeys 用来获取类型 T 中可选类型的属性组成的联合类型

  • 我们通过 {} extends Pick<T, P> 来判断类型属性是否是可选的,看以下示例:


// T0 = falsetype T0 = {} extends { sex: string } ? true : false;
// T1 = truetype T1 = {} extends { sex?: string } ? true : false;
复制代码


  • 如果属性是可选的,那么将属性类型设置为属性名称 P 否则设置成 never

  • 之后利用 {xxx}[keyof T] 获取类型可选属性名称组成的联合类型


type T0 = 'sex' | never;
// 等价于,never并不会统计到联合类型中type T0 = 'sex';
复制代码


  • 定义映射类型 PickOptional

  • [P in keyof Pick<T, OptionalKeys<T>>]-?: T[P]

  • Pick<T, OptionalKeys<T>> 获取类型可选属性组成的新类型

  • 通过 keyof 获取所有可选属性,注意此处我们使用的是 keyof Pick<T, OptionalKeys<T>>,而没有直接使用 OptionalKeys<T>


// 经过测试-?操作符将失效type PickOptional<T> = {  [P in OptionalKeys<T>]-?: T[P]}
复制代码


  • -? 通过此映射操作符,去掉属性可选

  • T[P] 设置属性类型


最后我们的 PickOptional 类型属性就实现了。


业务实践

工程环境配置 


早期版本:

react + webpack + babel-loader + ts-loader


当前版本:

react + webpack + babel7 + babel-loader + @babel/preset-react + @babel/preset-typescript


为什么使用 babel 来编译 ts 代码?

  • babel 丰富的生态圈

  • 只需要管理一个编译器

  • 更快的编译速度

  • 剥离类型检查,不会中断编译,提高开发效率


以下是我们工程代码中有关 babel 的部分配置,注意通过 babel 配置后,tsconfig.json 不在参与代码的编译工作,仅作为类型检查配置及开发工具如 vscode 等使用。



const babelOpts = { presets: [ [ require.resolve('@babel/preset-env'), { useBuiltIns: 'usage', corejs: 3, modules: false, targets } ], require.resolve('@babel/preset-react'), require.resolve('@babel/preset-typescript'), ...(buildConfig.extraBabelPresets || []) ], plugins: [ ...(buildConfig.extraBabelPlugins || []) ].filter(Boolean)};
复制代码


问题 1:使用 babel 来编译 TypeScript 如何进行类型检查?


由于通过 babel 编译 TypeScript,并不会进行类型检查,所以我们需要单独配置命令来实现,在 package.json 中添加:


{  "scripts": {    "check-types": "tsc",    "check-types:watch": "tsc --watch"  }}
复制代码


在根目录添加 tsconfig.json 供 tsc 命令使用:


{  "compilerOptions": {    // Target latest version of ECMAScript.    "target": "esnext",    // Search under node_modules for non-relative imports.    "moduleResolution": "node",    // Process & infer types from .js files.    "allowJs": true,    // Don't emit; allow Babel to transform files.    "noEmit": true,    // Enable strictest settings like strictNullChecks & noImplicitAny.    "strict": true,    // Disallow features that require cross-file information for emit.    "isolatedModules": true,    // Import non-ES modules as default imports.    "esModuleInterop": true  },  "include": [    // Your source code dir    "src"  ]}
复制代码


现在可以通过命令启动项目类型监听功能:


npm run check-types
# 监听模式npm run check-types:watch
复制代码


当然你也可以通过 husky 工具实现在代码提交前进行类型检查,配置如下:.husky/pre-commit


#!/bin/sh. "$(dirname "$0")/_/husky.sh"
npm run check-types
复制代码


问题 2:开发工具如 vscode 无法解析 webpack 里面配置的 alias 别名路径


  • 方案一:手动配置 tsconfig.json

  • 方案二:使用 webpack 插件 tsconfig-paths-webpack-plugin

点击链接查看),将 tsconfig.json 中的 path 同步到 webpack alias 中

  • 方案三:使用 webpack 插件 alias-jsconfig-webpack-plugin

点击链接查看),将 webpack alias 同步到 tsconfig.json 的 path 中


问题 3:为什么自定义的 .d.ts 文件没有生效?


例如我们通过类型文件 types.d.ts 声明图片相关的模块。


declare module '*.svg';declare module '*.png';declare module '*.jpg';declare module '*.jpeg';declare module '*.gif';declare module '*.bmp';declare module '*.tiff';
复制代码


然后在 tsconfig.json 中 include 这个文件


{  "include": ["types.d.ts"]}
复制代码


然而这样配置会出现 App.tsx



由于配置了 include 属性,原来默认的 TypeScript 编译范围被覆盖,上述配置表示告诉 Ts 我只要编译 types.d.ts 一个文件,所以我们还需要把的代码目录也配置进来,才能生效。


{  "include": ["src/**/*", "types.d.ts"]}
复制代码



此时,编译工具已经能够正确识别到模块类型了。


接口请求改造 


在业务迁移到 TypeScript 时,接口数据类型补全占了相当一部分工作,以往没有使用 TypeScript 时,基本大家都是对着接口文档直接撸,相对正规一点的可能还会写个 jsdoc 来规范一下业务代码,例如:


/** * 查询自定义对象关联tab数量 * @param {object} data 参数 * @param {number} data.label 对象label * @param {number} data.id 对象id * @returns 数量 */ export function crmObjectNum(data) {  return request({    url: `crmObject/num`,    method: 'POST',    data: data,  })}
复制代码


然而当你使用 TypeScript 开发项目时,这将是一个非常痛苦的过程,例如:

  1. 项目接口数量众多,补齐非常耗时

  2. 接口类型之间有很多可复用的类型,靠人工去维护,费时费力

  3. 后端接口更新后,需要手动找到对应的接口补齐类型参数

  4. 接口类型不预先补全,直接影响业务代码开发,直接表现:不是提示 any,就是报异常


针对前 3 个问题,我们开发类型生成工具 pp-type 来实现 Nei(内部接口文档)类型自动同步。


pp-type 设计流程



其实现原理是通过 json-schema json-schema-to-typescript 来完成对接口文档的类型转换,并生成 .d.ts 声明文件到项目中。


json-schema:它是用来声明和验证 JSON 数据的。

json-schema-to-typescript:负责把 json-schema 转成 TypeScirpt 类型。


举个例子:通过 json-schema 描述了一个 Person 类型


{  "title": "Person",  "type": "object",  "properties": {    "name": {      "type": "string",      "description": "姓名",    },    "age": {      "description": "年龄",      "type": "integer",    },    "hairColor": {      "description": "头发颜色",      "enum": ["black", "brown", "blue"],      "type": "string"    },    "books": {      "description": "书本-数组类型",      "type": "array",      "items": { type: 'string' }    },    "games": {      "description": "游戏-自定义类型",      "type": "array",      "items": { tsType: 'Game' }    },  },  "additionalProperties": false,  "required": ["name"]}
复制代码


通过 json-schema-to-typescript 转换我们可以得到:


/* tslint:disable *//*** This file was automatically generated by json-schema-to-typescript.* DO NOT MODIFY IT BY HAND. Instead, modify the source JSONSchema file,* and run json-schema-to-typescript to regenerate this file.*/
export interface Person { /** * 姓名 */ name: string; /** * 年龄 */ age?: number; /** * 头发颜色 */ hairColor?: "black" | "brown" | "blue"; /** * 书本-数组类型 */ books?: string[]; /** * 游戏-自定义类型 */ games?: Game[];}
复制代码


业务中使用场景:



运行 pp type update 命令后会生成两个类型文件



  • nei_model.d.ts 模型文件

  • interface_model.d.ts 参数类型文件


nei_model.d.ts 用来存放基础模型


/* tslint:disable *//* eslint-disable */
/** 该文件由 工具 自动生成,请勿直接修改!!!*/
/** * @nei地址 https://xxxx.com/xxxx * @负责人 网易智企 * @更新时间 `2022-03-17 16:35:55` */export interface RelateSelectVo { id: string; key: string; children: RelateSelectVo[]; [k: string]: unknown;}
/** * @nei地址 https://xxxx.com/xxxx * @负责人 网易智企 * @更新时间 `2022-03-17 16:38:49` */export interface RelateSelectConfigVo { groups: RelateSelectVo[]; [k: string]: unknown;}
复制代码


interface_model.d.ts 用来存放接口请求/响应类型


/* tslint:disable *//* eslint-disable */
/** 该文件由 工具 自动生成,请勿直接修改!!!*/
import { RelateSelectVo, RelateSelectConfigVo,} from "./nei_model";
/** * @接口名称 根据基础选项组加载子选项信息 * @接口地址 /receiver/view/getAllQuestionConfig * @nei接口详情地址 https://xxxx.com/xxxx */export interface ReceiverViewGetAllQuestionConfigIn { /** * 问卷id */ surveyId: number; /** * 配置id */ configId: string;}
export interface ReceiverViewGetAllQuestionConfigOut { resultCode: number; resultDesc: string; data: RelateSelectConfigVo;}
复制代码


api/index.ts


import { request } from './request';import type {  ReceiverViewGetAllQuestionConfigIn,  ReceiverViewGetAllQuestionConfigOut,} from '../typings/interface_model';/** * 获取关联选择映射关系 */export const getSelectRelateMap: (  params: ReceiverViewGetConfigRelationIn,) => Promise<ReceiverViewGetConfigRelationOut> = async params => {  const result = await request({    url: '/receiver/view/getConfigRelation',    method: 'GET',    params,    cache: false,  });  return result;};
复制代码


至于 request 函数的具体实现,并没有做过多要求,你可以使用 axios 也可以使用原生的 fetch 或者其他请求库。


上述案例主要是提供基本的请求改造思路,如果大家使用的是 Yapi 或者 Swagger,那么社区上也有一些工具支持自动生成 TypeScript 代码,如果是像我们这样自研的,不妨自己实现一个工具。

其他常见问题及解决方案

第三方模块的识别 


目前 npm 上大部分项目都已经支持 TypeScript 了,但仍然有部分模块还未支持,此时如果引入,那么编译器就会提示异常,此时我们可以通过声明模块解决。


// types.d.tsdeclare module "react-i18next";
复制代码

图片、样式文件的识别


// types.d.tsdeclare module '*.svg';declare module '*.png';declare module '*.jpg';declare module '*.jpeg';declare module '*.gif';declare module '*.bmp';declare module '*.tiff';
declare module '*.css'declare module '*.less'declare module '*.scss'
复制代码

Window 对象增加全局属性 


当我们直接在 Window 上扩展属性时,TypeScript 会提示如下异常:



我们可以在类型文件上对 window 进行扩展

初始化为 {} 的对象如何添加属性 


// types.d.tsdeclare global {  interface Window {    test: any;  }}
复制代码


在 js 开发的时候,我们经常会这么写,先声明一个空的对象,然后设置它的属性,但是在 TypeScript 中,这将抛出类型错误。



方案一:类型补齐(推荐)


interface Person {  name?: string;}const person: Person = {};
person.name = 'Jack';
复制代码


方案二:映射类型,如果是就项目改造,可能一时半会很难去把所有类型补齐,那么可以使用映射类型 Record


const person: Record<string, any> = {};
person.name = 'Jack'
复制代码

编译器提示 Module '**' has no default export


提示模块代码里没有 export defaultt,而你却用 import ** from ** 这种默认导入的形式。


我们可以配置 tsconfig.json 的两个编译参数

  • allowSyntheticDefaultImports

  • esModuleInterop


"compilerOptions": {    // 忽略异常提示,允许默认从没有默认导出的模块导入    "allowSyntheticDefaultImports": true,    // 修改导入规则,支持导入commonjs模块代码    "esModuleInterop": true,}
复制代码

如何为组件添加静态类型 


通常我们在使用部分组件的时候,会从当前组件中直接引用关联组件,例如以下代码片段:


<Select style={{width: 300}}>  <Select.Option value={0}>{'选项0'}</Select.Option>  <Select.Option value={1}>{'选项1'}</Select.Option>  <Select.Option value={2}>{'选项2'}</Select.Option>  <Select.Option value={3}>{'选项3'}</Select.Option></Select>
复制代码


我们通过 Select.Option 就能够使用 Select 的关联组件 Option 组件,在以往 js 开发时,我们只需要简单的通过以下方式:


Select.Option = Option
复制代码


但是在 TypeScript 中直接这么做会提示异常:



所以我们需要做下调整:


const Select: React.FC<{}> = () => {  return <div></div>}
const Options: React.FC<{}> = () => { return <div></div>}
type ExprotSelectType = typeof Select & { Options: typeof Options}const ExportSelect = Select as ExprotSelectType
ExportSelect.Options = Options
export default ExportSelect;
复制代码


我们通过 & 扩展了 Select 类型,在其之上新增了 Options 类型,并用新的类型 ExprotSelectType 来描述 ExportSelect 组件,之后我们就可以通过 ExportSelect.Options = Options 来扩展属性。


但是如果每次扩展组件属性都这么做的话,未免也有些麻烦,所以我们可以写一个函数来实现组件属性的扩展。


export function extendsComponents<C, P extends Record<string, any>>(  component: C,  properties: P): C & P {  const ret = component as any  for (const key in properties) {    if (properties.hasOwnProperty(key)) {      ret[key] = properties[key]    }  }  return ret}
复制代码


之后我们就可以使用以下方式来扩展组件属性:


const Select: React.FC<{}> = () => {  return <div></div>}
const Options: React.FC<{}> = () => { return <div></div>}

export default extendsComponents(Select, { Options });
复制代码


参考文献 

  • typescriptlang 官方文档

  • TypeScript 体操运动员进阶指南

  • [译] TypeScript 牵手 Babel:一场美丽的婚姻

  • Ts 高手篇:22 个示例深入讲解 Ts 最晦涩难懂的高级类型工具

  • 搞懂 TypeScript 中的映射类型(Mapped Types)

发布于: 刚刚阅读数: 3
用户头像

网易智企

关注

网易智企是网易旗下一站式企业服务提供商。 2022.05.09 加入

网易智企是网易旗下一站式企业服务提供商,包含网易易盾、网易云信、网易云商三大业务板块。

评论

发布
暂无评论
【网易云商】TypeScript 进阶指南,突破基本类型_typescript_网易智企_InfoQ写作社区