Make type narrowing for destructured discriminated unions work for more types
#59.657 aberto em 16 de ago. de 2024
Métricas do repositório
- Stars
- (48.455 stars)
- Métricas de merge de PR
- (Mesclagem média 6d 17h) (9 fundiu PRs em 30d)
Description
🔍 Search Terms
discriminant union, ref, control flow guard, type narrowing
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ Suggestion
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"
}
Treat types like Ref<T> as a discriminant property in a union or find a way to narrow the type of payload
📃 Motivating Example
This is a very common use case in the Vue Pinia state store library, millions of projects use this library and have code like
const store = useDataStore()
const { ready, payload } = storeToRefs(store)
If we can improve this type narrowing behavior, the narrowed payload type can helps developer write safer code than before
// 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
💻 Use Cases
The use cases is actually shown in the motivating example.
I've dig into the checker.ts for some time and here's my findings
getDiscriminantPropertyAccesscannot treatreadyas a discriminant property now because it needs to checkCheckFlags.Discriminantwhich impliesCheckFlags.HasLiteralType. It's a pretty strict check and as its name describes,Ref<T>has no chance to pass this check.- I'm not sure is it possible for relaxing the discriminate requirements but it seems to be a bad idea after some search. #29110 is what I found but it's a really old PR so maybe time changes now
- If we cannot solve it by using discriminant property narrowing, as a newbie to the typescript project, I just tried to debug the checker and have another idea
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
}
Can we use type predicates or assert function to add more information to payload's flow list? If it's possible, maybe we can do following steps while examine the payload
- check
payload's symbol, if its declaration is aBindingPattern - check the flow list for
payload, if the narroweddatais the initializer ofpayload's declaration - narrow the
payloadbased on the narroweddata - maybe it's gibberish but hope it helps