typescript用什么软件写_用 typescript 写一个工具函数库

原标题:用 typescript 写一个工具函数库

作者介绍

翟旭光,2019 年 11 月加入 Qunar 机票前端团队,目前在国内基础平台。喜欢工程化和 type,喜欢各种提升开发效率的工具,对这方面有浓厚的兴趣,有很多想法待实现。喜欢阅读源码,热衷于探索源码的乐趣,就像在深海探险,会因为偶然发现的一个冷僻的知识点或技巧而兴奋。

技术点介绍

工具函数的复杂类型的声明(难点)

用 ts-mocha + chai 做单元测试

用 ts + rollup 打不同模块规范的包

前言

先看一段代码

const { name = 'xxx' , age } = { name : null , age : 18 }

console . log ( name );

name 输出的是 null,因为解构赋值的默认值只有当值为 undefined 时才会生效,这点如果不注意就会引起 bug。我们组内最近就遇到了因为这点而引起的一个 bug,服务端返回的数据,因为使用了解构赋值的默认值,结果因为值为 null 没有被赋值,而导致了问题。

那么如何能避免这种问题呢?

我们最终的方案有两种,第一种服务端返回数据之后递归的设置默认值,之后就不需要再做判断,直接处理就行。第二种是当取属性的时候去做判断,如果为 null 或 undefined 就设置默认值。为了支持这两种方案,我们封装了一个工具函数包 @qnpm/flight-common-utils。

这个工具包首先要包含 setDefaults、getProperty 这两个函数,第一个是递归设置默认值的,第二个是取属性并设置默认值的。除此之外还可以包含一些别的工具函数,把一些通用逻辑封装进来以跨项目复用。比如判空 isEmpty,递归判断对象和属性是否相等 isEqual 等。

因为用了 typ,通用函数考虑的情况很多,为了更精准的类型提示,类型的逻辑写的很复杂,比实现逻辑的代码都多。

使用

npm install @qnpm / flight - common - utils -- save -- registry =公司 npm 仓库

或者

yarn add @qnpm / flight - common - utils -- registry =公司 npm 仓库

实现工具函数

这里只介绍类型较为复杂的 setDefaults、getProperty。

setDefaults

这个函数的参数是一个待处理对象,若干个默认对象,最后一个参数可以传入一个函数自定义处理逻辑。

function setDefaults ( obj , ... defaultObjs ) {

}

希望使用时这样调用:

setDefaults ({ a : { b : 2 }}, { a : { c : 3 }} ); // {a: {b: 2, c: 3}}

这里的类型的特点是函数返回值是原对象和一些默认对象的合并,并且参数个数不确定。所以用到了函数类型的重载,加上 any 的兜底。

type SetDefaultsCustomizer = ( objectValue : any , sourceValue : any , key ?: string , object ?: {}, source ?: {}) => any ;

SetDefaultsCustomizer 是自定义处理函数的类型,接受两个需要处理的值,和 key 的名字,还有两个对象。

然后是 setDefauts 的类型,这里重载了很多情况的类型。

function setDefaults < TObject >( object : TObject ): TObject ;

如果只有一个参数,那么直接返回这个对象。

function setDefaults < TObject , TSource >( object : TObject , source : TSource , customizer : SetDefaultsCustomizer ): TObject & TSource ;

当传入一个 source 对象时,返回的对象为两个对象的合并 TObject & TSource。

function setDefaults < TObject , TSource1 , TSource2 >( object : TObject , source1 : TSource1 , source2 : TSource2 , customizer : SetDefaultsCustomizer ): TObject & TSource1 & TSource2 ;

function setDefaults < TObject , TSource1 , TSource2 , TSource3 >( object : TObject , source1 : TSource1 , source2 : TSource2 , source3 : TSource3 , customizer : SetDefaultsCustomizer ): TObject & TSource1 & TSource2 & TSource3 ;

function setDefaults < TObject , TSource1 , TSource2 , TSource3 , TSource4 >( object : TObject , source1 : TSource1 , source2 : TSource2 , source3 : TSource3 , source4 : TSource4 , customizer : SetDefaultsCustomizer ): TObject & TSource1 & TSource2 & TSource3 & TSource4 ;

因为参数数量不固定,所以需要枚举参数为 1,2,3,4 的情况,同时加一个 any 的情况来兜底,这样声明当用户写 4 个和以下参数的时候都是有提示的,但超过 4 个就只能提示 any 了,能覆盖大多数使用场景。

