Feature request / Bug report
🔍 Search Terms
Contextual inference, generic call-site
✅ Viability Checklist
My suggestion meets these guidelines:
⭐ Suggestion / Problem
Let's first study contextual inference in different scenarios. Playground for code below. Highly recommended to view it in the playground so that you can see where are the red underlines, what is the inference and what are the completions.
typeX<T=never>={$: "foo"|T}declareconst$1: <Z>(x: Z)=>{$: Z}declareconst$2: <Z>(x: NoInfer<Z>)=>{$: Z}typeNoInfer<T>=[T][Textendsany ? 0 : never]let_: X;_=$1("")// s1t1 - Z is inferred as "" and does not compile ........................ ok_=$2("")// s1t2 - Z is inferred as "foo" and does not compile ..................... nice_=$1("foo")// s1t3 - Z is inferred as "foo" and compiles ............................. ok_=$2("foo")// s1t4 - Z is inferred as "foo" and compiles ............................. nicedeclareconstm1: (x: X)=>voidm1($1(""))// s2t1 - Z is inferred as "" and does not compile ........................ okm1($2(""))// s2t2 - Z is inferred as "foo" and does not compile ..................... nicem1($1("foo"))// s2t3 - Z is inferred as "foo" and compiles ............................. okm1($2("foo"))// s2t4 - Z is inferred as "foo" and compiles ............................. nicedeclareconstm2: <T>(x: InferStringLiteral<T>,_: X<NoInfer<T>>)=>voidtypeInferStringLiteral<T>=Textendsstring ? T : stringm2("a",$1(""))// s3t1 - Z is inferred as "" and does not compile ........................ okm2("a",$2(""))// s3t2 - Z is inferred as "foo" | "a" and does not compile ............... nicem2("a",$1("foo"))// s3t3 - Z is inferred as "foo" and compiles ............................. okm2("a",$2("a"))// s3t4 - Z is inferred as "foo" | "a" and compiles ....................... nicedeclareconstm3: (_: {x: X})=>voidm3({x: $1("")})// s4t1 - Z is inferred as "" and does not compile ........................ okm3({x: $2("")})// s4t2 - Z is inferred as "foo" and does not compile ..................... nicem3({x: $1("foo")})// s4t3 - Z is inferred as "foo" and compiles ............................. okm3({x: $2("foo")})// s4t4 - Z is inferred as "foo" and compiles ............................. nicedeclareconstm4: <Textends{a?: number,x: X<keyofT>}>(_: T)=>voidm4({x: $1(""),a: 1})// s5t1 - Z is inferred as "" and does not compile ........................ ok m4({x: $2(""),a: 1})// s5t2 - Z is inferred as string | number | symbol and does not compile... ugh// (quickinfo is incorrect see #44879)m4({x: $1("a"),a: 1})// s5t3 - Z is inferred as "a" and does not compile ....................... okm4({x: $2("a"),a: 1})// s5t4 - Z is inferred as string | number | symbol and does not compile .. ughhh// (quickinfo is incorrect see #44879)declareconstm5: <T,UextendsX<T>>(a: InferStringLiteral<T>,x: U)=>voidm5("a",$1(""))// s6t1 - Z is inferred as "" and does not compile ........................ ok m5("a",$2("foo"))// s6t2 - Z is inferred as unknown and does not compile.................... ugh// (quickinfo is incorrect see #44879)m5("a",$1("a"))// s6t3 - Z is inferred as "a" and does not compile ....................... okm5("a",$2("a"))// s6t4 - Z is inferred as unknown and does not compile ................... ughhh// (quickinfo is incorrect see #44879)The contextual inference is expected and consistent for s1, s2, s3 & s4. But weird yet consistent for s5 & s6.
I think the following should be the expected behavior...
s5t2 - Z is inferred as "foo" | "a" | "x" and does not compile
s5t4 - Z is inferred as "foo" | "a" | "x" and compiles
s6t2 - Z is inferred as "foo" | "a" and does not compile
s6t4 - Z is inferred as "foo" | "a" and compiles
In s5t4 & s6t4, "a" is one of the completions but they are incorrect because the language server infers the type parameter different from the compiler. This is most probably a bug which might be accommodated in #44879. So all in all the completions are useless and when the language server is fixed there would be no completions.
Generally speaking, a sugar-like abstraction should not result in compromises in developer experience. If you inline $*("") as { $: "" } in s5 & s6 then the completions and the compiler would work as expected.
My analysis is that when the type expected from calling $* is generic itself (ie s5 & s6) then compiler can't infer the type parameter Z correctly. It resolves all other generics to unknown hence the type parameter Z is inferred as keyof unknown and unknown in s5 and s6, respectively.
Aside: Though I would expected other generics to resolve to their constraint meaning I'd have expected (like not ultimately but considering the weirdness itself) the type parameter of $2 in the following scenario as "a" | "b" instead of unknown. Playground.
typeX<T=never>={$: T|"foo"}declareconstm6: <Textends"a"|"b",UextendsX<T>>(a: T,x: U)=>voidm6("a",$2("a"))// Z is inferred as unknowndeclareconst$2: <Z>(x: NoInfer<Z>)=>{$: Z}typeNoInfer<T>=[T][Textendsany ? 0 : never]#44999 is most probably the consequence of this weirdness as it fits the precondition and it's marked as a bug, so imo this should be also be considered as a bug. It's not apparent because the repros are vague but hopefully the following examples would make it more clear how essential it is to get this right.
Maybe $1should work same as $2
📃 Motivating Example
Library authors provide sugar-like abstractions all the time. Take the following as an example. Playground.
createColors({base: {primary: "red"},derived: {primary400: lightenWithNoInfer("primary",0.4),// does not compileprimary500: lightenWithoutNoInfer("primary",0.5),// does not compileprimary600: lightenWithoutNoInfer("primary"asconst,0.6),primary700: lightenWithoutNoInferWithInferStringLiteral("primary",0.7)}})declareconstcreateColors: <Cextendsstring>(theme:
{base: {[colorIdentifierinC]: Color},derived: {[derivedColorinstring]: {base: NoInfer<C>// `NoInfer` so that `C` is inferred only from `base`,operation: (c: Color)=>Color}}})=>"TODO"declareconstlightenWithNoInfer:
<Z>(base: NoInfer<Z>,weight: number)=>{base: Z,operation: (c: Color)=>Color}declareconstlightenWithoutNoInfer:
<Z>(base: Z,weight: number)=>{base: Z,operation: (c: Color)=>Color}declareconstlightenWithoutNoInferWithInferStringLiteral:
<Z>(base: InferStringLiteral<Z>,weight: number)=>{base: Z,operation: (c: Color)=>Color}typeColor=string;typeNoInfer<T>=[T][Textendsany ? 0 : never]typeInferStringLiteral<T>=Textendsstring ? T : neverNone of the lighten version is good.
lightenWithNoInfer has completions but are incorrect and doesn't compilelightenWithoutNoInfer infers Z as string instead of "primary" hence doesn't compilelightenWithoutNoInferWithInferStringLiteral compiles but has no completions
If I were to be frank, there is no rocket science going on here, lightenWithoutNoInfer (or at least lightenWithNoInfer) should "just work" with completions and compilation.
And the problem isn't about string literals per se. Here's another real world example from xstate. Playground.
createMachine({schema: {event: createSchema<{type: "FETCH"}>()},initial: "idle",states: {idle: {entry: send({type: "FETCH"}),// does not compileon: {FETCH: "fetching"}},fetching: {}}})declareconstcreateMachine: <Stateextendsstring,Eventextends{type: string}>(m:
{schema: {event: Event}// we want `Event` to be inferred only from here,initial: NoInfer<State>,states:
{[SinState]: // we want `State` to be inferred only from here{entry?: |{type: "xstate.send",event: NoInfer<Event>},on?:
{[EinEvent["type"]]?: NoInfer<State>}}}})=>voiddeclareconstsend:
<E>(event: NoInfer<E>)=>{type: "xstate.send",event: E}declareconstcreateSchema: <T>()=>TtypeNoInfer<T>=[T][Textendsany ? 0 : never];The problem exactly is same as above. And here you can even inline send({ type: "FETCH" }) to { type: "xstate.send", type: "FETCH" } and it compiles and even provides completions.
Aside: If we add { type: string } to the entry union send("")compiles but { type: "xstate.send", event: "" }doesn't, which is kinda weird too because both are equivalent.
💻 Use Cases
Any case where the type parameters of a function are to be inferred from the return type instead of parameters AND location of the function call is a generic; is a use case. I suspect this improvement/bugfix will have a huge impact especially for library authors. Probably there are some folks out there banging theirs heads to make the completions work when they should probably "just work" without having to do anything.
Some "workarounds"
For s5
declareconst$:
<R,ZextendsRextends{$: infer X} ? X : never>(x: Z)=>R&{$: Z}For lighten
declareconstlighten:
<R,CextendsRextends{base: infer X} ? X : never>(base: C,weight: number)=>&R&{base: C,operation: (c: Color)=>Color}For send
declareconstsend:
<R,EextendsRextends{event: infer X} ? X : never>(event: E)=>&R&{type: "xstate.send",event: E}All the above provide completions and compile too. Though complex cases like s4 don't work with this workaround.
Thanks for reading!
Feature request / Bug report
🔍 Search Terms
Contextual inference, generic call-site
✅ Viability Checklist
My suggestion meets these guidelines:
⭐ Suggestion / Problem
Let's first study contextual inference in different scenarios. Playground for code below. Highly recommended to view it in the playground so that you can see where are the red underlines, what is the inference and what are the completions.
The contextual inference is expected and consistent for s1, s2, s3 & s4. But weird yet consistent for s5 & s6.
I think the following should be the expected behavior...
In s5t4 & s6t4,
"a"is one of the completions but they are incorrect because the language server infers the type parameter different from the compiler. This is most probably a bug which might be accommodated in #44879. So all in all the completions are useless and when the language server is fixed there would be no completions.Generally speaking, a sugar-like abstraction should not result in compromises in developer experience. If you inline
$*("")as{ $: "" }in s5 & s6 then the completions and the compiler would work as expected.My analysis is that when the type expected from calling
$*is generic itself (ie s5 & s6) then compiler can't infer the type parameterZcorrectly. It resolves all other generics tounknownhence the type parameterZis inferred askeyof unknownandunknownin s5 and s6, respectively.Aside: Though I would expected other generics to resolve to their constraint meaning I'd have expected (like not ultimately but considering the weirdness itself) the type parameter of
$2in the following scenario as"a" | "b"instead ofunknown. Playground.#44999 is most probably the consequence of this weirdness as it fits the precondition and it's marked as a bug, so imo this should be also be considered as a bug. It's not apparent because the repros are vague but hopefully the following examples would make it more clear how essential it is to get this right.
Maybe
$1should work same as$2📃 Motivating Example
Library authors provide sugar-like abstractions all the time. Take the following as an example. Playground.
None of the
lightenversion is good.lightenWithNoInferhas completions but are incorrect and doesn't compilelightenWithoutNoInferinfersZasstringinstead of"primary"hence doesn't compilelightenWithoutNoInferWithInferStringLiteralcompiles but has no completionsIf I were to be frank, there is no rocket science going on here,
lightenWithoutNoInfer(or at leastlightenWithNoInfer) should "just work" with completions and compilation.And the problem isn't about string literals per se. Here's another real world example from xstate. Playground.
The problem exactly is same as above. And here you can even inline
send({ type: "FETCH" })to{ type: "xstate.send", type: "FETCH" }and it compiles and even provides completions.Aside: If we add
{ type: string }to theentryunionsend("")compiles but{ type: "xstate.send", event: "" }doesn't, which is kinda weird too because both are equivalent.💻 Use Cases
Any case where the type parameters of a function are to be inferred from the return type instead of parameters AND location of the function call is a generic; is a use case. I suspect this improvement/bugfix will have a huge impact especially for library authors. Probably there are some folks out there banging theirs heads to make the completions work when they should probably "just work" without having to do anything.
Some "workarounds"
For s5
For
lightenFor
sendAll the above provide completions and compile too. Though complex cases like s4 don't work with this workaround.
Thanks for reading!