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

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

🔍 搜索词

discriminant union, ref, control flow guard, type narrowing

✅ 可实现性检查清单

⭐ 建议

  1. type Ref<T> = { value: T }
  2. type Data =
  3. | { ready: Ref<true>, payload: string }
  4. | { ready: Ref<false>, payload: null }
  5. declare const data: Data
  6. const { ready, payload } = data
  7. if (ready.value) {
  8. payload // <== currently inferred as "string | null" but should be "string"
  9. }

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

📃 动机示例

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

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

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

  1. // before, Non-null assertion everywhere
  2. if (ready.value) {
  3. payload.xxxx() // <=== false alert raised by typescript and developers have to use ?. or ! to avoid it
  4. payload?.xxxx() // <=== ?. is unnecessary, generates dead code and brings cognitive confusion
  5. xxxxx(payload!)
  6. }
  7. xxxxx(payload!) // <=== copied from the if block and forget to remove the ! mark, cannot receive alert from typescript
  8. // after, everything works fine
  9. if (ready.value) {
  10. payload.xxxx()
  11. xxxxx(payload)
  12. }
  13. 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 项目还不太熟悉的新手,我会尝试调试检查器并提出另一个想法。
  1. interface Ref<T> { value: T }
  2. type ToRefs<T> = { [K in keyof T]: Ref<T[K]> }
  3. function toRefs<T>(o: T): ToRefs<T> {
  4. return {} as any
  5. }
  6. interface DataPrepared {
  7. ready: true
  8. payload: string
  9. }
  10. interface DataNotPrepared {
  11. ready: false
  12. payload: null
  13. }
  14. type Data = DataPrepared | DataNotPrepared
  15. declare const data: Data
  16. const { ready, payload } = toRefs(data)
  17. function isDataReady(d: Data): d is DataPrepared {
  18. return d.ready.value
  19. }
  20. if (isDataReady(data)) {
  21. ready.value // <=== inferred as boolean but should be true
  22. payload.value // <=== inferred as "string | null" but should be string
  23. }
  24. function assertDataReady(d: Data): asserts d is DataPrepared {}
  25. if (ready.value) {
  26. assertDataReady(data)
  27. ready.value // <=== inferred as true which is expected but it's narrowed by other code path
  28. payload.value // <=== inferred as "string | null" but should be string
  29. }

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

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

3zwjbxry1#

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

  1. declare const data: Data
  2. const { ready, payload } = toRefs(data)
  3. if (isDataPrepared(data): data is DataPrepared) {
  4. ready.value
  5. // ^ narrow |ready| based on asserted |data| because it shares same symbol with argument |data| in |toRefs|
  6. }

现在我可以检查初始化器是 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

展开查看全部

相关问题