-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Correlated type constraint breaks under return type inference #32804
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
From the behaviour of the above snippets and the inferred types displayed by the tooltips, it just feels like someone forgot to copy-paste some type inference logic or something. Like, the |
!@#$ YES! type XStr = {x:string};
type XNum = {x:number};
type U = XStr|XNum;
type Args = { str : XStr, num : XNum };
declare function foo<
ReturnT extends U,
ValueT extends ReturnT["x"]
> (
...f : (
(
ReturnT extends U ?
[((args : Args) => ReturnT), ValueT] :
never
)
)
) : ValueT;
/*
Error as expected.
Type 'string | number' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'.
*/
foo<XStr, string|number>(
(args:Args) => args.str,
""
);
//Inferred type, foo<XStr, string>
foo(
args => args.str,
/*
Error as expected.
Type 'string | number' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'.
*/
"" as string|number
);
//Inferred type, foo<XStr, string>
foo(
//Added explicit type annotation to function params
(args:Args) => args.str,
/*
Error as expected.
Type 'string | number' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'.
*/
"" as string|number
);
/////
/*
Error as expected.
Type '1' does not satisfy the constraint 'string'.
*/
foo<XStr, 1>(
(args:Args) => args.str,
1
);
//Inferred type, foo<XStr, string>
foo(
args => args.str,
/*
Error as expected.
Type '1' does not satisfy the constraint 'string'.
*/
1
);
//Inferred type, foo<XStr, string>
foo(
//Added explicit type annotation to function params
(args:Args) => args.str,
/*
Error as expected.
Type '1' does not satisfy the constraint 'string'.
*/
1
);
//Return type is "hello" as expected
foo(
args => args.str,
"hello"
);
//Return type is 42 as expected
foo(
args => args.num,
42
); The magic is here, ...f : (
(
ReturnT extends U ?
[((args : Args) => ReturnT), ValueT] :
never
)
) It turns [Edit] After spending a good chunk of my Saturday trying to think of a workaround, I suddenly came up with this idea after randomly remembering my comment here, I had brought up using tuples and rest parameters as a possible workaround for that issue (there was a different, better way to express it in that issue, though) [Edit]
My use case requires both [Edit] Fixed it to use [Edit] Nevermind. I tried to adapt it to fit my actual use case and this workaround broke apart. So, I can't have the conditional type perform distribution. |
This is closer to my personal use-case, type XStr = {x:string};
type XNum = {x:number};
type U = XStr|XNum;
type Args = { str : XStr, num : XNum };
declare function foo<
ReturnT extends U,
ValueT extends ReturnT["x"]
> (
...f : (
(
//Uses `& ReturnT["x"]` to make this work
[((args : Args) => ReturnT), ValueT & ReturnT["x"]]
)
)
) : ValueT;
//OK!
//Inferred type, foo<XStr | XNum, string | number>
foo(
args => Math.random() > 0.5 ? args.str : args.num,
"" as string|number
);
/*
Error as expected.
Type 'string | number' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'.
*/
foo<XStr, string|number>(
(args:Args) => args.str,
""
);
//Inferred type, foo<XStr, string|number>
foo(
args => args.str,
/*
Expected: Error
Actual: OK
*/
"" as string|number
);
//Inferred type, foo<XStr, string>
foo(
//Added explicit type annotation to function params
(args:Args) => args.str,
/*
Error as expected.
Type 'string | number' does not satisfy the constraint 'string'.
Type 'number' is not assignable to type 'string'.
*/
"" as string|number
);
/////
/*
Error as expected.
Type '1' does not satisfy the constraint 'string'.
*/
foo<XStr, 1>(
(args:Args) => args.str,
1
);
//Inferred type, foo<XStr, 1>
foo(
args => args.str,
/*
Expected: Error
Actual: OK
*/
1
);
//Inferred type, foo<XStr, string>
foo(
//Added explicit type annotation to function params
(args:Args) => args.str,
/*
Error as expected.
Type '1' does not satisfy the constraint 'string'.
*/
1
);
//Return type is "hello" as expected
foo(
args => args.str,
"hello"
);
//Return type is 42 as expected
foo(
args => args.num,
42
); I need to allow a union type to be returned and handle that accordingly. |
Seems like using type UnionToIntersection<U> = (
(
U extends any ? (k: U) => void : never
) extends (
(k: infer I) => void
) ? I : never
);
type XStr = {x:{propStr:string}};
type XNum = {x:{propNum:number}};
type U = XStr|XNum;
type Args = { str : XStr, num : XNum };
declare function foo<
ReturnT extends U
> (
...f : (
(
[
((args : Args) => ReturnT),
UnionToIntersection<
ReturnT["x"]
>
]
)
)
) : void;
foo(
args => Math.random() > 0.5 ? args.str : args.num,
//OK!
//Error: Property 'propNum' is missing
{
propStr : "hello",
}
);
foo(
args => Math.random() > 0.5 ? args.str : args.num,
//OK!
//Error: Property 'propStr' is missing
{
propNum : 42,
}
);
foo(
args => Math.random() > 0.5 ? args.str : args.num,
//OK!
//No error
{
propStr : "hello",
propNum : 42,
}
);
//Expected: foo<XStr>
//Actual : foo<U>
foo(
args => args.str,
//Expected: OK
//Actual : Property 'propNum' is missing
{ propStr : "hello" }
);
type NoInfer<T> = [T][T extends any ? 0 : never];
declare function foo2<
ReturnT extends U
> (
...f : (
(
[
((args : Args) => ReturnT),
UnionToIntersection<
//NoInfer doesn't seem to work here
NoInfer<ReturnT>["x"]
>
]
)
)
) : void;
//Expected: foo2<XStr>
//Actual : foo2<U>
foo2(
args => args.str,
//Expected: OK
//Actual : Property 'propNum' is missing
{ propStr : "hello" }
); Better repro of how |
I'm ignoring the long posts other than the OP 😐 Why isn't the function definition just this? declare function foo<ReturnT extends U>(
f: (args: Args) => ReturnT,
arg: ReturnT["x"]
): void; Alternatively it seems like you need a non-inferential type parameter usage operator? #14829 |
I need It's used for the return type in my actual use case. (The return type was All attempts at using the workarounds for non inferential type param usage break for this use case, at the moment =/ The function represents a column in SQL. The return type is the type of the column. The Let's say we have this query being built by the library, SELECT
myTable.myColumn
FROM
myTable Let's say the type of the column is If we add this SELECT
myTable.myColumn
FROM
myTable
WHERE
myTable.myColumn <=> 1 Then we can statically narrow the type of |
TypeScript Version: 3.5.1
Search Terms:
return type, generic, constraint, assignable, correlated type
Code
Expected behavior:
I'm just calling it a correlated type because it reminds me of correlated subqueries from SQL.
ValueT
is dependent on the type ofReturnT
.f
does not have parameters, or all parameters are explicitly annotated,ValueT
is inferred correctly.f
has parameters that are not explicitly annotated,ValueT
is inferred incorrectly.foo<XStr, string|number>
should not be allowedfoo<XStr, 1>
should not be allowedActual behavior:
foo<XStr, string|number>
is allowed under inferencefoo<XStr, 1>
is allowed under inferencePlayground Link:
Playground
Related Issues:
#32540 (comment)
#29133
A different, more complex example,
#14829 (comment)
[Edit]
Can someone come up with a better name for this?
I'm working on rewriting my type-safe SQL builder library and it relies on the return type of generic functions being inferred correctly. But it seems like return type inference just breaks in so many unexpected ways.
Anonymous callback functions are used a lot for building the
WHERE
,ORDER BY
,GROUP BY
,HAVING
,JOIN
, etc. clauses.Since return type inference for generic functions is not robust, it's basically a blocker for me =(
The text was updated successfully, but these errors were encountered: