最佳实践:如何实现函数参数之间的TS类型相依赖和自动推断
引入
最近在开发一款极致优雅的前端状态管理库AutoStore时碰到这样一个问题。
拟实现Field
组件,该组件相关类型简化代码如下:
type Field = (props:{
validate,
render:(props:{
value,
isValid
})
})
该组件,具有validate
和render
两个属性:
- 其中
validate
是校验函数,可以同步校验函数或者异步校验函数 - 重点是
render
渲染函数的props.isValid
的类型,则是动态的,我们希望其依赖于字段的validate
属性。- 当
validate
是同步校验函数时,render.props.isValid=<boolean>
- 当
validate
是异步校验函数时,render.props.isValid={value:boolean,loading:boolean}
- 当
问题很明了,就是函数中的一个参数的类型依赖于另外一个参数的类型。
下面,我详细分析如何实现以及介绍踩过的坑,马上开始。
路坑实现过程
第1步:声明validate
校验参数的类型
首先,我们声明validate
校验参数的类型:
type ComputedGetter<Value=any> = ()=>Value
type AsyncComputedGetter<Value=any> = ()=>Promise<Value>
ComputedGetter
和AsyncComputedGetter
分别代码同步校验和异步校验函数。
第2步:根据不同校验方式转换返回值
接着,编写一个PickResult
类型,根据同步校验
和异步校验函数
返回不同的类型。
type PickResult<T> = T extends AsyncComputedGetter<infer X> ? {value:X,loading:boolean} :
(T extends ComputedGetter<infer X> ? X :
T
)
如果是同步校验校验则返回boolean
,如果是异步校验就返回{value:X,loading:boolean}
第3步:编写Field
组件
然后编写Field
组件声明
function Field<
Value = string,
Validate extends ComputedGetter<boolean> | AsyncComputedGetter<boolean> = ComputedGetter<boolean> | AsyncComputedGetter<boolean>
>(props:{
validate:Validate,
render:(props:{
value:Value,
isValid: PickResult<Validate>
})=>React.ReactNode
}
){
return <>{props.render({
value:"AutoStore"
} as any) }</>
}
Field
组件提供了两个Value
和Validate
两个泛型参数validate
属性可以是同步或异步校验函数- 重点:
render
属性的props.isValid
是由Validate
泛型参数决定的。
第4步:渲染Field
组件
最后让我们来使用渲染Field
组件
- **同步校验参数时 **
<Field
validate = {()=>true}
render={({value,isValid}) =>{
return <>{ isValid ?
<span>{value}</span>
: <span>No</span>
}</>
}}
/>
</>
以上validate = {()=>true}
,所以isValid
被自动推导为boolean
- **异步校验参数时 **
()=>{
return <>
<Field
validate = {async ()=>true}
render={({value,isValid}) =>{
return <>{ isValid.value ?
<span>{value}</span>
: <span>No</span>
}</>
}}
/>
</>
}
以上validate = {async ()=>true}
,所以isValid
被自动推导为{value:boolean,loading:boolean}
第5步:开始踩坑
以上我们编写的组件,已经实现了能根据validate
的属性输入来自动推断render.props
的类型。
render.props.isValid
的类型是根据validate
动态推断而来的。
看起来很完美是不是?
别急,让我们为泛型value
指定一个类型。
()=>{
<>
<Field<number>
validate = {()=>true}
render={({value,isValid}) =>{
return <>{ isValid ?
<span>{value}</span>
: <span>No</span>
}</>
}}
/>
</>
}
- 以上我们为
value
指定了泛型number
,然后我们马上就发现isValid
被推断为:
boolean | {
value: boolean;
loading: boolean;
}
自动推断不生效了? 为什么会这样?
所以,如果我们不指定任何泛型参数,则自动推断生效,如果指定则失效。
问题就在这里:
在Typescript
里面,当不指定任何泛型参数时,Validate
是根据输入自动推断的,而一旦指定了任何泛型参数,则泛型匹配就开始,而以上我们为Validate
指定了一个默认值。
所以,Typescript
就按指定的默认值来为Validate
赋值,而不是根据动态输入,就相当于自动推断失效了.
那么如果我们不为validate
指定默认类型行不行呢?当然可以,但是显得很烦琐。
第6步:解决方案
我们想实现的是:
- 为
validate
指定类型约束 - 能根据动态输入自动推断
显然,上述方案存在问题,并不理想。
问题的核心在于Typescript
对是否指定泛型参数时的自动推规则
- 当没有指定泛形参数时,会进行自动推断。
- 当指定了泛形参数时, 则根据使用泛型参数声明,不进行动态的自动推断
知道了此规则,我们就有了如下的解决方案。
使用函数重载来解决此问题
function Field<Value = string,Validate extends ComputedGetter<boolean> = ComputedGetter<boolean>>(
props:{validate:Validate,render:(props:{value:Value,isValid: PickResult<Validate>})=>React.ReactNode}):any
function Field<Value = string,Validate extends AsyncComputedGetter<boolean> = AsyncComputedGetter<boolean>>(
props:{validate:Validate,render:(props:{value:Value,isValid: PickResult<Validate>})=>React.ReactNode}):any
function Field<Value,Validate>(props:{validate:Validate,render:(props:{value:Value,isValid: PickResult<Validate>})=>React.ReactNode}):any{
return <>{props.render({
value:"AutoStore"
} as any) }</>
}
小结
一开始使用Validate extends ComputedGetter<boolean> | AsyncComputedGetter<boolean> = ComputedGetter<boolean> | AsyncComputedGetter<boolean>
来约束的validate
的本意是:
- 为
Validate
指定ComputedGetter<boolean> | AsyncComputedGetter<boolean>
约束 - 然后可以根据组件的
validate
参数来自动推断,让其他属性也可以自动推断。
但是由于Typescript
对是否指定泛型参数时的自动推规则的问题,需要采用函数重载方式才可以实现此功能。
至此,我们完美地实现以上功能。
顺推一下,AutoStore是新进出炉的一款响应式状态管理库,设计精良,功能强大,大家可以看看。
开源推荐
以下是我的一大波开源项目推荐:
- 全流程一健化React/Vue/Nodejs国际化方案 - VoerkaI18n
- 极致优雅的状态管理库 - AutoStore
- 无以伦比的React表单开发库 - speedform
- 终端界面开发增强库 - Logsets
- 简单的日志输出库 - VoerkaLogger
- 装饰器开发 - FlexDecorators
- 有限状态机库 - FlexState
- 通用函数工具库 - FlexTools
- 小巧优雅的CSS-IN-JS库 - Styledfc
- 为JSON文件添加注释的VSCODE插件 - json_comments_extension
- 开发交互式命令行程序库 - mixed-cli
- 强大的字符串插值变量处理工具库 - flexvars
- 前端link调试辅助工具 - yald
- 异步信号 - asyncsignal
- React/Vue/WebComponent树组件 - LiteTree