TypeScript Make type narrowing for destructured discriminated unions work for more types

ogsagwnx  于 4个月前  发布在  TypeScript
关注(0)|答案(1)|浏览(59)

🔍 搜索词

discriminant union, ref, control flow guard, type narrowing

✅ 可实现性检查清单

⭐ 建议

type Ref<T> = { value: T }
type Data =
    | { ready: Ref<true>, payload: string }
    | { ready: Ref<false>, payload: null }

declare const data: Data
const { ready, payload } = data

if (ready.value) {
    payload    // <== currently inferred as "string | null" but should be "string"
}

将类型 Ref<T> 像 discriminant property 一样在联合中处理,或者找到一种缩小 payload 类型的方法。

📃 动机示例

这是 Vue Pinia 状态存储库中的一个非常常见的用例,数百万个项目使用这个库,并且有类似以下的代码:

const store = useDataStore()
const { ready, payload } = storeToRefs(store)

如果我们能改进这种类型缩小的行为,缩小后的 payload 类型可以帮助开发者编写比以前更安全的代码。

// before, Non-null assertion everywhere
if (ready.value) {
    payload.xxxx()     // <=== false alert raised by typescript and developers have to use ?. or ! to avoid it
    payload?.xxxx()    // <=== ?. is unnecessary, generates dead code and brings cognitive confusion
    xxxxx(payload!)
}
xxxxx(payload!)        // <=== copied from the if block and forget to remove the ! mark, cannot receive alert from typescript

// after, everything works fine
if (ready.value) {
    payload.xxxx()
    xxxxx(payload)
}
xxxxx(payload)         // received the null check protection from typescript

💻 用例

更详细的Playground链接
实际上用例是在动机示例中展示的。
我花了一段时间研究 checker.ts,以下是我的发现:

  1. 现在 getDiscriminantPropertyAccess 不能将 ready 作为 discriminant property 处理,因为它需要检查 CheckFlags.Discriminant,这意味着 CheckFlags.HasLiteralType 没有通过检查。这是一个相当严格的检查,正如其名称所描述的那样,Ref<T> 没有通过这次检查的机会。
  2. 我不确定是否可以通过放松 discriminant 的要求来解决这个问题,但在进行一些搜索后,这似乎是一个坏主意。我找到了 Fix discriminant property check #29110,但那是一个非常旧的 PR,所以也许现在情况有所改变。
  3. 如果我们不能通过使用 discriminant property narrowing 来解决问题,作为一个对 TypeScript 项目还不太熟悉的新手,我会尝试调试检查器并提出另一个想法。
interface Ref<T> { value: T }
type ToRefs<T> = { [K in keyof T]: Ref<T[K]> }
function toRefs<T>(o: T): ToRefs<T> {
    return {} as any
}

interface DataPrepared {
    ready: true
    payload: string
}

interface DataNotPrepared {
    ready: false
    payload: null
}

type Data = DataPrepared | DataNotPrepared

declare const data: Data
const { ready, payload } = toRefs(data)

function isDataReady(d: Data): d is DataPrepared {
  return d.ready.value
}

if (isDataReady(data)) {
  ready.value     // <=== inferred as boolean but should be true
  payload.value   // <=== inferred as "string | null" but should be string
}

function assertDataReady(d: Data): asserts d is DataPrepared {}

if (ready.value) {
  assertDataReady(data)
  ready.value     // <=== inferred as true which is expected but it's narrowed by other code path
  payload.value   // <=== inferred as "string | null" but should be string
}

我们能否使用类型 predicate 或Assert函数向 payload 的流列表添加更多信息?如果可以的话,在我们检查 payload 时,可能可以执行以下步骤:

  1. 检查 payload 的符号,如果它的声明是一个 BindingPattern
  2. 检查流列表中的 payload ,如果缩小后的 datapayload 声明的初始化器
  3. 根据缩小后的 payload 缩小 data
  4. 也许这听起来像是废话,但希望它有所帮助
3zwjbxry

3zwjbxry1#

我正在研究"替代方案",旨在使用类型预测或Assert函数来缩小解构变量的类型。
我的发现:

declare const data: Data
const { ready, payload } = toRefs(data)

if (isDataPrepared(data): data is DataPrepared) {
    ready.value
    // ^ narrow |ready| based on asserted |data| because it shares same symbol with argument |data| in |toRefs|
}

现在我可以检查初始化器是 toRefs(data),同时检查标识符 ready。在 ready 的流列表中,不难推断 dataisDataPrepared(data) 中的参数是 DataPrepared
我的想法是在 narrowTypeByCallExpression 中添加更多逻辑,而 reference 是一个调用表达式,其参数与 callExpression 有重叠,我们可以重新计算 reference 的返回类型。
所以我当前的问题是:是否有可能使用给定的参数类型 data 缩小 CallExpression toRefs(data)?我已经检查了 checkCallExpression,参数类型缩小似乎只发生在标识符的流中,似乎无法指定流到 checkCallExpression 并告诉它使用该流来缩小类型。
希望在这里能得到一些建议,谢谢
更新:找到了这个文档 https://github.com/microsoft/TypeScript/wiki/Reference-Checker-Inference#type-parameter-inference

相关问题