最近重新翻刷了官方文档,有那么一句话,不推荐在 reactive 使用泛型。
然后我就疑惑了,日常使用中,对于 ref 我们需要限定其中类型时,我们就会通过泛型来约束,而 reactive 也很自然沿用了这种思维,也没遇到什么问题。
那这里说的不推荐,是什么情况呢?
可能带来的问题
既然这句话写着深层次 ref 相关,那就尝试写一个例子看看会出现什么问题。
有这么一段伪代码:我们已有 name 变量,有一个表单 form,其中属性 name 是来自 name 变量。
interface Form {
id: string
name: string
}
const name = '我的名字'
const form = reactive<Form>({
id: '',
name
})
这样使用 name 是没问题的,但是如果 name 是一个 Ref 类型的话
const name = ref('我的名字')
const form = reactive<Form>({
id: '',
name
})
name 就会报错:Type 'Ref<string>' is not assignable to type 'string'。
但这时候你可能会想:将 Form 中的 name 改为 Ref 不就好了吗?
interface Form {
id: string
name: Ref<string>
}
这时候确实解决报错的问题了,但你会发现另外一个问题,form 的自动推导类型 name 是 string,而我们定义的接口 Form 里面的 name 却是 Ref<string>。
所以问题找到了: reactive带有深层次的 ref时,我们如果通过泛型来约束类型,类型是会对应不上的!
分析
既然问题是出在类型上,我们就深入 reactive 这个函数的类型看看做了什么事情(vscode 按键 Command+ 鼠标左键)?
// 这里把大部分相关类型贴出来,一些用不到的就不一一贴出来了
declare type BaseTypes = string | number | boolean;
export declare function reactive<T extends object>(target: T): UnwrapNestedRefs<T>;
export declare type UnwrapNestedRefs<T> = T extends Ref ? T : UnwrapRefSimple<T>;
declare type UnwrapRefSimple<T> = T extends Function | CollectionTypes | BaseTypes | Ref | RefUnwrapBailTypes[keyof RefUnwrapBailTypes] | {
[RawSymbol]?: true;
} ? T : T extends Array<any> ? {
[K in keyof T]: UnwrapRefSimple<T[K]>;
} : T extends object & {
[ShallowReactiveMarker]?: never;
} ? {
[P in keyof T]: P extends symbol ? T[P] : UnwrapRef<T[P]>;
} : T;
export declare type UnwrapRef<T> = T extends ShallowRef<infer V> ? V : T extends Ref<infer V> ? UnwrapRefSimple<V> : UnwrapRefSimple<T>;
我们通过代入下面的类型 Form,来分析 reactive<Form> 会返回什么?
interface Form {
id: string
name: Ref<string>
}
-
reactive类型判断一下泛型是否对象,调用UnwrapNestedRefs<Form> -
UnwrapNestedRefs类型判断Form是否Ref类型,不是直接调用UnwrapRefSimple<Form> -
UnwrapRefSimple<Form>类型里面的类型约束比较多,我们一个个看:
-
第一个看是否在
Function | CollectionTypes | BaseTypes | Ref | RefUnwrapBailTypes[keyof RefUnwrapBailTypes] | { [RawSymbol]?: true; }联合类型其中之一,结果否到下一个类型约束 -
第二个约束是否
Array<any>,结果否到下一个类型约束 -
第三个约束是否是
object & { [ShallowReactiveMarker]?: never; }这个交叉类型,Form是满足object类型的,而[ShallowReactiveMarker]是一个可选属性,所以满足 -
满足调用的是
{ [P in keyof Form]: P extends symbol ? Form[P] : UnwrapRef<Form[P]>; },in keyof的意思遍历Form所有的键,每次的键定义为P然后判断是否symbol- 第一个键是
id,类型时string,判断不是symbol所以调用UnwrapRef<string>;string不满足ShallowRef<infer V>,也不满足Ref<infer V>,那就调用UnwrapRefSimple<string> - 你会发现又回到第三步了,没错这个就是类型的递归了!这次比较简单发现
string是BaseTypes,所以直接返回string,那么id的类型就还是string(饶了一圈结果是它本来的类型,因为这些类型不需要二次处理) - 第二键是
name,类型时Ref<string>,判断不是symbol所以调用UnwrapRef<Ref<string>>;Ref<string>不满足ShallowRef<infer V>,但满足Ref<infer V>,infer可以推断出传入类型的泛型V,那就是string了,结果调用UnwrapRefSimple<string>(注意这里类型变了,这个就是解包的过程);然后又调用UnwrapRefSimple<string>,最后结果就是name的类型就变成string
- 第一个键是
- 最后我们得出返回就是:
{
id: string
name: string
}
上面的类型源码用了很多高级用法,这里列举一下相关官方出处:
结论
很显然,reactive 函数会做一个隐式的解包操作,如果通过泛型来约束类型,可能在某些场合下带来一些类型的疑惑,所以不推荐使用。那么我们在日常的使用下,需要类型约束直接显示定义即可:
const form: Form = reactive({
id: '',
name
})
但如果你真的需要使用泛型,建议去细细品味源码类型一下做了什么,能够更好地规避类似的问题。