Vue3 中创建 useHooks 遇到的一个 TS 泛型和 Reactive 响应式类型冲突的问题

TIP
不推荐使用 reactive() 的泛型参数,因为处理了深层次 ref 解包的返回值与泛型参数的类型不同。

这段时间总算是得空可以把早些时候快速开发时期挖的坑填一下了,所以准备着手改造一下前人遗留下来的 useTable 这个组合式函数。
期望可以在使用 useTable 的时候可以正确推导出来 tableDataqueryParams 对应的数据类型。

一开始的改造非常顺利,使用泛型可以正确的推导出来我需求的属性类型,直到我遇到了下面这两个TS告警:

Cannot assign type 'T[]' to type 'UnwrapRefSimple<T>[]'. ts(2322) 
不能将类型“T[]”分配给类型“UnwrapRefSimple<T>[]”。ts(2322)
Argument of type 'T' is not assignable to parameter of type 'UnwrapRefSimple<T>'.ts(2345)
类型“T”的参数不能赋给类型“UnwrapRefSimple<T>”的参数。ts(2345)

当然可以选择简单暴力的使用 as 来解决这个问题,但是我总觉得是不是有一些其他更好的方式来解决。


从原因上来看是因为:

The issue seems to stem from the fact that it is impossible for TypeScript to assert at compile time if the generic type T in UnwrapRefSimple<T> is going to be a Builtin, Ref, etc. which would affect the conditional type branching and thus generate a different type, so it leaves the UnwrapRefSimple<T> alone.

该问题似乎源于 TypeScript 无法在编译时断言泛型类型 TUnwrapRefSimple<T> 中是否会成为 BuiltinRef 等类型,这种类型判断会影响条件类型的分支选择进而生成不同的类型,因此编译器会保持 UnwrapRefSimple<T> 的原样不做处理。
Parameters of type ‘T’ cannot be assigned to parameters of type ‘UnwrapRefSimple‘. ts(2345) · Issue #13755 · vuejs/core

所以如果不需要保持 ref/reactive 深层响应来触发视图更新,那么可以使用 shallowRef/shallowReactive 来替换。这样就可以避免因为类型解包问题导致的类型不匹配。

以下是一个简易 Demo:

export interface TableOptionType<T = unknown, Q = Record<string, unknown>> {
  queryForm: Q,
  tableData: T[],
}

export function useList<
  T = unknown,
  Q = Record<string, unknown>
>(option: TableOptionType<T, Q>) {
  // const state = reactive<TableOptionType<T, Q>>(option);
  const state = shallowReactive<TableOptionType<T, Q>>(option); // 使用 shallowReactive 替换 reactive

  const setData = (data: T[]) => {
    state.tableData = data;
  };

  return {
    state,
    setData,
  };
}

但是我不确定项目中是否有一些特殊的业务做了表格行编辑功能,或者未来是否会增加这个功能。为了向后兼容和避免造成开发上的困扰,我没有选择使用 shallowRef/shallowReactive 来替换。

当前的处理方式

因为需要保持内部属性的深层响应式,所以暂时还是利用 as 断言来解决当前类型不匹配的问题。
但是仍然会考虑在未来继续调整。


🚧 其他

在查阅各种资料的时候,如果是简单的非嵌套对象,比如说下面这个 Demo

import { ref } from 'vue'

function test<T>() {
  const list = ref<T[]>([]);
  const li: T[] = [];
  // Argument of type 'T' is not assignable to parameter of type 'UnwrapRefSimple<T>'.ts(2345)
  list.value.push(...li);
}

可以调整类型声明的方式来解决 UnwrapRefSimple<T> 的类型问题:

import { ref } from 'vue'
import type { Ref } from 'vue'

function test<T>() {
  const list: Ref<T[]> = ref([]);
  const li: T[] = [];
  list.value.push(...li);
}

📚 相关资源