Return type inference breaks in function parameter; tooltip also inconsistent
See original GitHub issueTypeScript 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
c
argument, it correctly infersKeyT
in the error message -
With
c
argument, it incorrectly infersKeyT
in the tooltip -
With string literals, it correctly infers
KeyT
in the error message -
With string literals, it correctly infers
KeyT
in the tooltip
noSuperKey()
-
With
c
argument, it incorrectly infersKeyT
in the error message -
With
c
argument, it incorrectly infersKeyT
in the tooltip -
With string literals, it correctly infers
KeyT
in the error message -
With string literals, it correctly infers
KeyT
in the tooltip
Playground Link:
Related Issues:
Off the top of my head, I’ve made similar reports before and other similar repro cases,
https://github.com/microsoft/TypeScript/issues/29133
https://github.com/microsoft/TypeScript/issues/23689#issuecomment-512114782
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()
Issue Analytics
- State:
- Created 4 years ago
- Comments:17 (17 by maintainers)
Top GitHub Comments
@AnyhowStep Please unassign yourself when you reach a conclusion and provide a more actionable summary if possible - thanks! 😁
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, https://github.com/microsoft/TypeScript/issues/14829#issuecomment-520191642