实现这个函数:

type AnyObject = Record < string | number | symbol , any >;

function setDefaults ( obj : any , ... defaultObjs : any []): any {

// 把数组赋值一份

const defaultObjsArr = Array . prototype . slice . call ( defaultObjs );

// 取出自定义处理函数

const customizer = ( function {

if ( defaultObjsArr . length && typeof defaultObjsArr [ defaultObjs . length - 1 ] === "function" ) {

return defaultObjsArr . splice (- 1 )[ 0 ];

}

});

// 通过reduce循环设置默认值

return defaultObjsArr . reduce (( curObj : AnyObject , defaultObj : AnyObject ) => {

return assignObjectDeep ( curObj , defaultObj , customizer );

}, Object ( obj ));

}

Record 是内置类型,具体实现是:

type Record < K extends string | number | symbol , T > = { [ P in K ]: T ; }

所以,AnyObject 其实就是一个值为any类型的对象。

把参数数组赋值一份后,取出自定义处理函数,通过 reduce 循环设置默认值。assignObjectDeep 实现的是给一个对象递归设置默认值的逻辑。

const assignObjectDeep = < TObj extends AnyObject , Key extends keyof TObj >(

obj : TObj ,

srcObj : TObj ,

customizer : SetDefaultsCustomizer

): TObj => {

for ( const key in Object ( srcObj )) {

if (

typeof obj [ key ] === "object" &&

typeof srcObj [ key ] === "object" &&

getTag ( srcObj [ key ]) !== "[object Array]"

) {

obj [ key as Key ] = assignObjectDeep ( obj [ key ], srcObj [ key ], customizer );

} else {

obj [ key as Key ] = customizer

? customizer ( obj [ key ], srcObj [ key ], key , obj , srcObj )

: obj [ key ] == void 0

? srcObj [ key ]

: obj [ key ];

}

}

return obj ;

};

类型只限制了必须是一个对象也就是 TObj extends AnyObject,同时 key 必须是这个对象的索引 Key extends keyof TObj。

通过 for in 遍历这个对象,如果是数组,那么就递归,否则合并两个对象,当有 customizer 时,调用该函数处理,否则判断该对象的值是否为 null 或 undefined,是则用默认值。(void 0 是 undefeind,== void 0 就是判断是否为 null 或 undefeind)

getProperty

getProperty 有三个参数,对象,属性路径和默认值。

function getProperty ( object , path , defaultValue ){}

希望使用时这样调用:

const object = { 'a' : [{ 'b' : { 'c' : 3 } }] }

getProperty ( object , 'a[0].b.c' )

// => 3

getProperty ( object , [ 'a' , '0' , 'b' , 'c' ])

// => 3

getProperty ( object , 'a.b.c' , 'default' )

// => 'default'

因为重载情况较多,类型比较复杂,这是工具类函数的特点。首先声明几个用到的类型:

type AnyObject = Record < string | number | symbol , any >;

type Many < T > = T | ReadonlyArray < T >;

type PropertyName = string | number | symbol ;

type PropertyPath = Many < PropertyName >;

interface NumericDictionary < T > {

[ index : number ]: T ;

}

AnyObject 为值为 any 的对象类型。Record 和 ReadonlyArray 是内置类型。PropertyName 为对象的索引类型,只有三种,string、number、symbol,PropertyPath 是 path 的类型,可以是单个的 name,也可以是他们的数组,所以写了一个工具类型 Many 来生成这个类型。NumericDictionary 是一个 name类型为 number,值类型固定的对象,类似数组。

首先是 object 为 null 和 undefined 的情况:

function getProperty (

object : null | undefined ,

path : PropertyPath

): undefined ;

function getProperty < TDefault >(

object : null | undefined ,

path : PropertyPath ,

defaultValue : TDefault

): TDefault ;

然后是 object 为数组时的类型:

function getProperty < T >(

object : NumericDictionary < T >,

path : number

): T ;

function getProperty < T >(

object : NumericDictionary < T > | null | undefined ,

path : number

): T | undefined ;

function getProperty < T , TDefault >(

object : NumericDictionary < T > | null | undefined ,

path : number ,

defaultValue : TDefault

): T | TDefault ;

接下来是 object 为对象的情况,这里的特点和 setDefaults 一样,path 可能为元素任意个的数组,又要声明他们的顺序,这里只是写了参数分别为 1 个 、2 个 、3 个、 4 个的类型,然后加上 any 来兜底。

当 path 的元素只有一个的时候:

function getProperty < TObject extends object , TKey extends keyof TObject >(

object : TObject ,

path : TKey | [ TKey ]

): TObject [ TKey ];

function getProperty < TObject extends object , TKey extends keyof TObject >(

object : TObject | null | undefined ,

path : TKey | [ TKey ]

): TObject [ TKey ] | undefined ;

function getProperty < TObject extends object , TKey extends keyof TObject , TDefault >(

object : TObject | null | undefined ,

path : TKey | [ TKey ],

defaultValue : TDefault

): Exclude < TObject [ TKey ], undefined > | TDefault ;

当传入默认值时,返回值可能是默认值 TDefault,也可能是对象的值 TObject[TKey],但 TObject[TKey]一定不是 undefined,所以这里这么写。

Exclude < TObject [ TKey ], undefined > | TDefault

然后是 path 有 2 个元素的时候:

function getProperty < TObject extends object , TKey1 extends keyof TObject , TKey2 extends keyof TObject [ TKey1 ]>(

object : TObject | null | undefined ,

path : [ TKey1 , TKey2 ]

): TObject [ TKey1 ][ TKey2 ] | undefined ;

function getProperty < TObject extends object , TKey1 extends keyof TObject , TKey2 extends keyof TObject [ TKey1 ], TDefault >(

object : TObject | null | undefined ,

path : [ TKey1 , TKey2 ],

defaultValue : TDefault

): Exclude < TObject [ TKey1 ][ TKey2 ], undefined > | TDefault ;

3 个、4 个也是一样,就不列了。

兜底类型:

function getProperty (

object : any ,

path : PropertyPath ,

defaultValue ?: any

): any ;

实现思路是先处理 null 和 undefined 的情况,然后循环取属性值,如果值为 undefined 则返回默认值,否则返回取到的值。这里参考了 lodash 的实现。

function getProperty ( object : any , path : PropertyPath , defaultValue ?: any ): any {

//处理null 和undefined

const result = object == null ? undefined : baseGet ( object , path )

//如果取到的值是undefined或null则返回默认值(根据我们的需求,null时也需要返回默认值)

return result == undefined ? defaultValue : result

}

function baseGet ( object : any , path : PropertyPath ): any {

path = castPath ( path , object )

let index = 0

const length = path . length

// 循环取path对象的属性值

while ( object != null && index < length ) {

object = object [ toKey ( path [ index ++])]

}

// 如果取到了最后一个元素,则返回该值,否则返回undefined

return ( index && index === length ) ? object : undefined

}

测试

测试使用的 ts-mocha 组织测试用例,使用 chai 做断言。

getProperty 的测试,测试了 object 为无效值、对象、数组,还有 path 写错的时候的逻辑。

describe ( 'getProperty' , => {

const obj = { a : { b : { c : 1 , d : null } } }

const arr = [ 1 , 2 , 3 , {

obj

}]

it ( '对象为无效值时,返回默认值' , => {

assert . strictEqual ( getProperty ( undefined , 'a.b.c' , 1 ), 1 )

assert . strictEqual ( getProperty ( null , 'a.b.c' , 1 ), 1 )

assert . strictEqual ( getProperty ( '' , 'a.b.c' , 1 ), 1 )

})

it ( '能拿到对象的属性path的值' , => {

assert . strictEqual ( getProperty ( obj , 'a.b.c' ), 1 )

assert . strictEqual ( getProperty ( obj , 'a[b][c]' ), 1 )

assert . strictEqual ( getProperty ( obj , [ 'a' , 'b' , 'c' ]), 1 )

assert . strictEqual ( getProperty ( obj , 'a.b.d.e' , 1 ), 1 )

})

it ( '错误的属性path的值会返回默认值' , => {

assert . strictEqual ( getProperty ( obj , 'c.b.a' , 100 ), 100 )

assert . strictEqual ( getProperty ( obj , 'a[c]' , 100 ), 100 )

assert . strictEqual ( getProperty ( obj , [], 100 ), 100 )

})

it ( '数组能取到属性path的值' , => {

assert . strictEqual ( getProperty ( arr , '1' ), 2 )

assert . strictEqual ( getProperty ( arr , [ 1 ]), 2 )

assert . strictEqual ( getProperty ( arr , [ 3 , 'obj' , 'a' , 'b' , 'c' ]), 1 )

})

})

