Skip to content

Make type narrowing for destructured discriminated unions work for more types #59657

Description

@nekolab

🔍 Search Terms

discriminant union, ref, control flow guard, type narrowing

✅ Viability Checklist

⭐ Suggestion

typeRef<T>={value: T}typeData=|{ready: Ref<true>,payload: string}|{ready: Ref<false>,payload: null}declareconstdata: Dataconst{ ready, payload }=dataif(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

conststore=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 everywhereif(ready.value){payload.xxxx()// <=== false alert raised by typescript and developers have to use ?. or ! to avoid itpayload?.xxxx()// <=== ?. is unnecessary, generates dead code and brings cognitive confusionxxxxx(payload!)}xxxxx(payload!)// <=== copied from the if block and forget to remove the ! mark, cannot receive alert from typescript// after, everything works fineif(ready.value){payload.xxxx()xxxxx(payload)}xxxxx(payload)// received the null check protection from typescript

💻 Use Cases

More detailed playground link

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

  1. getDiscriminantPropertyAccess cannot treat ready as a discriminant property now because it needs to check CheckFlags.Discriminant which implies CheckFlags.HasLiteralType. It's a pretty strict check and as its name describes, Ref<T> has no chance to pass this check.
  2. I'm not sure is it possible for relaxing the discriminate requirements but it seems to be a bad idea after some search. Fix discriminant property check #29110 is what I found but it's a really old PR so maybe time changes now
  3. 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
interfaceRef<T>{value: T}typeToRefs<T>={[KinkeyofT]: Ref<T[K]>}functiontoRefs<T>(o: T): ToRefs<T>{return{}asany}interfaceDataPrepared{ready: truepayload: string}interfaceDataNotPrepared{ready: falsepayload: null}typeData=DataPrepared|DataNotPrepareddeclareconstdata: Dataconst{ ready, payload }=toRefs(data)functionisDataReady(d: Data): d is DataPrepared{returnd.ready.value}if(isDataReady(data)){ready.value// <=== inferred as boolean but should be truepayload.value// <=== inferred as "string | null" but should be string}functionassertDataReady(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 pathpayload.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

  1. check payload's symbol, if its declaration is a BindingPattern
  2. check the flow list for payload, if the narrowed data is the initializer of payload's declaration
  3. narrow the payload based on the narrowed data
  4. maybe it's gibberish but hope it helps

Metadata

Metadata

Assignees

No one assigned

    Labels

    Domain: check: Control FlowThe issue relates to control flow analysisHelp WantedYou can do thisPossible ImprovementThe current behavior isn't wrong, but it's possible to see that it might be better in some cases

    Type

    No type

    Projects

    No projects

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions