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
Referencing ReturnType<FooT> in generic param, and passing a function that has one param causes incorrect type inference... #29133
Search Terms: ReturnType, Generic Param, Generic Function Argument
Code
//Innocent enough.//Take a T, return a TtypeFoo<Textendsnumber>=(t: T)=>T;//=== NOT using ReturnType<FooT> in Generic Parameters ==declarefunctionbar<Textendsnumber,FooTextendsFoo<T>>(t: T,foo: FooT): ReturnType<FooT>//OK! `barResult` is of type `3`constbarResult=bar(3as(3|5),()=>3);//OK! `barResult2` is of type `3`constbarResult2=bar(3as(3|5),//`t` is of type `3|5`//Return type is 3 t=>3);//=== Using ReturnType<FooT> in Generic Parameters ==declarefunctionbaz<Textendsnumber,FooTextendsFoo<T>,//The same as bar<> but we reference ReturnType<FooT> hereRetextendsReturnType<FooT>>(t: T,foo: FooT,ret: Ret): ReturnType<FooT>//OK! `bazResult` is of type `3`constbazResult=baz(3as(3|5),()=>3,3);//NOT OK! `bazResult2` is of type `3|5`!constbazResult2=baz(3as(3|5),//`t` is of type `3|5`//Return type is `3`t=>3,3);//NOT OK! `bazResult3` is of type `3|5`!constbazResult3=baz(3as(3|5),//`t` is of type `3|5`//Return type is `3`//We *force* the return type to be 3(t) : 3=>(3as3),3);
Expected behavior:
bazResult2 and bazResult3 should be of type 3, the same as bazResult
Actual behavior:
bazResult2 and bazResult3 are of type 3|5, different from bazResult (which is of type 3)
The difference between bazResult and bazResult2 is literally 3 characters but it causes the inferred types to be very different.
My real use case is more complicated than the above example but I need the return type of a generic function to be inferred correctly to perform stronger compile-time type checks.
The strange thing is that () => 3 will give me the correct return type, but t => 3 will not.
In my own code, using the argument t is absolutely necessary.
As far as I've seen, this problem only occurs both the following are true,
ReturnType<FooT> is used in a generic param constraint
The concrete type of FooT has parameters (Like t => 3 has parameters, () => 3 does not)
If either of the above are false, this problem does not occur
changed the title [-]Referencing ReturnType<Foo> in generic param causes incorrect type inference...[/-][+]Referencing ReturnType<FooT> in generic param causes incorrect type inference...[/+]on Dec 23, 2018
changed the title [-]Referencing ReturnType<FooT> in generic param causes incorrect type inference...[/-][+]Referencing ReturnType<FooT> in generic param, and passing a function that has one param causes incorrect type inference...[/+]on Dec 23, 2018
//Innocent enough.//Returns a TtypeFoo<T>=(...args : any[])=>T;//=== NOT using ReturnType<FooT> in Generic Parameters ==declarefunctionbar<FooTextendsFoo<3|5>>(foo: FooT): ReturnType<FooT>//OK! `barResult` is of type `3`constbarResult=bar(()=>3);//OK! `barResult2` is of type `3`constbarResult2=bar(//`t` is of type `3|5`//Return type is 3 t=>3);//=== Using ReturnType<FooT> in Generic Parameters ==declarefunctionbaz<FooTextendsFoo<3|5>,//The same as bar<> but we reference ReturnType<FooT> hereRetextendsReturnType<FooT>>(foo: FooT,ret: Ret): ReturnType<FooT>//OK! `bazResult` is of type `3`constbazResult=baz(()=>3,3);//NOT OK! `bazResult2` is of type `3|5`!constbazResult2=baz(//`t` is of type `3|5`//Return type is `3`t=>3,3);//NOT OK! `bazResult3` is of type `3|5`!constbazResult3=baz(//`t` is of type `3|5`//Return type is `3`//We *force* the return type to be 3(t) : 3=>(3as3),3as3);
It doesn't use ReturnType<FooT> in the generic params. It uses it in the function param.
//Innocent enough.//Returns a TtypeFoo<T>=(...args : any[])=>T;//=== NOT using ReturnType<FooT> in Generic Parameters ==declarefunctionbar<FooTextendsFoo<3|5>>(foo: FooT): ReturnType<FooT>//OK! `barResult` is of type `3`constbarResult=bar(()=>3);//OK! `barResult2` is of type `3`constbarResult2=bar(//`t` is of type `3|5`//Return type is 3 t=>3);//=== Using ReturnType<FooT> in Generic Parameters ==declarefunctionbaz<FooTextendsFoo<3|5>,>(foo: FooT&(ReturnType<FooT>extends3 ?
unknown :
["Only 3 allowed, received",ReturnType<FooT>])): ReturnType<FooT>//OK! `bazResult` is of type `3`constbazResult=baz(()=>3);//NOT OK! `bazResult2` is of type `3|5`!constbazResult2=baz(//`t` is of type `3|5`//Return type is `3`//Error, ["Only 3 allowed, received", 3 | 5]t=>3);//NOT OK! `bazResult3` is of type `3|5`!constbazResult3=baz(//`t` is of type `3|5`//Return type is `3`//We *force* the return type to be 3//Error, ["Only 3 allowed, received", 3 | 5](t) : 3=>(3as3));
Of note is bazResult3 where I went all-out and explicitly said that it returns 3 but the type checker still thinks it's 3|5
In this repro, the intention is compile-time error messages at the parameter that does not satisfy some potentially complicated constraint.
It could be re-written to have the checks in the return type and then returning never if the constraint fails but it loses the kind of safety I'm looking for.
For one, calling the function would still compile if I choose to return never.
What I want is for the function call itself to not compile if the constraint fails.
These kind of checks on parameters works for every other type I've tried. It's only functions that take arguments that fail. Functions that don't take arguments work fine.
Also, returning never when the constraint fails means no helpful error messages.
I've gotten TS to infer the return type of a generic function correctly, only in one part of my code.
I've been trying to dissect it and figure out what makes it work, when so many others fail.
Haven't made any headway, though. It's the most complicated of my types but it works for some reason ._.
After poking around a bit more, it seems to work fine with classes.
As in, the return type is inferred correctly.
classC<T>{constructor(readonlyt : T){}baz<FooTextends((t : T)=>(T|undefined))>(foo: FooT&(ReturnType<FooT>extends1|2?
["1|2 is not allowed, received",ReturnType<FooT>] :
unknown)) : C<ReturnType<FooT>>{returnnewC(foo(this.t)asReturnType<FooT>);}}constc=newC(1as1|2|3);constc2=c.baz(()=>undefined);constc3=c.baz(()=>3);//'() => 2' is missing the following properties from type '["1|2 is not allowed, received", 2]'constc4=c.baz(()=>2);//'(1|2|3) => 2' is missing the following properties from type '["1|2 is not allowed, received", 2]'constc5=c.baz(t=>2);//'() => 1' is missing the following properties from type '["1|2 is not allowed, received", 1]'constc6=c.baz(()=>1);//'(1|2|3) => 1' is missing the following properties from type '["1|2 is not allowed, received", 1]'constc7=c.baz(t=>1);
Seems like classes really do run on different rules.
classC<T>{constructor(readonlyt : T){}}functionbaz<CTypeextendsC<any>,FooTextends((t: CType["t"])=>(CType["t"]|undefined))>(c : CType,foo: FooT&(ReturnType<FooT>extends1|2?
["1|2 is not allowed, received",ReturnType<FooT>] :
unknown)) : C<ReturnType<FooT>>{returnnewC(foo(c.t)asReturnType<FooT>);}constc=newC(1as1|2|3);constc2=baz(c,()=>undefined);constc2b=baz(c,t=>undefined);constc3=baz(c,()=>3as3);constc3b=baz(c,t=>3as3);//Need to use `as 2` here but the error message is as expected//'() => 2' is missing the following properties from type '["1|2 is not allowed, received", 2]' constc4=baz(c,()=>2as2);//Need to use `as 2` here but the error message is as expected//'(1|2|3) => 2' is missing the following properties from type '["1|2 is not allowed, received", 2]'constc5=baz(c,t=>2as2);//Need to use `as 1` here but the error message is as expected//'() => 1' is missing the following properties from type '["1|2 is not allowed, received", 1]'constc6=baz(c,()=>1as1);//Need to use `as 1` here but the error message is as expected//'(1|2|3) => 1' is missing the following properties from type '["1|2 is not allowed, received", 1]'constc7=baz(c,t=>1as1);
Okay. So, not all classes/interfaces are different. A bunch of my own code uses classes/interfaces and functions and ReturnType<FooT> but it just doesn't work =/
I ran the above code samples, and only this one still fails,
//Innocent enough.//Returns a TtypeFoo<T>=(...args : any[])=>T;//=== NOT using ReturnType<FooT> in Generic Parameters ==declarefunctionbar<FooTextendsFoo<3|5>>(foo: FooT): ReturnType<FooT>//OK! `barResult` is of type `3`constbarResult=bar(()=>3);//OK! `barResult2` is of type `3`constbarResult2=bar(//`t` is of type `3|5`//Return type is 3 t=>3);//=== Using ReturnType<FooT> in Generic Parameters ==declarefunctionbaz<FooTextendsFoo<3|5>,>(foo: FooT&(ReturnType<FooT>extends3 ?
unknown :
["Only 3 allowed, received",ReturnType<FooT>])): ReturnType<FooT>//OK! `bazResult` is of type `3`constbazResult=baz(()=>3);//NOT OK! `bazResult2` is of type `3|5`!constbazResult2=baz(//Return type is `3`//Error, ["Only 3 allowed, received", 3 | 5]t=>3);//NOT OK! `bazResult3` is of type `3|5`!constbazResult3=baz(//Return type is `3`//We *force* the return type to be 3//Error, ["Only 3 allowed, received", 3 | 5](t) : 3=>(3as3));
I believe #30215 is what fixes most of whatever was error'ing before.
However, it would be nice if the above sample was fixed, too. I love putting error messages in parameters because it pinpoints the exact location of the problem.
TypeScript thinks the return type is 3|5, even when I explicitly say it is 3, this should never happen.
Even if I leave it implicit, it would be nice if it realized that the return type of t => 3 extends 3.
I've been playing with this a bit more and a cumbersome workaround is,
//Innocent enough.//Returns a TtypeFoo<T>=(...args : any[])=>T;//=== Using ReturnType<FooT> in Generic Parameters ==declarefunctionbaz<FooTextendsFoo<3|5>,>(foo: FooT): ((this: (ReturnType<FooT>extends3 ?
unknown :
["Only 3 allowed, received",ReturnType<FooT>]))=>ReturnType<FooT>)//OK! `bazResult` is of type `3`constbazResult=baz(()=>3);//OK! `bazResult2` is of type `3`constbazResult2=baz(t=>3)();//OK! `bazResult3` is of type `3`constbazResult3=baz((t) : 3=>(3as3))();/* OK! `bazResult4` is of type `5` Error as expected, The 'this' context of type 'void' is not assignable to method's 'this' of type '["Only 3 allowed, received", 5]'.*/constbazResult4=baz(t=>5)();
Since ReturnType<FooT> does not work when using it in the function parameter list, we move it to the return type annotation.
We make the return type a function that uses ReturnType<FooT> in its this parameter, that becomes an impossible to fulfill type when some condition is fulfilled.
It's cumbersome because we now have to perform an extra function call,
baz(t => result)() vs baz(t => result).
Also, the error message is now on the function call and not the parameter itself.
But the extra function call has the benefit of making the error message much shorter and easier to read.
The 'this' context of type 'void' is not assignable to
method's 'this' of type *Error Type*
vs, potentially,
Notice the length of the scroll bar on that tooltip... It's all just saying that one type is not assignable to another type.
But, of course, if we had some invalid type or type-level throw, as suggested in #23689 , then I may not need to go through all this
Activity
[-]Referencing ReturnType<Foo> in generic param causes incorrect type inference...[/-][+]Referencing ReturnType<FooT> in generic param causes incorrect type inference...[/+][-]Referencing ReturnType<FooT> in generic param causes incorrect type inference...[/-][+]Referencing ReturnType<FooT> in generic param, and passing a function that has one param causes incorrect type inference...[/+]AnyhowStep commentedon Dec 23, 2018
Shorter repro,
Playground
AnyhowStep commentedon Dec 23, 2018
This is a different repro,
It doesn't use
ReturnType<FooT>in the generic params. It uses it in the function param.Playground
Of note is
bazResult3where I went all-out and explicitly said that it returns3but the type checker still thinks it's3|5In this repro, the intention is compile-time error messages at the parameter that does not satisfy some potentially complicated constraint.
It could be re-written to have the checks in the return type and then returning
neverif the constraint fails but it loses the kind of safety I'm looking for.For one, calling the function would still compile if I choose to return
never.What I want is for the function call itself to not compile if the constraint fails.
These kind of checks on parameters works for every other type I've tried. It's only functions that take arguments that fail. Functions that don't take arguments work fine.
Also, returning
neverwhen the constraint fails means no helpful error messages.AnyhowStep commentedon Dec 31, 2018
Poked around a bit more and it looks like if I explicitly say what type the parameters are, it infers the return type just fine.
But in my actual use-case, the parameter type is pretty complicated because it depends on chaining many generic types, and it uses a builder pattern.
AnyhowStep commentedon Dec 31, 2018
I've gotten TS to infer the return type of a generic function correctly, only in one part of my code.
I've been trying to dissect it and figure out what makes it work, when so many others fail.
Haven't made any headway, though. It's the most complicated of my types but it works for some reason ._.
AnyhowStep commentedon Dec 31, 2018
After poking around a bit more, it seems to work fine with classes.
As in, the return type is inferred correctly.
AnyhowStep commentedon Dec 31, 2018
Seems like classes really do run on different rules.
AnyhowStep commentedon Dec 31, 2018
Okay. So, not all classes/interfaces are different. A bunch of my own code uses classes/interfaces and functions and
ReturnType<FooT>but it just doesn't work =/AnyhowStep commentedon May 24, 2019
So, I decided to play with
3.5.0-dev.20190523,I ran the above code samples, and only this one still fails,
Playground
I believe #30215 is what fixes most of whatever was error'ing before.
However, it would be nice if the above sample was fixed, too. I love putting error messages in parameters because it pinpoints the exact location of the problem.
TypeScript thinks the return type is
3|5, even when I explicitly say it is3, this should never happen.Even if I leave it implicit, it would be nice if it realized that the return type of
t => 3extends3.AnyhowStep commentedon May 25, 2019
I've been playing with this a bit more and a cumbersome workaround is,
Playground
Since
ReturnType<FooT>does not work when using it in the function parameter list, we move it to the return type annotation.We make the return type a function that uses
ReturnType<FooT>in itsthisparameter, that becomes an impossible to fulfill type when some condition is fulfilled.It's cumbersome because we now have to perform an extra function call,
baz(t => result)()vsbaz(t => result).Also, the error message is now on the function call and not the parameter itself.
But the extra function call has the benefit of making the error message much shorter and easier to read.
vs, potentially,
Notice the length of the scroll bar on that tooltip... It's all just saying that one type is not assignable to another type.
But, of course, if we had some invalid type or type-level throw, as suggested in #23689 , then I may not need to go through all this
AnyhowStep commentedon May 30, 2019
Jack Williams (@jack-williams)
What do you think of my last two comments?
RyanCavanaugh commentedon Aug 22, 2019
AnyhowStep please log a concise issue if needed. Thanks!