🔍 搜索词
discriminant union, ref, control flow guard, type narrowing
✅ 可实现性检查清单
- 这不会对现有的TypeScript/JavaScript代码造成破坏性的改变
- 这不会改变现有JavaScript代码的运行时行为
- 我们可以在不根据表达式的类型发射不同的JS的情况下实现这个功能
- 这不是一个运行时特性(例如库功能、带有JavaScript输出的非ECMAScript语法、新的JS语法糖等)
- 这不是要求添加一个新的实用类型: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- 这个特性将与我们设计目标的其他部分一致: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ 建议
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
,以下是我的发现:
- 现在
getDiscriminantPropertyAccess
不能将ready
作为 discriminant property 处理,因为它需要检查CheckFlags.Discriminant
,这意味着CheckFlags.HasLiteralType
没有通过检查。这是一个相当严格的检查,正如其名称所描述的那样,Ref<T>
没有通过这次检查的机会。 - 我不确定是否可以通过放松 discriminant 的要求来解决这个问题,但在进行一些搜索后,这似乎是一个坏主意。我找到了 Fix discriminant property check #29110,但那是一个非常旧的 PR,所以也许现在情况有所改变。
- 如果我们不能通过使用 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
时,可能可以执行以下步骤:
- 检查
payload
的符号,如果它的声明是一个BindingPattern
- 检查流列表中的
payload
,如果缩小后的data
是payload
声明的初始化器 - 根据缩小后的
payload
缩小data
- 也许这听起来像是废话,但希望它有所帮助
1条答案
按热度按时间3zwjbxry1#
我正在研究"替代方案",旨在使用类型预测或Assert函数来缩小解构变量的类型。
我的发现:
现在我可以检查初始化器是
toRefs(data)
,同时检查标识符ready
。在ready
的流列表中,不难推断data
在isDataPrepared(data)
中的参数是DataPrepared
。我的想法是在
narrowTypeByCallExpression
中添加更多逻辑,而reference
是一个调用表达式,其参数与callExpression
有重叠,我们可以重新计算reference
的返回类型。所以我当前的问题是:是否有可能使用给定的参数类型
data
缩小 CallExpressiontoRefs(data)
?我已经检查了checkCallExpression
,参数类型缩小似乎只发生在标识符的流中,似乎无法指定流到checkCallExpression
并告诉它使用该流来缩小类型。希望在这里能得到一些建议,谢谢
更新:找到了这个文档 https://github.com/microsoft/TypeScript/wiki/Reference-Checker-Inference#type-parameter-inference