You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
{{ message }}
Repository navigation
Suggestion: Noninferential type parameter usage #14829
We keep getting bugs like this and I keep not finding the original so I'm making a new one so we can find it.
Keywords: ignore type parameter optional inference
Problem
Often with generics, there will be some locations where a type parameter should be inferrable from usage, and other places where the type parameter should only be used to enforce typechecking. This comes up in a variety of contexts
classAnimal{move}classDogextendsAnimal{woof}functiondoSomething<T>(value: T,getDefault: ()=>T){}// Wanted an error here - getDefault() ought to return same type as 'value'doSomething(newDog(),()=>newAnimal());
We should be able to mark type parameter consumption sites as being "not eligible for inference". For example, let's say we had a special global type that the compiler knew not to unwrap during inference:
typeNoInfer<T>=T;
Then we can annotate usage sites
functiondoSomething<T>(value: T,getDefault: ()=>NoInfer<T>){}// Wanted an error here - getDefault() ought to return same type as 'value'doSomething(newDog(),()=>newAnimal());
Reacted by Klaus Meinhardt, Asad Saeeduddin, Aluan Haddad, Viktor, brianush1, Ali, piotrwalkusz1, Joe Calzaretta, Wessel Kronemeijer, Edaqa Mortoray and 5 more
not 100% sure if this is a bug or not but I would not take it as designed behavior.
Ryan Cavanaugh (@RyanCavanaugh) can I expect an official statement whether this is supported / intended behavior or not? I don't want to rely on a bug that - when fixed - will break the build.
There may already be some programmers relying on it. I found that pattern while reading some issues in this bug tracker.
Just an update - T & { } creates a "lower-priority" inference site for Tby design. I would move this from the "definitely don't depend on this" column to the "it's probably going to work for the foreseeable future" column.
Reacted by Timothy Degryse, jwbay, Sean M. Vieira, Paul Koerbitz, Alec Larson, AnyhowStep, Joe Calzaretta, ExE Boss, たふみ / Tafumi, Eero Ruohola and 6 more
Reacted by Joshua J.
Reacted by Joshua J., Peter Flynn and Craig P Hicks
Reacted by Sean M. Vieira, Andrey Goncharov, Joe Calzaretta, ExE Boss, Thodoris Greasidis and Marko Kaznovac
Ryan Cavanaugh (@RyanCavanaugh) I wonder if, with the recent additions of type parameter modifiers, it wouldn't be possible to use "usage modifiers" over intrinsic types. What I mean is that this could be implemented using a custom syntax/keyword, like this:
I also wonder what's the current "status" of this proposal. I understand that the TS team is currently not working on this - but what if I would attempt to implement this? I totally understand that any PR is just a PR and nothing is set in stone until merged (and not even that makes anything set in stone 🤣 ) - but I'm hesitant to implement features that are likely to be rejected.
Nathan Shively-Sanders (@sandersn) has asked me if there are any learnings that I could share based on my PoC implementation of this feature, so here we go:
the implementation is pretty straightforward and I didn't encounter many obstacles
IMHO an intrinsic type is the best approach for this feature, it's flexible and clearly marks part of the type as "blocked" - with it there are no questions about "operator precedence"-like things
It's a powerful tool for declaration authors. Not all libraries would benefit from it but some would. There are a couple of types floating around in the ecosystem that tries to accomplish this. We are using this in XState, I know that RTK is also using this and there are probably more.
I encountered some issues with the custom NoInfer (the one proposed by Joe Calzaretta (@jcalz)). I don't recall what they were now and what was the exact case but in some complex cases, this technique didn't manage to deliver what it should. Having a built-in would set up clear expectations for the behavior and wouldn't rely on implementation details of things like deferral of evaluating unresolved conditional types
Further things could be explored later (like LowInfer). However, I find the likelihood of that to be quite low. Inferences priorities are also implementation details and a type like this could dangerously leak things to the userland. It's one thing to block the inference completely on given nodes (it doesn't expose much of the internals to the userland) and another thing to give hints about desired inference priorities.
TLDR: TS is insanely expressive already, this would add a new tool to author things that are not always possible today without "hacks" or unintuitive workarounds + the cost of the feature seems to be pretty low to me. What not to like about it? 😉
Reacted by David, Mike Rippon, Jason Kuhrt, Bjørn Polat-Snoen, razh, Homa Wong, devgioele, Enteleform and David P.
My PR doesn't assume that this "does nothing". It blocks the whole type from being used as an inference source. You could intentionally wrap a type that references multiple type parameters with a single NoInfer type.
I'm not 100% sure about this yet but I also think that the builtin NoInfer could just perform way better (perf-wise) than the one that we have to use today. In sufficiently complex types TS has to "expand" a lot of types by their constraints and explore all branches to find inference candidates. With NoInfer it still has to do that - but it just learns nothing when recursing into types containing it. If a builtin NoInfer could wrap "non-leaf" types then TS could bail out early in certain branches - resulting in a better inference performance.
FYI, the following code seems to do the trick without messing up with the declared types: the drawback is that it has a second type parameter, and, though that parameter has a default so that it needn't be specified, it is quite "inelegant" and, IMO, hardly acceptable (maybe because I can't think of a significant use case for it: that we like it or not, the type T is "acquired" as soon as it's (in) the type of any of the arguments, and there is no point in forcing the user to declare what's already there):
functiondoSomething<T=never,UextendsT=T>(t: U){// do something with `t`}typeNum={x: number};doSomething<Num>({x: 1});// doSomething<Num, Num> => OKdoSomething({x: 1})// doSomething<never, never> => ERROR!
OTOH, a single type parameter T = never does work as long as no argument has T appearing in its type: and this pattern I do have used, since, with the type parameter simply T, if the user is not explicit with the type, s/he gets unknown inferred (or anyway the constraint on T, i.e. B if the type was T extends B), and this may indeed be not acceptable/good enough in some cases...
P.S. Sorry, what I have said above, about a single type parameter T = never doing the job in case no arguments have type T, is just not true, for example consider this code:
// for example, just notice that `T` does not appear in the type of the arguments:functioncoercion<T=never>(t: unknown): T{returntasT;}typeNum={x: number};coercion<Num>({x: 1});// coercion<Num> => OKcoercion({x: 1})// coercion<never> => OK, returns `never`!
I do have a use case where a plain T = never does the trick, but (after looking at it again) it's the specific way I am using T to construct the return type that guarantees that I get a compiler error in user code when T is never...
I have just updated a big code base to use generic react components thanks to NoInfer<T>. So far very happy with the feature. I would just suggest to completely hide NoInfer<T> in the tooltip.
Type declarations are complicated enough already, and NoInfer<T> is a hint to the compiler from the library creator but doesn't mean anything for the library consumer.
Reacted by Jason Kuhrt, Jrson, csha, cryo-warden and Mark van Lagen
I just wanted to chime in and say that NoInfer saved me from an unintuitive and unexpected "Type instantiation is excessively deep and possibly infinite." error when using a function call as a prop value in a complex object type. Before using NoInfer, TS wanted to infer a generic parameter from the return type of the function, leading to the error. After I marked the function's return value as NoInfer, the error went away!
Reacted by Andrii Dieiev, Ryan Cavanaugh, cryo-warden and Jared Poole
Activity
mhegazy commentedon Mar 24, 2017
Is using multiple type parameters not an option?
johnfn commentedon Mar 24, 2017
Mohamed Hegazy (@mhegazy), speaking from my experience, I never ever would have guessed that
function doSomething<T>(value: T, getDefault: () => T) ;could be amended to do what I want by changing it to
function doSomething<T, U extends T>(value: T, getDefault: () => U) ;Multiple type parameters are an option! It's just that right now they are a very surprising and unintuitive one.
mhegazy commentedon Mar 24, 2017
I would not have guessed that NoInfer was the solution :)
PyroVortex commentedon Mar 25, 2017
Unfortunately, using the
U extends Toption has inconsistent behavior for contextual typing, due to the subtly different semantics.Fix type parameter of applyWithFunction (#2396)
RyanCavanaugh commentedon May 1, 2017
Need to collect some use concrete cases before attempting to move forward with this
ajafff commentedon May 1, 2017
I also ran into the problem that
U extends Tallows excess properties as PyroVortex mentions above.I found a rather simple work around:
{} & TThe example above would then be:
RyanCavanaugh commentedon May 1, 2017
Klaus Meinhardt (@ajafff) 😲 !
I would expect
{ } & Tto be indistinguishable fromT- not 100% sure if this is a bug or not but I would not take it as designed behavior.ajafff commentedon May 3, 2017
Ryan Cavanaugh (@RyanCavanaugh) can I expect an official statement whether this is supported / intended behavior or not? I don't want to rely on a bug that - when fixed - will break the build.
There may already be some programmers relying on it. I found that pattern while reading some issues in this bug tracker.
jwbay commentedon Jun 12, 2017
Ran into this when writing some tests.
Edit: making the undesired inference location an intersection per above fixes things up here, too! Results in the expected missing property error.
RyanCavanaugh commentedon Aug 7, 2017
Just an update -
T & { }creates a "lower-priority" inference site forTby design. I would move this from the "definitely don't depend on this" column to the "it's probably going to work for the foreseeable future" column.21 remaining items
Andarist commentedon Feb 6, 2023
I've seen cases when
NoInfer"fails to deliver" its promise though. It would be very cool if this would have builtin support in the compiler :pAndarist commentedon Feb 6, 2023
Ryan Cavanaugh (@RyanCavanaugh) I wonder if, with the recent additions of type parameter modifiers, it wouldn't be possible to use "usage modifiers" over intrinsic types. What I mean is that this could be implemented using a custom syntax/keyword, like this:
I also wonder what's the current "status" of this proposal. I understand that the TS team is currently not working on this - but what if I would attempt to implement this? I totally understand that any PR is just a PR and nothing is set in stone until merged (and not even that makes anything set in stone 🤣 ) - but I'm hesitant to implement features that are likely to be rejected.
dinofx commentedon Feb 24, 2023
I don't see why a new keyword would be needed (
noinfer). Maybe the glass is half full 😉:infershould be unambiguous there, as it isn't part of a conditional extends (related to #52791)Andarist commentedon Feb 25, 2023
I started working on this here. Feedback is welcome :)
Andarist commentedon Mar 7, 2023
Nathan Shively-Sanders (@sandersn) has asked me if there are any learnings that I could share based on my PoC implementation of this feature, so here we go:
NoInfer(the one proposed by Joe Calzaretta (@jcalz)). I don't recall what they were now and what was the exact case but in some complex cases, this technique didn't manage to deliver what it should. Having a built-in would set up clear expectations for the behavior and wouldn't rely on implementation details of things like deferral of evaluating unresolved conditional typesLowInfer). However, I find the likelihood of that to be quite low. Inferences priorities are also implementation details and a type like this could dangerously leak things to the userland. It's one thing to block the inference completely on given nodes (it doesn't expose much of the internals to the userland) and another thing to give hints about desired inference priorities.TLDR: TS is insanely expressive already, this would add a new tool to author things that are not always possible today without "hacks" or unintuitive workarounds + the cost of the feature seems to be pretty low to me. What not to like about it? 😉
dinofx commentedon Mar 17, 2023
What happens when
NoInferis used in a place where it does nothing? for example:Where the author intended to do:
Andarist commentedon Mar 17, 2023
My PR doesn't assume that this "does nothing". It blocks the whole type from being used as an inference source. You could intentionally wrap a type that references multiple type parameters with a single
NoInfertype.Andarist commentedon Mar 31, 2023
I'm not 100% sure about this yet but I also think that the builtin
NoInfercould just perform way better (perf-wise) than the one that we have to use today. In sufficiently complex types TS has to "expand" a lot of types by their constraints and explore all branches to find inference candidates. WithNoInferit still has to do that - but it just learns nothing when recursing into types containing it. If a builtinNoInfercould wrap "non-leaf" types then TS could bail out early in certain branches - resulting in a better inference performance.jasonkuhrt commentedon Apr 2, 2023
Just discovered this thread via https://stackoverflow.com/questions/75909231/how-can-this-advanced-function-type-be-achieved/75909852#75909852 where Joe Calzaretta (@jcalz) enlightened me and pointed out this issue. Thanks for your work Mateusz Burzyński (@Andarist).
jp-diegidio commentedon Jul 19, 2023
FYI, the following code seems to do the trick without messing up with the declared types: the drawback is that it has a second type parameter, and, though that parameter has a default so that it needn't be specified, it is quite "inelegant" and, IMO, hardly acceptable (maybe because I can't think of a significant use case for it: that we like it or not, the type
Tis "acquired" as soon as it's (in) the type of any of the arguments, and there is no point in forcing the user to declare what's already there):OTOH, a single type parameter
T = neverdoes work as long as no argument hasTappearing in its type: and this pattern I do have used, since, with the type parameter simplyT, if the user is not explicit with the type, s/he getsunknowninferred (or anyway the constraint onT, i.e.Bif the type wasT extends B), and this may indeed be not acceptable/good enough in some cases...jp-diegidio commentedon Jul 19, 2023
P.S. Sorry, what I have said above, about a single type parameter
T = neverdoing the job in case no arguments have typeT, is just not true, for example consider this code:I do have a use case where a plain
T = neverdoes the trick, but (after looking at it again) it's the specific way I am usingTto construct the return type that guarantees that I get a compiler error in user code whenTisnever...craigphicks commentedon Dec 18, 2023
In this formula it is completely feed forward, no inference between variables.
olmobrutall commentedon Feb 15, 2024
I have just updated a big code base to use generic react components thanks to
NoInfer<T>. So far very happy with the feature. I would just suggest to completely hideNoInfer<T>in the tooltip.Type declarations are complicated enough already, and
NoInfer<T>is a hint to the compiler from the library creator but doesn't mean anything for the library consumer.cshaa commentedon Jun 26, 2024
I just wanted to chime in and say that
NoInfersaved me from an unintuitive and unexpected"Type instantiation is excessively deep and possibly infinite."error when using a function call as a prop value in a complex object type. Before usingNoInfer, TS wanted to infer a generic parameter from the return type of the function, leading to the error. After I marked the function's return value asNoInfer, the error went away!