-
Notifications
You must be signed in to change notification settings - Fork 12.7k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Referencing ReturnType<FooT> in generic param, and passing a function that has one param causes incorrect type inference... #29133
Comments
Shorter repro, //Innocent enough.
//Returns a T
type Foo<T> = (...args : any[]) => T;
//=== NOT using ReturnType<FooT> in Generic Parameters ==
declare function bar<
FooT extends Foo<3|5>
>(foo: FooT): ReturnType<FooT>
//OK! `barResult` is of type `3`
const barResult = bar(
() => 3
);
//OK! `barResult2` is of type `3`
const barResult2 = bar(
//`t` is of type `3|5`
//Return type is 3
t => 3
);
//=== Using ReturnType<FooT> in Generic Parameters ==
declare function baz<
FooT extends Foo<3|5>,
//The same as bar<> but we reference ReturnType<FooT> here
Ret extends ReturnType<FooT>
>(foo: FooT, ret: Ret): ReturnType<FooT>
//OK! `bazResult` is of type `3`
const bazResult = baz(
() => 3,
3
);
//NOT OK! `bazResult2` is of type `3|5`!
const bazResult2 = baz(
//`t` is of type `3|5`
//Return type is `3`
t => 3,
3
);
//NOT OK! `bazResult3` is of type `3|5`!
const bazResult3 = baz(
//`t` is of type `3|5`
//Return type is `3`
//We *force* the return type to be 3
(t) : 3 => (3 as 3),
3 as 3
); |
This is a different repro, It doesn't use //Innocent enough.
//Returns a T
type Foo<T> = (...args : any[]) => T;
//=== NOT using ReturnType<FooT> in Generic Parameters ==
declare function bar<
FooT extends Foo<3|5>
>(foo: FooT): ReturnType<FooT>
//OK! `barResult` is of type `3`
const barResult = bar(
() => 3
);
//OK! `barResult2` is of type `3`
const barResult2 = bar(
//`t` is of type `3|5`
//Return type is 3
t => 3
);
//=== Using ReturnType<FooT> in Generic Parameters ==
declare function baz<
FooT extends Foo<3|5>,
>(
foo: FooT & (
ReturnType<FooT> extends 3 ?
unknown :
[
"Only 3 allowed, received",
ReturnType<FooT>
]
)
): ReturnType<FooT>
//OK! `bazResult` is of type `3`
const bazResult = baz(
() => 3
);
//NOT OK! `bazResult2` is of type `3|5`!
const bazResult2 = 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`!
const bazResult3 = 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 => (3 as 3)
); Of note is 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 For one, calling the function would still compile if I choose to return 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 |
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. //OK! `bazResult2` is of type `3`
const bazResult2 = baz(
(t : number) => 3
);
//OK! `bazResult3` is of type `3`
const bazResult3 = baz(
(t : number) : 3 => (3 as 3)
); 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. |
I've gotten TS to infer the return type of a generic function correctly, only in one part of my code. 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. class C<T> {
constructor(readonly t : T) {
}
baz<FooT extends ((t : T) => (T | undefined))>(
foo: FooT & (
ReturnType<FooT> extends 1|2?
["1|2 is not allowed, received", ReturnType<FooT>] :
unknown
)
) : C<ReturnType<FooT>> {
return new C(foo(this.t) as ReturnType<FooT>);
}
}
const c = new C(1 as 1|2|3);
const c2 = c.baz(() => undefined);
const c3 = c.baz(() => 3);
//'() => 2' is missing the following properties from type '["1|2 is not allowed, received", 2]'
const c4 = c.baz(() => 2);
//'(1|2|3) => 2' is missing the following properties from type '["1|2 is not allowed, received", 2]'
const c5 = c.baz(t => 2);
//'() => 1' is missing the following properties from type '["1|2 is not allowed, received", 1]'
const c6 = c.baz(() => 1);
//'(1|2|3) => 1' is missing the following properties from type '["1|2 is not allowed, received", 1]'
const c7 = c.baz(t => 1); |
Seems like classes really do run on different rules. class C<T> {
constructor(readonly t : T) {
}
}
function baz<
CType extends C<any>,
FooT extends ((t: CType["t"]) => (CType["t"] | undefined)
)>(
c : CType,
foo: FooT & (
ReturnType<FooT> extends 1|2?
["1|2 is not allowed, received", ReturnType<FooT>] :
unknown
)
) : C<ReturnType<FooT>> {
return new C(foo(c.t) as ReturnType<FooT>);
}
const c = new C(1 as 1|2|3);
const c2 = baz(c, () => undefined);
const c2b = baz(c, t => undefined);
const c3 = baz(c, () => 3 as 3);
const c3b = baz(c, t => 3 as 3);
//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]'
const c4 = baz(c, () => 2 as 2);
//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]'
const c5 = baz(c, t => 2 as 2);
//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]'
const c6 = baz(c, () => 1 as 1);
//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]'
const c7 = baz(c, t => 1 as 1); |
Okay. So, not all classes/interfaces are different. A bunch of my own code uses classes/interfaces and functions and |
So, I decided to play with I ran the above code samples, and only this one still fails, //Innocent enough.
//Returns a T
type Foo<T> = (...args : any[]) => T;
//=== NOT using ReturnType<FooT> in Generic Parameters ==
declare function bar<
FooT extends Foo<3|5>
>(foo: FooT): ReturnType<FooT>
//OK! `barResult` is of type `3`
const barResult = bar(
() => 3
);
//OK! `barResult2` is of type `3`
const barResult2 = bar(
//`t` is of type `3|5`
//Return type is 3
t => 3
);
//=== Using ReturnType<FooT> in Generic Parameters ==
declare function baz<
FooT extends Foo<3|5>,
>(
foo: FooT & (
ReturnType<FooT> extends 3 ?
unknown :
[
"Only 3 allowed, received",
ReturnType<FooT>
]
)
): ReturnType<FooT>
//OK! `bazResult` is of type `3`
const bazResult = baz(
() => 3
);
//NOT OK! `bazResult2` is of type `3|5`!
const bazResult2 = baz(
//Return type is `3`
//Error, ["Only 3 allowed, received", 3 | 5]
t => 3
);
//NOT OK! `bazResult3` is of type `3|5`!
const bazResult3 = baz(
//Return type is `3`
//We *force* the return type to be 3
//Error, ["Only 3 allowed, received", 3 | 5]
(t) : 3 => (3 as 3)
); 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 |
I've been playing with this a bit more and a cumbersome workaround is, //Innocent enough.
//Returns a T
type Foo<T> = (...args : any[]) => T;
//=== Using ReturnType<FooT> in Generic Parameters ==
declare function baz<
FooT extends Foo<3|5>,
>(
foo: FooT
): (
(this: (
ReturnType<FooT> extends 3 ?
unknown :
[
"Only 3 allowed, received",
ReturnType<FooT>
]
)) => ReturnType<FooT>
)
//OK! `bazResult` is of type `3`
const bazResult = baz(
() => 3
);
//OK! `bazResult2` is of type `3`
const bazResult2 = baz(
t => 3
)();
//OK! `bazResult3` is of type `3`
const bazResult3 = baz(
(t) : 3 => (3 as 3)
)();
/*
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]'.
*/
const bazResult4 = baz(
t => 5
)(); Since We make the return type a function that uses It's cumbersome because we now have to perform an extra function call,
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 |
@jack-williams |
@AnyhowStep please log a concise issue if needed. Thanks! |
TypeScript Version: 3.3.0-dev.20181129
Search Terms: ReturnType, Generic Param, Generic Function Argument
Code
Expected behavior:
bazResult2
andbazResult3
should be of type3
, the same asbazResult
Actual behavior:
bazResult2
andbazResult3
are of type3|5
, different frombazResult
(which is of type3
)The difference between
bazResult
andbazResult2
is literally 3 characters but it causes the inferred types to be very different.Playground Link: Here
I don't even know how to describe this...
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, butt => 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 constraintFooT
has parameters (Liket => 3
has parameters,() => 3
does not)If either of the above are false, this problem does not occur
The text was updated successfully, but these errors were encountered: