-
Notifications
You must be signed in to change notification settings - Fork 12.2k
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
Generic parameter should take on its default (instead of unknown
) when inference of another bound type param fails
#56108
Comments
Duplicate of #16229.
|
I am not convinced. Although the reason for closure that you quoted would also apply, both issues are different in my opinion. The comment on that issue argues why inference should result in an error rather than accept a default type that would act to suppress the issue ( I would be interested in knowing if there are other reasons as to why generic parameter defaults play no part in inference, specifically in cases such as the given example. |
All other things equal, a simpler model of inference is certainly better. The case for involving the default needs to be made on its own terms. Let's take this example and modify it a bit function foo<T = number>(cb: (arg0: T) => void) {
return function(arg: T) { }
}
function cbFactory<U>(): (arg0: U) => void {
return (arg0: U) => {}
}
const caller = foo(cbFactory());
caller("hello world"); I argue that this is a correct inference, because function foo<T = number>(cb: (arg0: T) => void) {
return function(arg: T) { }
}
function cbFactory<U extends unknown>(): (arg0: U) => void {
return (arg0: U) => {}
}
const caller = foo(cbFactory());
caller("hello world"); Now the argument being made here is that function foo<T = number>(cb: (arg0: T) => void) {
return function(arg: T) { }
}
function cbFactory<U extends string>(): (arg0: U) => void {
return (arg0: U) => {}
}
const caller = foo(cbFactory());
caller("hello world"); Now deciding that So you have to decide on at least one of these:
|
Certainly there are cases where selecting the generic parameter default without checking it to be valid will lead to type errors. Perhaps my original suggestion was unclear, but what I am proposing is only that the generic parameter default be considered as a candidate for the inference, meaning that (to my understanding at least) it will be selected if it is valid, satisfying all applicable constraints. In the example you gave, This, as you mention, would require an additional pass in the inference algorithm to evaluate the validity of the default after exhausting all other candidates. And to your question, those constraints are different for the same reasons they are different in any other context, in that |
This already happens. When there are zero other candidates, the default is chosen: declare function fn<T = number>(arg?: T): T;
// m: number
const m = fn(); But the OP example doesn't have zero candidates; it has one candidate - the implicit constraint of I'm not sure how we'd unify slotting the default into the middle of the candidate priority list -- intuitively, it should be at the bottom, and is. Making the default higher priority than it already is seems very confusing when encountered in more complex cases -- if there's another candidate, surely we should be picking that candidate over the default, otherwise the default should have been a constraint. |
I'm pretty sure the proposed behavior would make it so that const cb = cbFactory(); // U = unknown
foo(cb); // T = unknown produces a different inference than foo(cbFactory()); // U = T = string which sounds less than ideal and potentially very confusing. |
This issue has been marked as "Working as Intended" and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
π Search Terms
generic param default ignored unknown infer
β Viability Checklist
β Suggestion
The issue is that given a generic param with a default, where the type is bound to other sites, when the generic param type cannot be inferred at any of the inference sites, it takes on
unknown
rather than the default (which would be valid).From the TS documentation for Generic Parameter Defaults:
Which means that once the inference fails at the bound sites, the
unknown
is selected (as the only candidate), and then is propagated back up to the generic param, which takes on the typeunknown
and skips applying the default.In the example,
U
fails to infer first, soU
isunknown
andT
is inferred from that.I would expect inference to backtrack from the failure to infer at the bound site and continue with a pass to test the default type as a candidate. Otherwise, it makes it seem as though the
unknown
from the failure to infer was actually received as a successfully inferred type (that would supersede the default).Indeed, the default is selected as expected when it is for the generic param on the contained call (e.g. if the default were on
U
in the example).From my understanding, assigning the default to the generic param in such cases would:
a) be valid, in all cases where the generic type param is
unknown
due to failing to infer (and not due to having successfully inferred the type asunknown
, e.g. from an explicitunknown
)b) be less surprising than the alternative, which is the generic type parameter ignoring the default when inference fails, instead having to be explicitly specified by the caller
(Even if a) does not hold, I would expect inference to at least attempt the default as a candidate.)
π Motivating Example
When called such that the type for
T
is bound to the type forU
, butU
cannot be inferred, soT
andU
take on the typeunknown
:However, since the default
number
type for the generic paramT
is valid given the usage, the default should be applied toT
which would propagate toU
. Indeed, this would be the case if the usage manually specified the type forT
:π» Use Cases
My use case is a constructor where the supplied callback can either take in a key or a transformed key from another callback, and so in the absence of transformation function, the key type should be defaulted to the non-transformed type.
The current approach is to not rely on inference, and instead explicitly specify types for the generic params. This is tedious, as those types could instead be inferred from usage.
A workaround is to avoid the generic param default altogether, and instead place the default type as a union member at the inference site(s) (e.g. in the call signature).
With the example, that would look like:
With the same usage,
T
is now successfully inferred asnumber
. This does have the limitation that where before, the type forarg0
of thecb
was completely open-ended, it is now constrained to be a union withnumber
. In some cases this is fine.The text was updated successfully, but these errors were encountered: