typescript动态key-技术干货| TypeScript进阶手册,突破基本类型

2023-08-29 0 8,836 百度已收录

#部分类型

自定义工具类型

了解了一些外部工具类型的实现之后,我们尝试实现一些自定义的中间类型

部分可选

指定类型中的某些属性是可选的。

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

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

在上面的例子中,我们有一个组件Select,它的参数值和选项都是必填的。 基于一些业务需求,我们需要扩展Select的一些功能,同时扩展组件中的外部选项参数,所以我们需要将原来的组件添加到新组件中,将参数中的选项设为可选,否则会出现TypeScript异常提示。

我们看一下PartOptional的实现逻辑:

type PartOptional = {  [P in keyof Omit]: T[P];} & {  [P in K]?: T[P];};
interface Person { name: string; age: number; sex: string;};
type PersonSimple = PartOptional;

选择可选

从所选类型中选择可选类型并生成新类型。

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

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

在高层组件中,我们将原始组件的可选参数作为单独的配置开放

// { sex: string; likes: string[] }type OptionConfig = PickOptional;

实现原理

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

类型拆解:

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

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

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

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

商业实践

工程环境配置

早期版本:

React+webpack+babel-loader+ts-loader

当前版本:

React+Webpack+babel7+babel-loader+@babel/preset-react+@babel/preset-typescript

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

以下是我们项目代码中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别名路径

问题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": ["types.d.ts"]}

但这样配置就会出现App.tsx

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

{  "include": ["src/**/*", "types.d.ts"]}

此时,编译工具已经可以正确识别模块类型。

杰克恳求改造

当业务迁移到TypeScript时,socket数据类型补全占据了相当一部分工作。 以前不使用TypeScript的时候,基本上都是直接读取socket文档就可以了。 如果比较正式的话,可以写一个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来开发项目时,这将是一个非常繁琐的过程,比如:

工程socket数量多,弥补了特殊耗时

socket类型中可复用的类型较多,手动维护费时费力

前端socket更新后,需要自动寻找对应的socket来补全类型参数

socket类型没有提前完成,直接影响业务代码的开发typescript动态key,直接表明要么提示any,要么报异常

针对前三个问题typescript动态key,我们开发了类型生成工具pp-type来实现Nei(内部socket文档)类型的手动同步。

pp型设计流程

实现原理是通过json-schema()和json-schema-to-typescript()完成socket文档的类型转换,并生成.d.ts声明文件放入项目中。

json-schema:用于声明和验证 JSON 数据。

json-schema-to-typescript:负责将 json-schema 转换为 TypeScript 类型。

举个反例:通过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[];}

业务使用场景:

运行pptypeupdate命令后,会生成两个type文件

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 用于存储socket请求/响应类型

/* 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 = async params => {  const result = await request({    url: '/receiver/view/getConfigRelation',    method: 'GET',    params,    cache: false,  });  return result;};

至于请求功能的具体实现,没有太多的要求。 您可以使用 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 会提示以下异常:

我们可以扩展类型文件上的窗口

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

// types.d.tsdeclare global {  interface Window {    test: any;  }}

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

选项 1:类型互补(推荐)

interface Person {  name?: string;}const person: Person = {};
person.name = 'Jack';

解决方案2:映射类型。 如果是项目重构,可能一时半会很难凑齐所有类型,所以可以使用映射类型Record。

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

编译器提示Module'**'hasnodefaultexport

提示模块代码中没有exportdefaultt,但是你使用了import**from**的默认导出方式。

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

"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 
}
const Options: React.FC = () => { return
}
type ExprotSelectType = typeof Select & { Options: typeof Options}const ExportSelect = Select as ExprotSelectType
ExportSelect.Options = Options
export default ExportSelect;

我们通过&扩展了Select类型,在其之上添加了Options类型,并使用新类型ExrotSelectType来描述ExportSelect组件,然后我们可以通过ExportSelect.Options=Options来扩展属性。

而如果每次扩展组件属性时都这样做,那就有点麻烦了,所以我们可以写一个函数来实现组件属性的扩展。

export function extendsComponents<C, P extends Record>(  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 
}
const Options: React.FC = () => { return
}

export default extendsComponents(Select, { Options });

参考

1.Typescriptlang官方文档

2.TypeScript举重运动员高级手册

3.【翻译】TypeScript 和 Babel:美好的婚姻

4、Ts大神文章:22个例子深入讲解Ts最深奥、最难的中级类型工具

5.了解TypeScript中的映射类型(MappedTypes)

关于作者

韩高正,网易云业务后端开发工程师,负责网易定位、内部组件库、内部预制组件工具等开发工作。

活动推荐

收藏 (0) 打赏

感谢您的支持,我会继续努力的!

打开微信/支付宝扫一扫,即可进行扫码打赏哦,分享从这里开始,精彩与您同在
点赞 (0)

悟空资源网 typescript typescript动态key-技术干货| TypeScript进阶手册,突破基本类型 https://www.wkzy.net/game/169158.html

常见问题

相关文章

官方客服团队

为您解决烦忧 - 24小时在线 专业服务