-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Return type inference breaks in function parameter; tooltip also inconsistent #32540
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
Comments
What's extra weird to me is that But using |
Also, if you move 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 FindSubKey<
ArrT extends readonly Key[],
KeyT extends Key
> = (
ExtractSubKey<
ArrT[number],
KeyT
>
);
/**
* Moved `c` to a global variable.
* `c` is no longer a function arg.
*/
declare const c : {
x : "x",
y : "y",
z : "z"
};
declare function noSuperKey<KeyT extends Key> (
arg : (
(() => KeyT) &
(
FindSubKey<
(("x"|"y")[])[],
KeyT
> extends never ?
unknown :
CompileError<[
KeyT,
"is a super key of",
FindSubKey<
(("x"|"y")[])[],
KeyT
>
]>
)
)
) : void
//OK!
//Expected: Infer KeyT as "z"[]
//Actual : Infer KeyT as "z"[]
//Tooltip : noSubKey<"z"[]>
noSuperKey(() => [c.z])
//OK!
//Expected: Infer KeyT as "x"[]
//Actual : Infer KeyT as "x"[]
//Tooltip : noSubKey<"x"[]>
noSuperKey(() => [c.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 : noSubKey<("x" | "y")[]>
noSuperKey(() => [c.x, c.y])
//OK!
//Expected: Infer KeyT as "z"[]
//Actual : Infer KeyT as "z"[]
//Tooltip : noSubKey<"z"[]>
noSuperKey(() => ["z" as "z"]);
//OK!
//Expected: Infer KeyT as "x"[]
//Actual : Infer KeyT as "x"[]
//Tooltip : noSubKey<"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 : noSubKey<("x" | "y")[]>
noSuperKey(() => ["x" as "x", "y" as "y"]); |
So, Function arg |
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 |
Changing the types to, export type ExtractSubKey<
A extends Key,
B extends Key
> = (
A[number] extends B[number] ?
A :
never
);
export type ExtractSuperKey<
A extends Key,
B extends Key
> = (
B[number] extends A[number] ?
A :
never
); does not affect the results. So, smaller repro, |
It seems like moving the conditional type to the declare function noSuperKey<KeyT extends Key> (
this : (
FindSubKey<
(("x"|"y")[])[],
KeyT
> extends never ?
any :
CompileError<[
KeyT,
"is a super key of",
FindSubKey<
(("x"|"y")[])[],
KeyT
>
]>
),
arg : (
((c : {
x : "x",
y : "y",
z : "z"
}) => KeyT)
)
) : void Still works for 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. //OK!
//Expected: Infer KeyT as "z"[]
//Actual : Infer KeyT as "z"[]
//Tooltip : noSuperKey<"z"[]>
//Inferred type of callback: (c: { x: "x", y: "y", z: "z" }) => "z"[]
noSuperKey(
(c => [c.z]) as (
//Add an explicit cast and it works
(c: { x: "x", y: "y", z: "z" }) => "z"[]
)
); |
It looks like the order of They are almost exactly the same; it's just that one is This gives problems, A[number] extends B[number] ? This does not, (not as much) B[number] extends A[number] ? |
Uhhhh..... Wat? Changing the implementation of //This works, I have no idea why.
export type ExtractSubKey<
A extends Key,
B extends Key
> = (
A[number] extends Extract<B[number], A[number]> ?
A :
never
);
export type ExtractSuperKey<
A extends Key,
B extends Key
> = (
B[number] extends Extract<A[number], B[number]> ?
A :
never
); 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 give this implementation my blessing. May it go forth and procreate. There are still problems with the tooltip and it concerns me. //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[]> <-- Expected ("x" | "y")[]
//Error!
//this : any <-- Expected CompileError<>
noSubKey(c => [c.x, c.y]) Full repro and details comments about what is expected and what actually happens, |
I re-introduced the conditional types in export type ExtractSubKey<
A extends Key,
B extends Key
> = (
A extends Key ?
(
B extends Key ?
(
A[number] extends Extract<B[number], A[number]> ?
A :
never
) :
never
) :
never
);
export type ExtractSuperKey<
A extends Key,
B extends Key
> = (
A extends Key ?
(
B extends Key ?
(
B[number] extends Extract<A[number], B[number]> ?
A :
never
) :
never
) :
never
); With this implementation,
|
I tried changing,
to,
It gives the same result as This is super weird to me because I have been under the impression that 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 As an anecdote, I had many attempts at (relatively) simple things using However, there was one single attempt that worked. It was the type-level equivalent of, for (let i=0; i<ArrT["length"]; ++i) {
for (let j=i+1; j<ArrT["length"]; ++j) {
/*snip complicated type using ArrT, i and j*/
}
} 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 So, while everything else abandoned |
I needed a version that was I tried a modified version what I posted above and... It doesn't work. //THIS IS BROKEN
declare function subKeyOnly<KeyT extends Key> (
this : (
FindSuperKey<
(("x"|"y")[])[],
KeyT
> extends never ?
CompileError<[
KeyT,
"is NOT a sub key of",
(("x"|"y")[])[]
]> :
any
),
arg : (
(c : {
x : "x",
y : "y",
z : "z"
}) => KeyT
)
) : void
//THIS IS BROKEN
declare function superKeyOnly<KeyT extends Key> (
this : (
FindSubKey<
(("x"|"y")[])[],
KeyT
> extends never ?
CompileError<[
KeyT,
"is NOT a super key of",
(("x"|"y")[])[]
]> :
any
),
arg : (
((c : {
x : "x",
y : "y",
z : "z"
}) => KeyT)
)
) : void As usual, string literals are okay, but using function args breaks. While the inferred type of It shows |
By reverting back to this, export type ExtractSubKey<
A extends Key,
B extends Key
> = (
A[number] extends B[number] ?
A :
never
);
export type ExtractSuperKey<
A extends Key,
B extends Key
> = (
B[number] extends A[number] ?
A :
never
); I get, //Broken
subKeyOnly(c => [c.z])
//Broken
subKeyOnly(c => [c.x])
//Broken
subKeyOnly(c => [c.x, c.y])
//OK!
subKeyOnly(() => ["z" as "z"]);
//OK!
subKeyOnly(() => ["x" as "x"]);
//OK!
subKeyOnly(() => ["x" as "x", "y" as "y"]);
//OK!
superKeyOnly(c => [c.z])
//OK!
superKeyOnly(c => [c.x])
//OK!
superKeyOnly(c => [c.x, c.y])
//OK!
superKeyOnly(() => ["z" as "z"]);
//OK!
superKeyOnly(() => ["x" as "x"]);
//OK!
superKeyOnly(() => ["x" as "x", "y" as "y"]); Guess I gotta' figure out how to make it work for this particular case. |
It seems like this implementation of declare function subKeyOnly<KeyT extends Key> (
arg : (
(c : {
x : "x",
y : "y",
z : "z"
}) => KeyT &
(
FindSuperKey<
(("x"|"y")[])[],
KeyT
> extends never ?
CompileError<[
KeyT,
"is not a sub key of",
(("x"|"y")[])[]
]> :
unknown
)
)
) : void If I tweak it ever so slightly, it breaks, Spoiler!The first snippet is,
The second snippet is,
The difference is an extra parentheses. declare function subKeyOnly<KeyT extends Key> (
arg : (
((c : {
x : "x",
y : "y",
z : "z"
}) => KeyT) &
(
FindSuperKey<
(("x"|"y")[])[],
KeyT
> extends never ?
CompileError<[
KeyT,
"is not a sub key of",
(("x"|"y")[])[]
]> :
unknown
)
)
) : void |
However, moving it to the declare function subKeyOnly<KeyT extends Key> (
this : (
FindSuperKey<
(("x"|"y")[])[],
KeyT
> extends never ?
CompileError<[
KeyT,
"is not a sub key of",
(("x"|"y")[])[]
]> :
unknown
),
arg : (
(c : {
x : "x",
y : "y",
z : "z"
}) => KeyT
)
) : void Moving the callback's type to the inside of the conditional type breaks it, declare function subKeyOnly<KeyT extends Key> (
arg : (
(
FindSuperKey<
(("x"|"y")[])[],
KeyT
> extends never ?
CompileError<[
KeyT,
"is not a sub key of",
(("x"|"y")[])[]
]> :
(c : {
x : "x",
y : "y",
z : "z"
}) => KeyT
)
)
) : void |
@AnyhowStep Please unassign yourself when you reach a conclusion and provide a more actionable summary if possible - thanks! 😁 |
Sorry for the wall of comments. I used it to track the myriad of different ways to do this one specific thing, type PerformComplicatedCompileTimeChecks<FunctionT extends (...args:any[]) => any, T0, T1, /*snip*/> = (
/*snip*/ extends /*snip*/ ?
//success
FunctionT :
//failure
CompileError<["Some error message"]>
);
declare function foo<FunctionT extends (...args:any[]) => any> (
f : PerformComplicatedCompileTimeChecks<FunctionT, T0, T1, /*snip*/>
) : void
//Assume this is OK
//Return type of `1` is fine
//Compiles fine.
//No squiggly
foo(() => 1);
//Assume this somehow breaks some custom compile-time constraint
//Return type of `2` violates the very basic principles of the universe
//Should not compile.
//Red squiggly saying, CompileError<["Some error message"]>
foo(() => 2); There are many ways to implement the above. We basically need to test the
However, 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 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 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. I believe the one-true-way for it to work is this, type PerformComplicatedCompileTimeChecks<ReturnT, T0, T1, /*snip*/> = (
ReturnT &
(
/*snip*/ extends /*snip*/ ?
//success
unknown :
//failure
CompileError<["Some error message"]>
)
);
declare function foo<ReturnT> (
f : (/*snip args*/) => PerformComplicatedCompileTimeChecks<
ReturnT, T0, T1, /*snip*/
>
) : void
//Assume this is OK
//Return type of `1` is fine
//Compiles fine.
//No squiggly
foo(() => 1);
//Assume this somehow breaks some custom compile-time constraint
//Return type of `2` violates the very basic principles of the universe
//Should not compile.
//Red squiggly saying, 2 is not assignable to `2 & CompileError<["Some error message"]>`
foo(() => 2);
However, 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. |
After further experimentation, it seems like even my one-true-way attempt isn't perfect. 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 |
Please log a new issue if any action is required on our side. Thanks! |
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.
Expected behavior:
Whether I use the
c
argument or not when callingnoSubKey()/noSuperKey()
,it should always infer the type of
KeyT
correctly for all cases.Actual behavior:
noSubKey()
With
c
argument, it correctly infersKeyT
in the error messageWith
c
argument, it incorrectly infersKeyT
in the tooltipWith string literals, it correctly infers
KeyT
in the error messageWith string literals, it correctly infers
KeyT
in the tooltipnoSuperKey()
With
c
argument, it incorrectly infersKeyT
in the error messageWith
c
argument, it incorrectly infersKeyT
in the tooltipWith string literals, it correctly infers
KeyT
in the error messageWith string literals, it correctly infers
KeyT
in the tooltipPlayground Link:
Playground
Related Issues:
Off the top of my head, I've made similar reports before and other similar repro cases,
#29133
#23689 (comment)
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<>
onFunctionT
.I use
KeyT
and make the parameter(c) => KeyT
and it works in some cases but breaks in this case fornoSuperKey()
and works (mostly) fine fornoSubKey()
The text was updated successfully, but these errors were encountered: