当前位置: 首页 > Web前端 > HTML

使用TypeScript定义业务字典

时间:2023-03-28 14:36:10 HTML

本文作者:htl业务字典在业务开发中,我们经常需要定义一些枚举值。假设我们正在开发一个音乐应用,需要在业务代码中定义音乐的类型进行业务逻辑判断:constMUSIC_TYPE={POP:1,ROCK:2,RAP:3,//...};if(data.type===MUSIC_TYPE.POP){//当音乐类型为流行音乐时,执行一些逻辑}随着业务逻辑的扩展,简单的枚举值往往会生成很多关联的字典。比如我们需要定义一个对应音乐类型的名称constMUSIC_TYPE_NAMES={[MUSIC_TYPE.POP]:'流行音乐',[MUSIC_TYPE.ROCK]:'摇滚音乐',[MUSIC_TYPE.RAP]:'说唱音乐',//...};//显示音乐类型名称

{MUSIC_TYPE_NAMES[data.type]}
或者需要定义一个音乐类型对应的图标:constMUSIC_TYPE_ICONS={[MUSIC_TYPE.POP]:'流行音乐。svg',[MUSIC_TYPE.ROCK]:'rock.svg',[MUSIC_TYPE.RAP]:'rap.svg',//...};//显示音乐类型图标list场景下,我们可能需要定义一个数组形式的字典:constMUSIC_TYPE_LIST=[{type:MUSIC_TYPE.POP,name:'popmusic',icon:'pop.svg',},{type:MUSIC_TYPE.ROCK,name:'摇滚音乐',icon:'rock.svg',},{type:MUSIC_TYPE.RAP,name:'rapmusic',icon:'rap.svg',},//...];
{MUSIC_TYPE_LIST.map((item)=>(
{item.name}
))}
;或者你想使用key-object形式来避免从多个字典中取值:constMUSIC_TYPE_MAP_BY_VALUE={[MUSIC_TYPE.POP]:{name:'流行音乐',icon:'pop.svg',},[MUSIC_TYPE.ROCK]:{name:'rockmusic',icon:'rock.svg',},[MUSIC_TYPE.RAP]:{name:'rapmusic',icon:'rap.svg',},//...};constmusicTypeInfo=MUSIC_TYPE_MAP_BY_VALUE[data.type];
{musicTypeInfo.name}:{musicTypeInfo.icon}
;这些商业词典以不同的形式存在将给代码带来重复和混乱当我们需要更改或增删某个类型或某个类型中的某个值时,我们需要同时修改多个字典,容易出现遗漏和错误,尤其是当这些字典定义分布在不同的文件中。对于用户来说,分散的词典定义也是一种负担。在业务中使用某部词典时,需要查阅现有的词典,了解其定义。如果现有词典不能完全满足要求,则可能定义新的词典,进一步增加业务词典的混乱度。字典工厂函数我们可以实现一个实用函数,将一个定义转换成各种格式的字典。首先考虑输入参数的格式。显然,作为原始数据,入参必须能够包含完整的字典信息,包括key、value、所有的扩展字段,甚至是列表场景中的显示顺序。我们可以使用一个对象数组作为输入参数:/***listexample:*[*{*key:'POP',*value:1,*name:'popmusic',*},*{*key:'ROCK',*value:2,*name:'Rockmusic',*},*//...*]*/functiondefineConstants(list){//...}接下来,考虑格式输出参数。输出参数应该是一个对象,包含各种格式的字典:const{KV,VK,LIST,MAP_BY_KEY,MAP_BY_VALUE}=defineConstants([{key:'POP',value:1,name:'Popmusic',},{key:'ROCK',value:2,name:'摇滚音乐',},//...]);KV;//{POP:1,ROCK:2,...}VK;//{1:'POP',2:'摇滚',...}LIST;//[{key:'POP',value:1,name:'PopMusic'},{key:'ROCK',value:2,name:'RockMusic'},...]MAP_BY_KEY;//{POP:{key:'POP',value:1,name:'PopMusic'},ROCK:{key:'ROCK',value:2,name:'RockMusic'},...}MAP_BY_VALUE;//{1:{key:'POP',value:1,name:'PopMusic'},2:{key:'ROCK',value:2,name:'摇滚音乐'},...}In实际业务中,我们会为不同的资源定义字典,因此需要为实用函数提供命名空间。使用第二个输入参数为输出参数中的键添加前缀:const{MUSIC_TYPE_KV,MUSIC_TYPE_VK,MUSIC_TYPE_LIST,MUSIC_TYPE_MAP_BY_KEY,MUSIC_TYPE_MAP_BY_VALUE,}=defineConstants([{key:'POP',value:1,name:'PopularMusic',},{key:'ROCK',value:2,name:'摇滚音乐',},//...],'MUSIC_TYPE',);至此,我们就完成了字典工厂功能的设计。这个函数的JavaScript实现并不复杂。你可能在一些项目中看到过类似的实用函数,但是当你真正使用它的时候你会发现一个问题。使用TypeScript实现类型提示使用字典工厂定义业务字典,可以让代码更加简洁,规范字典数据格式。但是,与直接定义相比,字典工厂的缺点是它们不能提供类型提示。这在两个层面上给开发者带来了不便。首先,在定义字典的时候,需要对效用函数的使用和实现有一定的了解,这样才能正确的传入参数和解构返回值;第二,无法获得Typehints,使用字典的开发者需要回过头来看看定义了哪些字段和值,还需要了解实用函数是如何使用的。为了解决这个问题,我们可以使用TypeScript来实现一个字典工厂函数。下面介绍了TypeScript类型系统的一些特性和一些技巧。LIST字典的实现先实现最简单的LIST字典,因为和入参完全一样:interfaceIBaseDef{key:PropertyKey;值:字符串|number;}functiondefineConstants(defs:T,namespace?:N,){constprefix=namespace?`${命名空间}_`:'';return{[`${prefix}LIST`]:defs,};}我们使用IBaseDef来标准化输入字典中item的类型,里面包含key和value两个字段。key的类型是PropertyKey,是string|的联合类型编号|symbol,即key的值可以是这三种类型中的任意一种。值的类型是字符串|数字。之所以没有符号是因为业务中value的值可能是从外部获取的,而key的值可能是运行时产生的。这两个字段是定义字典所必需的,其他字段可以根据业务需要任意添加。在defineConstants函数中,我们使用泛型来表示两个入参的类型,并使用extends关键字来约束泛型的类型。T的类型为IBaseDef[],保证输入参数defs的格式符合字典项数组。N的类型为字符串,入参命名空间保证为字符串。命名空间参数是可选的。如果在定义字典的时候没有传入字典,那么返回的字典Key是没有前缀的。所以我们需要创建一个前缀变量,并根据命名空间是否存在来确定它的值。然后我们返回一个只有一个LIST字典的对象,它的Key由prefix和LIST拼接而成,它的值是入参defs。这段代码的运行逻辑没有问题,但是缺少返回值的类型定义,通过IDE的代码提示无法得到正确的字典key:当你在IDE中检查dict的类型时,IDE不会真正执行JavaScript代码,而是通过TypeScript的类型系统生成类型。因此,我们需要使用类型系统定义defineConstants的返回类型。输入ToProperty=Nextends''?属性:`${N}_${Property}`;这里我们定义了一个类型,用来生成字典的Key。它接收两个泛型参数,Property代表字典的属性,N代表字典的命名空间。如果N为空字符串,则返回的Key为Property,否则为${N}_${Property}。这段代码中有一些JavaScript语法的影子,比如字符串、默认参数值、三元运算符、模板字符串等。但这些都是在TypeScript类型系统中运行的,可以认为它是一门独立的语言。例如,它没有if...else语句。这里的三元运算其实就是ConditionalTypes的语法。当N的类型符合''时,返回Property,否则返回${N}_${Property}。您可以将此类类型定义视为类型系统中的“函数”。与接受一个值作为参数并返回一个新值的JavaScript函数不同,它通过泛型接受一个类型并返回一个新类型。现在我们可以使用ToProperty生成字典的键类型:接下来,使用ToProperty结合MappedTypes和TypeAssertions指定defineConstants的返回类型:functiondefineConstants(defs:T,命名空间?:N,){constprefix=命名空间?`${命名空间}_`:'';返回{[`${prefix}LIST`]:defs,}as{[KeyinToProperty<'LIST',N>]:T;};}as关键字表示类型系统中的类型断言,是一种手动指定类型的方式。它允许您告诉编译器变量或值的类型是什么,而不是让编译器自动推断它。另一方面,类型映射是一种将现有类型转换为具有指定键值的新类型的方法。我们创建了一个新的对象类型,键为ToProperty<'LIST',N>,值为T。结合这些,defineConstants函数最终可以返回一个支持类型提示的字典:KV字典的实现后面是添加一个KV字典,它是一个键值对,key和value来自于key和value属性在输入字典项中。functiondefineConstants(defs:T,namespace?:N,){constprefix=namespace?`${命名空间}_`:'';返回{[`${prefix}LIST`]:defs,[`${prefix}KV`]:defs.reduce((map,item)=>({...map,[item.key]:item.值,}),{},),}asMergeIntersection<{[ToProperty<'LIST',N>]:T;}&{[输入ToProperty<'KV',N>]:{[输入ToProperty<'KV',N>]:ToKeyValue;};}>;}这段代码增加了MergeIntersection、ToSingleKeyValue和ToKeyValue三个类型转换“函数”,进一步限制泛型T为readonly。接下来,我们将解释这些类型转换的作用和实现,以及为什么T必须是只读的。MergeIntersection用于合并交集类型。由于我们实现中不同的字典类型是通过映射类型生成的,所以我们需要使用交集类型(IntersectionTypes)来合并它们,这在合并多种类型时变得难以阅读。使用MergeIntersection可以将交集类型合并为一个类型,视觉上更加清晰,方便后续处理:MergeIntersection的实现:typeMergeIntersection=AextendsinferT?{[KeyofT]:T[Key]}:never;在这里,我们再次使用条件类型和映射类型。infer关键字是类型推断(TypeInference)的语法,可以让我们获取条件类型中类型变量的具体类型,用于后续的映射类型。由于infer总是推断一个类型,条件类型的第二个结果永远不会出现,所以我们可以使用never类型。ToSingleKeyValue用于将单个字典项转换为键值对:ToSingleKeyValue的实现:typeToSingleKeyValue=Textends{readonlykey:inferK;只读值:推断V;}?K扩展PropertyKey?{[输入K]:V;}:从不:从不;我们使用infer关键字来获取键和值的具体类型,并在条件类型中使用它们。然后在第二个条件类型中指定key的类型为PropertyKey,这样就可以用来映射类型了。最后指定map类型中的keys和values。ToKeyValue用于将字典项数组转换为键值对:ToKeyValue的实现:typeToKeyValue=Textendsreadonly[inferA,...inferB]?B['长度']延伸0?ToSingleKeyValue:MergeIntersection&ToKeyValue>:[];这个实现的重点是利用类型推断结合扩展语法和递归特性来实现对数组类型的处理。我们在第一个条件类型中获取数组的第一个和剩余元素,然后在第二个条件类型中判断剩余元素的长度是否为0。如果为0,则表示数组只有一个元素,我们可以直接使用ToSingleKeyValue进行类型转换。否则转换第一个元素并使用ToKeyValue递归转换其余元素,最后使用MergeIntersection合并结果。defineConstants中readonly关键字的使用以及这些类型转换函数其实源于defineConstants的一个使用限制:使用defineConstants时,必须使用[constassertions(constassertions)](https://www.typescriptlang.or..),即在字典项数组之后添加asconst。defineConstants([{key:'POP',value:1,name:'popmusic',},{key:'ROCK',value:2,name:'rockmusic',},]asconst,'MUSIC_TYPE');对于代码中的常量定义,TypeScript会自动推断变量类型并擦除具体值。这在通常情况下是合理的,但是对于defineConstants类型提示的实现是一个很大的障碍。如果输入的字典项中的值信息丢失,我们就无法通过类型系统进行类型转换来生成字典的类型定义。比较一下usingasconst的区别:使用const断言也会让字典项的属性在类型系统中变成只读的,这也是我们在函数中使用readonly关键字的原因。以上内容基本涵盖了其余字典类型转换所需要的语法和技巧。比如VK格式只是交换了key和value,MAP_BY_KEY只是把value替换成了字典项的类型,这里不再赘述。GithubGist中提供了完整的实现,或者您可以直接在此CodeSandbox示例中试用效果。到目前为止,我们已经使用TypeScript实现了一个可以生成支持类型提示的业务字典工厂函数。通过这个函数定义和业务字典的使用,可以随处获取类型提示。Whendefiningdictionaries:Whenusingdictionaries:defectsandindifications这个工具在项目中给作者本人带来了很大的帮助,但是还是有一些缺陷和不足:只能在TypeScript项目中使用,需要使用aswhen定义字典??const关键字。通常实用函数是在TypeScript中实现的,只要提供良好的类型定义,就可以方便地在JavaScript项目中使用。但由于JavaScript不支持const断言或类似断言,因此此工具仅在TypeScript中可用。用户无法获取类型提示中的注解。当我们定义一个枚举值时,可能会加上一些注解:enumMusicTypes{/***Popular*/POP:1,}开发者在使用这个枚举值时,可以通过IDE获取评论内容。但是字典工厂函数生成的字典在转换后已经丢失了这些信息。不能同时导出类型定义。defineConstants返回一个字典值。当下游需要引用字典类型时,需要额外导出一个类型定义:exportconst{MUSIC_TYPE_VALUES}=defineConstants([...],'MUSIC_TYPE')//exportdictionaryTypeexporttypeMUSIC_TYPE=MUSIC_TYPE_VALUES[number]//下游类型定义import{MUSIC_TYPE}from'./constants'interfaceMusic{type:MUSIC_TYPE;//...}总结了本文为业务字典定义的场景,使用TypeScript实现了一个实用函数,用于生成各种形式的带有类型提示的业务字典。同时指出了该效用函数的一些局限和不足。本文由网易云音乐技术团队发布。未经授权禁止任何形式的转载。我们常年招聘各种技术岗位。如果你要跳槽,又恰好喜欢云音乐,那就加入我们吧grp.music-fe(at)corp.netease.com!