测试通过。

编译打包

工具函数包需要打包成 cmd、esm、umd 三种规范的包,同时要支持 type,所以要导出声明文件。

通过 type 编译器可以分别编译成 cmd、esm 版本,也支持导出.d.ts声明文件,umd 的打包使用 rollup。

其中,tsconfig.json 为:

{

"compilerOptions" : {

"noImplicitAny" : true ,

"removeComments" : true ,

"preserveConstEnums" : false ,

"allowSyntheticDefaultImports" : true ,

"sourceMap" : false ,

"types" : [ "node" , "mocha" ],

"lib" : [ "es5" ],

"downlevelIteration" : true //支持set等的迭代

},

"include" : [

"./src/**/*.ts"

]

}

然后 esm 和 cjs 还有 types 都继承了这个配置文件,重写了 module 的类型。

{

"extends" : "./tsconfig.json" ,

"compilerOptions" : {

"module" : "commonjs" ,

"target" : "es5" ,

"outDir" : "./dist/cjs"

}

}

{

"extends" : "./tsconfig.json" ,

"compilerOptions" : {

"module" : "esnext" ,

"target" : "es5" ,

"removeComments" : false ,

"outDir" : "./dist/esm"

},

}

同时,types 的配置要加上 declaration 为 true,并通过 declarationDir 指定类型文件的输出目录。

{

"extends" : "./tsconfig.json" ,

"compilerOptions" : {

"module" : "es2015" ,

"removeComments" : false ,

"declaration" : true ,

"declarationMap" : false ,

"declarationDir" : "./dist/types" ,

"emitDeclarationOnly" : true ,

"rootDir" : "./src"

}

}

还有 rollup 的 ts 配置文件也需要单独出来,module 类型为 esm,rollup 会做接下来的处理。

{

"extends" : "./tsconfig.json" ,

"compilerOptions" : {

"module" : "esnext" ,

"target" : "es5"

}

}

然后是 rollup 的配置,rollup 用来做 umd 的打包。

import nodeResolve from '@rollup/plugin-node-resolve'

import commonjs from '@rollup/plugin-commonjs'

import type from '@rollup/plugin-type'

import replace from 'rollup-plugin-replace'

import { terser } from 'rollup-plugin-terser'

import pkg from './package.json'

const env = process . env . NODE_ENV

const config = {

input : 'src/index.ts' ,

output : {

format : 'umd' ,

name : 'FlightCommonUtils'

},

external : Object . keys ( pkg . peerDependencies || {}),

plugins : [

commonjs ,

nodeResolve ({

jsnext : true

}),

type ({

tsconfig : './tsconfig.esm.rollup.json'

}),

replace ({

'process.env.NODE_ENV' : JSON . stringify ( env )

})

]

}

if ( env === 'production' ) {

config . plugins . push (

terser ({

compress : {

pure_getters : true ,

unsafe : true ,

unsafe_comps : true ,

warnings : false

}

})

)

}

package.json 中注册 s:

{

"s" : {

"build:cjs" : "tsc -b ./tsconfig.cjs.json" ,

"build:es" : "tsc -b ./tsconfig.esm.json" ,

"build:test" : "tsc -b ./tsconfig.test.json" ,

"build:types" : "tsc -b ./tsconfig.types.json" ,

"build:umd" : "cross-env NODE_ENV=development rollup -c -o dist/umd/flight-common-utils.js" ,

"build:umd:min" : "cross-env NODE_ENV=production rollup -c -o dist/umd/flight-common-utils.min.js" ,

"build" : "npm run clean && npm-run-all build:cjs build:es build:types build:umd build:umd:min" ,

"clean" : "rimraf lib dist es"

}

}

接下来,在 package.json 中对不同的模块类型的文件做声明。

main 是 node 会查找的字段,是 cjs 规范的包,module 是 webpack 和 rollup 会读取的,是 esm 规范的包,types 是 tsc 读取的,包含类型声明。umd 字段只是一个标识。

总结

本文详细讲述了封装这个包的原因,以及一些通用函数的实现逻辑,特别是复杂的类型如何去写。然后介绍了 ts-mocha + chai 来做测试,rollup + type 做编译打包。一个工具函数库就这么封装的。其中 type 的类型声明算是比较难的部分吧,想写出类型简单,把类型写的准确就不简单了,特别是工具函数,情况特别的多。

希望大家能有所收获。返回搜狐,查看更多

责任编辑:

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值