#部分类型
自定义工具类型
了解了一些外部工具类型的实现之后,我们尝试实现一些自定义的中间类型
部分可选
指定类型中的某些属性是可选的。
业务场景:在日常开发中,我们经常需要封装一些三方组件,并默认设置一些参数,而这些参数在原组件的参数类型中是必填的,如下:
type SelectProps = {
value: string;
options: string[];
}
const Select: React.FC = (props) => {
// ...
}
type SelectProps = PartOptional
const 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 = false
type T0 = {} extends { sex: string } ? true : false;
// T1 = true
type 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
# 监听模式
watch :
其实你也可以在代码提交前使用husky工具进行类型检测。 配置如下:.husky/pre-commit
. "$(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.ts
declare module "react-i18next";
图片和样式文件的识别
// types.d.ts
declare 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.ts
declare 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)
关于作者
韩高正,网易云业务后端开发工程师,负责网易定位、内部组件库、内部预制组件工具等开发工作。
活动推荐