Repository navigation
Suggestion: Noninferential type parameter usage #14829
Description
Activity
- addedIn DiscussionNot yet reached consensusNot yet reached consensusSuggestionAn idea for TypeScriptAn idea for TypeScript
on Mar 23, 2017 Is using multiple type parameters not an option?
function doSomething<T, U extends T>(value: T, getDefault: () => U) ; function assertEqual<T, U extends T>(actual: T, expected: U): boolean;
Reacted by Klaus Meinhardt, Asad Saeeduddin, Aluan Haddad, Viktor, brianush1, Ali, piotrwalkusz1, Joe Calzaretta, Wessel Kronemeijer, Edaqa Mortoray and 5 moreMohamed 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.
Reacted by Peter Flynn, Bernard, Yannick Stachelscheid, Kristian Gerardsson, Kraktus, Thomas Jahoda, Kilian Beuchard and Jay BeeI would not have guessed that NoInfer was the solution :)
Reacted by Aluan Haddad, Wesley Wigham, Alec Larson, brianush1, Edaqa Mortoray, Roma P., David Piepgrass and Zach BjornsonReacted by Aluan Haddad, Yannick Stachelscheid, Kraktus, Josh Ghoulberg 👻, Craig P Hicks, Jay Bee and Danilo FurrerUnfortunately, using the
U extends Toption has inconsistent behavior for contextual typing, due to the subtly different semantics.declare function invoke<T, U extends T, R>(func: (value: T) => R, value: T): R; declare function test(value: { x: number; }): number; invoke(test, { x: 1, y: 2 }); // Works test({ x: 1, y: 2 }); // Compiler error
- added a commit that references this issue
on Mar 31, 2017 - addedAwaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureand removedIn DiscussionNot yet reached consensusNot yet reached consensus
on May 1, 2017 RyanCavanaugh commented
on May 1, 2017 MemberAuthorMore actionsNeed to collect some use concrete cases before attempting to move forward with this
I also ran into the problem that
U extends Tallows excess properties as PyroVortex mentions above.I found a rather simple work around:
{} & T
The example above would then be:declare function invoke<T, R>(func: (value: T) => R, value: {} & T): R; declare function test(value: { x: number; }): number; invoke(test, { x: 1, y: 2 }); // Compiler error test({ x: 1, y: 2 }); // Compiler error
RyanCavanaugh commented
on May 1, 2017 MemberAuthorMore actionsI 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.Reacted by Aluan Haddad and Peter Flynnnot 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.
Reacted by ExE BossRan into this when writing some tests.
interface User { name: string; age: number; } type ApiCall<T> = (...args: any[]) => Promise<T>; declare const fetchUser: ApiCall<User>; declare function mock<T>(call: ApiCall<T>, result: T): void; //no error, expected missing property (and property completions ideally) mock(fetchUser, { name: '' });
Edit: making the undesired inference location an intersection per above fixes things up here, too! Results in the expected missing property error.
declare function mock<T>(call: ApiCall<T>, result: {} & T): void;
RyanCavanaugh commented
on Aug 7, 2017 MemberAuthorMore actionsJust 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.Reacted by Timothy Degryse, jwbay, Sean M. Vieira, Paul Koerbitz, Alec Larson, AnyhowStep, Joe Calzaretta, ExE Boss, たふみ / Tafumi, Eero Ruohola and 6 moreReacted by Joshua J.Reacted by Joshua J., Peter Flynn and Craig P HicksReacted by Sean M. Vieira, Andrey Goncharov, Joe Calzaretta, ExE Boss, Thodoris Greasidis and Marko KaznovacReacted by Jack Works, Toni Villena and SlurpTheo21 remaining items
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 :pRyan 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:
declare function assertEqual<T>(actual: T, expected: noinfer T): boolean; const g = { x: 3, y: 2 }; assertEqual(g, { x: 3 }); // Error
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.
I don't see why a new keyword would be needed (
noinfer). Maybe the glass is half full 😉:declare function invoke<T, R>(func: (value: infer T) => R, value: T): R;
infershould be unambiguous there, as it isn't part of a conditional extends (related to #52791)Andarist commented
on Feb 25, 2023 ContributorMore actionsI started working on this here. Feedback is welcome :)
Reacted by Honza Gál, Mike Rippon and EnteleformNathan 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.What happens when
NoInferis used in a place where it does nothing? for example:function append<T>(dest: T[], src: NoInfer<T[]>) { // TODO }
Where the author intended to do:
function append<T>(dest: T[], src: NoInfer<T>[]) { // TODO }
Andarist commented
on Mar 17, 2023 ContributorMore actionsMy 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 commented
on Mar 31, 2023 ContributorMore actionsI'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.Reacted by Craig P Hicks and cryo-wardenJust 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).
Reacted by Christian Kulesza and cryo-wardenFYI, 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):function doSomething<T = never, U extends T = T>(t: U) { // do something with `t` } type Num = { x: number }; doSomething<Num>({ x: 1 }); // doSomething<Num, Num> => OK doSomething({ x: 1 }) // doSomething<never, never> => ERROR!
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...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:// for example, just notice that `T` does not appear in the type of the arguments: function coercion<T = never>(t: unknown): T { return t as T; } type Num = { x: number }; coercion<Num>({ x: 1 }); // coercion<Num> => OK coercion({ x: 1 }) // coercion<never> => OK, returns `never`!
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...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:declare function invoke<T, R>(func: (value: T) => R, value: {} & T): R; declare function test(value: { x: number; }): number; invoke(test, { x: 1, y: 2 }); // Compiler error test({ x: 1, y: 2 }); // Compiler error
In this formula it is completely feed forward, no inference between variables.
declare function invoke<F extends ((value:any) => any)>(func:F, value: Parameters<F>["0"]): ReturnType<F>; declare function test(value: { x: number; }): number; invoke(test, { x: 1, y: 2 }); // Compiler Error // ~ test({ x: 1, y: 2 }); // Same Compiler error // ~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.Reacted by Jason Kuhrt, Jrson, csha, cryo-warden and Mark van LagenI 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!Reacted by Andrii Dieiev, Ryan Cavanaugh, cryo-warden and Jared Poole

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
Proposal Sketch
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:
Then we can annotate usage sites