You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Function arg type inference (incorrectly) unions with null but excludes others #49850
constmatchStrict=<T>(test: (x: unknown)=>x is T)=>{return(...options: Array<T|undefined>): T|undefined=>{returnoptions.find((o)=>test(o));};};constmatchLoose=<T>(test: (x: unknown)=>x is T,
...options: Array<T|undefined>): T|undefined=>{returnoptions.find((o)=>test(o));};constisStr=(x: unknown): x is string=>{returntypeofx==='string';};// Works as expectedmatchStrict(isStr)(123,'a');// ✅ 'number' is not assignablematchStrict(isStr)(null,'a');// ✅ 'null' is not assignable// Inconsistent behaviormatchLoose(isStr,123,'a');// ✅ 'number' is not assignablematchLoose(isStr,null,'a');// ⛔️ WTF! matchLoose<string | null>matchLoose<string>(isStr,null,'a');// ✅ 'null' is not assignable
I expect matchLoose(isStr, null) and matchLoose(isStr, 123) to both throw errors, because isStr narrows <T> strictly. (I expect the intersection to work regardless of argument order too.)
A workaround could be using the type of type guard function as generic:
functionmatchLoose<TextendsFunction>(test: T, ...options: Textends(x: unknown)=>x is infer R ? Array<R> : never){returnoptions.find((o)=>test(o));}functionisStr(x: unknown): x is string{returntypeofx==='string';}matchLoose(isStr,123,'a');// Error as expectedmatchLoose(isStr,null,'a');// Error as expected
Seems like a duplicate of #14829. You don't want the options parameter to be used as an inference site for T.
TypeScript has some heuristics for accepting/rejecting/combining inference candidates; string from one and number from another tends to be rejected, but string from one and null from another tends to be accepted and combined via union. (See SO question, #19596, #44312, and probably a zillion other places I can't find right now).
Workarounds here usually involve lowering the priority of the options inference site, or otherwise blocking the inference. Your curried function is one way to do it, since T is already specified by the time the returned function is called. You can also define a NoInfer<T> utility and write the type of options as Array<NoInfer<T> | undefined>. Possibilities are listed in #14829. You can try type NoInfer<T> = T & {} or type NoInfer<T> = [T][T extends unknown ? 0 : 1] and see if that works for your use cases.
Activity
whzx5byb commentedon Jul 10, 2022
A workaround could be using the type of type guard function as generic:
jcalz commentedon Jul 11, 2022
Seems like a duplicate of #14829. You don't want the
optionsparameter to be used as an inference site forT.TypeScript has some heuristics for accepting/rejecting/combining inference candidates;
stringfrom one andnumberfrom another tends to be rejected, butstringfrom one andnullfrom another tends to be accepted and combined via union. (See SO question, #19596, #44312, and probably a zillion other places I can't find right now).Workarounds here usually involve lowering the priority of the
optionsinference site, or otherwise blocking the inference. Your curried function is one way to do it, sinceTis already specified by the time the returned function is called. You can also define aNoInfer<T>utility and write the type ofoptionsasArray<NoInfer<T> | undefined>. Possibilities are listed in #14829. You can trytype NoInfer<T> = T & {}ortype NoInfer<T> = [T][T extends unknown ? 0 : 1]and see if that works for your use cases.mikestopcontinues commentedon Jul 11, 2022
Thanks Joe Calzaretta (@jcalz), good tips!
mikestopcontinues commentedon Jul 11, 2022
For other who arrive here, I found this to be an excellent solution:
typescript-bot commentedon Jul 13, 2022
This issue has been marked as a 'Duplicate' and has seen no recent activity. It has been automatically closed for house-keeping purposes.