Repository navigation
Return type inference breaks in function parameter; tooltip also inconsistent #32540
Description
TypeScript Version: 3.5.1
Search Terms:
conditional type, type arg, inference, return type, argument, parameter, generic, callback function
Code
This is literally the minimum repro I could make and it's still too big.
export interface CompileError<_ErrorMessageT extends any[]> {
/**
* There should never be a value of this type
*/
readonly __compileError : never;
}
/**
* Each `string` element represents a column name.
*
* A "key" is a set of columns that uniquely identifies
* a row in a table.
*/
export type Key = readonly string[];
export type ExtractSubKey<
A extends Key,
B extends Key
> = (
A extends Key ?
(
B extends Key ?
(
A[number] extends B[number] ?
A :
never
) :
never
) :
never
);
export type ExtractSuperKey<
A extends Key,
B extends Key
> = (
A extends Key ?
(
B extends Key ?
(
B[number] extends A[number] ?
A :
never
) :
never
) :
never
);
export type FindSubKey<
ArrT extends readonly Key[],
KeyT extends Key
> = (
ExtractSubKey<
ArrT[number],
KeyT
>
);
export type FindSuperKey<
ArrT extends readonly Key[],
KeyT extends Key
> = (
ExtractSuperKey<
ArrT[number],
KeyT
>
);
declare function noSubKey<KeyT extends Key> (
arg : (
(c : {
x : "x",
y : "y",
z : "z"
}) => KeyT &
(
FindSuperKey<
(("x"|"y")[])[],
KeyT
> extends never ?
unknown :
CompileError<[
KeyT,
"is a sub key of",
FindSuperKey<
(("x"|"y")[])[],
KeyT
>
]>
)
)
) : void
//OK!
//Expected: Infer KeyT as "z"[]
//Actual : Infer KeyT as "z"[]
//Tooltip : noSubKey<"z"[]>
noSubKey(c => [c.z])
//OK!
//Expected: CompileError<["x"[], "is a sub key of", ("x" | "y")[]]>
//Actual : CompileError<["x"[], "is a sub key of", ("x" | "y")[]]>
//Error!
//Tooltip : noSubKey<readonly string[]>
noSubKey(c => [c.x])
//OK!
//Expected: CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
//Actual : CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
//Error!
//Tooltip : noSubKey<readonly string[]>
noSubKey(c => [c.x, c.y])
//OK!
//Expected: Infer KeyT as "z"[]
//Actual : Infer KeyT as "z"[]
//Tooltip : noSubKey<"z"[]>
noSubKey(() => ["z" as "z"]);
//OK!
//Expected: CompileError<["x"[], "is a sub key of", ("x" | "y")[]]>
//Actual : CompileError<["x"[], "is a sub key of", ("x" | "y")[]]>
//Tooltip : noSubKey<"x"[]>
noSubKey(() => ["x" as "x"]);
//OK!
//Expected: CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
//Actual : CompileError<[("x" | "y")[], "is a sub key of", ("x" | "y")[]]>
//Tooltip : noSubKey<("x" | "y")[]>
noSubKey(() => ["x" as "x", "y" as "y"]);
declare function noSuperKey<KeyT extends Key> (
arg : (
((c : {
x : "x",
y : "y",
z : "z"
}) => KeyT) &
(
FindSubKey<
(("x"|"y")[])[],
KeyT
> extends never ?
unknown :
CompileError<[
KeyT,
"is a super key of",
FindSubKey<
(("x"|"y")[])[],
KeyT
>
]>
)
)
) : void
//Error!
//Expected: Infer KeyT as "z"[]
//Actual : CompileError<[readonly string[], "is a super key of", ("x" | "y")[]]>
//Tooltip : noSuperKey<readonly string[]>
noSuperKey(c => [c.z])
//Error!
//Expected: Infer KeyT as "x"[]
//Actual : CompileError<[readonly string[], "is a super key of", ("x" | "y")[]]>
//Tooltip : noSuperKey<readonly string[]>
noSuperKey(c => [c.x])
//Error!
//Expected: CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
//Actual : CompileError<[readonly string[], "is a super key of", ("x" | "y")[]]>
//Tooltip : noSuperKey<readonly string[]>
noSuperKey(c => [c.x, c.y])
//OK!
//Expected: Infer KeyT as "z"[]
//Actual : Infer KeyT as "z"[]
//Tooltip : noSuperKey<"z"[]>
noSuperKey(() => ["z" as "z"]);
//OK!
//Expected: Infer KeyT as "x"[]
//Actual : Infer KeyT as "x"[]
//Tooltip : noSuperKey<"x"[]>
noSuperKey(() => ["x" as "x"]);
//OK!
//Expected: CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
//Actual : CompileError<[("x" | "y")[], "is a super key of", ("x" | "y")[]]>
//Tooltip : noSuperKey<("x" | "y")[]>
noSuperKey(() => ["x" as "x", "y" as "y"]);
/**
* Seems weird that using the `c` argument results in inference problems.
* But using string literals without the `c` argument is okay.
*/
Expected behavior:
Whether I use the c argument or not when calling noSubKey()/noSuperKey(),
it should always infer the type of KeyT correctly for all cases.
Actual behavior:
noSubKey()
-
With
cargument, it correctly infersKeyTin the error message -
With
cargument, it incorrectly infersKeyTin the tooltip -
With string literals, it correctly infers
KeyTin the error message -
With string literals, it correctly infers
KeyTin the tooltip
noSuperKey()
-
With
cargument, it incorrectly infersKeyTin the error message -
With
cargument, it incorrectly infersKeyTin the tooltip -
With string literals, it correctly infers
KeyTin the error message -
With string literals, it correctly infers
KeyTin the tooltip
Playground Link:
Related Issues:
Off the top of my head, I've made similar reports before and other similar repro cases,
From my personal experience, it feels like the moment you are using the ReturnType<> of a function in a conditional type (directly or indirectly) inside a parameter, you end up having weird issues with inference.
For the longest time, I've been trying to find a solid workaround but nothing seems to quite stick.
Every workaround I can come up with will work in some situation but not in others.
You'll notice that, in this repro, I never even use ReturnType<> on FunctionT.
I use KeyT and make the parameter (c) => KeyT and it works in some cases but breaks in this case for noSuperKey() and works (mostly) fine for noSubKey()
Activity
AnyhowStep commentedon Jul 24, 2019
What's extra weird to me is that
c.xis literally (pun, ha) of the literal type"x".But using
"x" as "x"is somehow more "kosher".AnyhowStep commentedon Jul 24, 2019
Also, if you move
coutside of the function params... It works fine again,Playground
AnyhowStep commentedon Jul 24, 2019
So,
"x" as "x"and globalc.xis okay with inference.Function arg
c.xis bad for inference.AnyhowStep commentedon Jul 24, 2019
In my particular use case, I'm writing a fluent API for a builder. Each method returns a new instance of the builder.
I'm using callback functions and checking the return type because there are some complicated constraints I need to enforce during compile time
AnyhowStep commentedon Jul 25, 2019
Changing the types to,
does not affect the results.
So, smaller repro,
Playground
AnyhowStep commentedon Jul 25, 2019
It seems like moving the conditional type to the
thisparam doesn't work either,Still works for
noSubKey(), thoughPlayground
If you hover over the inferred type of the callback functions, it seems to "know" the correct type.
It's just that the conditional type inside the param and the type param gets all confused.
Adding an explicit cast to the type that TS already knows seems to "fix" the inference.
Playground
AnyhowStep commentedon Jul 25, 2019
It looks like the order of
AandBmatters inExtractSubKey<>andExtractSuperKey<>.They are almost exactly the same; it's just that one is
A[number] extends B[number], the other isB[number] extends A[number]This gives problems,
This does not, (not as much)
AnyhowStep commentedon Jul 25, 2019
Uhhhh..... Wat?
Changing the implementation of
ExtractSubKey<>andExtractSuperKey<>to this makes both work (reasonably) well,I think I'm going crazy because all I did was shift types around (out of desperation, lol) and it works now... And I have no idea why one implementation works better than the other.
Also, this implementation is waywayway more complicated than the original implementation.... But somehow works.
I only tried this convoluted implementation because I wanted to do something different.
I wanted to stir the primordial soup of types and see if anything would evolve and survive the harsh world of the TS inference mechanism.
I give this implementation my blessing. May it go forth and procreate.
It is the fittest of the implementations and may live in my codebase.
There are still problems with the tooltip and it concerns me.
Full repro and details comments about what is expected and what actually happens,
Playground
AnyhowStep commentedon Jul 25, 2019
I re-introduced the conditional types in
ExtractSubKey<>andExtractSuperKey<>to distribute the types ofAandBand it still works (apart from tooltips generating the wrong types in some cases),Playground
With this implementation,
noSubKey()With
cargument, it correctly infersKeyTin the error messageWith
cargument, it incorrectly infersKeyTin the tooltipWith string literals, it correctly infers
KeyTin the error messageWith string literals, it correctly infers
KeyTin the tooltipnoSuperKey()With
cargument, it correctly infersKeyTin the error messageWith
cargument, it incorrectly infersKeyTin the tooltipWith string literals, it correctly infers
KeyTin the error messageWith string literals, it correctly infers
KeyTin the tooltip[-]Return type inference breaks in function parameter[/-][+]Return type inference breaks in function parameter; tooltip also inconsistent[/+]AnyhowStep commentedon Jul 25, 2019
I tried changing,
() => KeyTKeyT extends Keyto,
D extends SomeDelegateReturnType<D>It gives the same result as
KeyT,Playground
This is super weird to me because I have been under the impression that
ReturnType<>is broken when used in function parameters for the longest time. I've even filed issues about it and mentioned it in comments elsewhere with examples of how it seems to be broken.However, in my library, I could find examples of it working in some cases and not working in other cases. They were all such complicated examples that I could never find a small repro.
This is the first time I've ever actually found a decently small repro and found evidence that it isn't a
ReturnType<DelegateT> vs. ReturnTproblem. It's just that some generic types aren't inference-friendly.As an anecdote, I had many attempts at (relatively) simple things using
ReturnType<DelegateT>in function params and they failed really bad.However, there was one single attempt that worked.
It was the type-level equivalent of,
A type-level nested for-loop over a generic array type! And the actual type was an intersection of at least four other more complicated types.
I wanted to be consistent with the rest of my codebase and refactor it to not use
ReturnType<DelegateT>. However, it was so complex and scary and worked that I didn't dare to touch it.So, while everything else abandoned
ReturnType<DelegateT>in function params, that one function continued to have it. It was the only thing that defied my intuition thatReturnType<DelegateT>in param = badAnyhowStep commentedon Jul 25, 2019
I needed a version that was
subKeyOnly()andsuperKeyOnly().I tried a modified version what I posted above and... It doesn't work.
Playground
As usual, string literals are okay, but using function args breaks.
While the inferred type of
KeyTis broken by simply switching the types in thetrueandfalsebranches of the conditional type, it seems that the tooltip display for the type ofthisis consistent, at least.It shows
CompileError<>instead ofanyAnyhowStep commentedon Jul 25, 2019
By reverting back to this,
I get,
Playground
Guess I gotta' figure out how to make it work for this particular case.
AnyhowStep commentedon Jul 25, 2019
It seems like this implementation of
subKeyOnly()works,Playground
If I tweak it ever so slightly, it breaks,
(Can you spot the difference?)
Spoiler!
The first snippet is,(c : {/*snip*/}) => KeyT & (/*snip*/)The second snippet is,
((c : {/*snip*/}) => KeyT) & (/*snip*/)The difference is an extra parentheses.
Playground
AnyhowStep commentedon Jul 25, 2019
However, moving it to the
thisparam breaks it,Playground
Moving the callback's type to the inside of the conditional type breaks it,
Playground
RyanCavanaugh commentedon Jul 25, 2019
AnyhowStep Please unassign yourself when you reach a conclusion and provide a more actionable summary if possible - thanks! 😁
AnyhowStep commentedon Jul 25, 2019
Sorry for the wall of comments. I used it to track the myriad of different ways to do this one specific thing,
There are many ways to implement the above. We basically need to test the
ReturnType<>ofFunctionTinside offoo's parameter list.ReturnType<>satisfies a compile-time check, we letfbeFunctionTReturnType<>does not satisfy a compile-time check, we letfbenot FunctionTHowever, I have not been able to find the one-true-way to implement the above.
It seems like, no matter what attempt I use, it will work for some cases but completely break for other cases. And it is never intuitive why it works/does not work.
Getting something like the above to work has involved a lot of trial and error.
Generally, my attempts break when
FunctionTaccepts arguments and the concrete type ofFunctionTuses those arguments.I don't know why, though.
This has also been noted in a similar issue I created before, and I never found out why.
Using variables from outside
FunctionTor using literals works most of the time.For all the cases above where I say "this breaks", it seems to me like they are bugs.
Because they're all expressing the same thing I want, just with types shuffled around in hopes of appeasing TS' return type inference =x
I think I've finally found my Neo. I have found The One.
#32540 (comment)
I believe the one-true-way for it to work is this,
ReturnTsatisfies a compile-time check, we let the return type beReturnT & unknown = ReturnTReturnTdoes not satisfy a compile-time check, we let the return type beReturnT & CompileErrorHowever, I will need to test more with my code base.
When he is ready, I shall take him to see you (The Oracle)
And if he is The One, I'd like to know how he came to be The One
But I would like to know if all the other failures are bugs, or design limitations, or by design.
AnyhowStep commentedon Aug 11, 2019
After further experimentation, it seems like even my one-true-way attempt isn't perfect.
It works in most cases.
Except when you need to use the return type to create another type.
I'm going to call this other type a correlated type. Because it reminds me of a correlated subquery from SQL.
This comment shows that the supposed one-true-way (using
ReturnT) fails when used with a correlated type,#14829 (comment)
RyanCavanaugh commentedon Feb 24, 2024
Please log a new issue if any action is required on our side. Thanks!