Repository navigation
Infer-only function generics #62182
Description
🔍 Search Terms
generic infergeneric typeoftypeof argumentsinfer-only genericsinfer argumentsunoverridable generics
✅ Viability Checklist
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ Suggestion
Although there are many proposals for various syntax to allow specifying only specific, named generics (e.g. #23696) or skipping specification of certain generics (e.g. #26242), these miss a key use case: sometimes you want to reference an inferred type that doesn't make sense to override.
I propose adding a way to reference inferred types in generics, without making them able to be manually overridden, by using the infer keyword in function argument type definitions.
function create<
TOutput = InferOutput<TTableName>,
TInput = InferInput<TTableName>,
>(
table: infer TTableName extends string,
data?: TInput
): Promise<TOutput>Theoretically, this could also be applied to classes:
class DatabaseTable {
constructor(public readonly name: infer TTableName extends string) {}
create<TInput = InferInput<TTableName>, TOutput = InferOutput<TTableName>>(input: TInput): TOutput {}
}But I'll admit that I haven't thought super deeply about the use cases or implications of that, so I'm going to focus on the function use case.
📃 Motivating Example
Consider the following generic function that automatically infers the input and output types of a SQL CREATE query based on the provided table name:
async function create<
TTableName extends string,
TOutput = InferOutput<TTableName>,
TInput = InferInput<TTableName>,
>(
table: TTableName,
data?: TInput
): Promise<TOutput> {
// stub
return {} as unknown as TOutput
}This works as is, but it does come with a few limitations.
Limitations
Redundant and/or Contradictory TTableName definitions
Ideally, we want to be able to override TOutput and TInput for special cases, but overriding TTableName doesn't really make sense. However, we are currently forced to manually specify TTableName because it is a required generic parameter:
// this works, but is frustratingly redundant
create<'users', UserOut, UserIn>('users', {})It also leads to potentially confusing contradictions in the types,
// Argument of type '"friends"' is not assignable to parameter of type '"users"'. ts(2345)
create<'users', UserOut, UserIn>('admins', {})Inability to define a default value
Because TTableName is a standard generic that can be overridden, we can't define a default value for table in the function definition -- after all, the caller could have specified a value for TTableName, and our default value might not agree with it!
create<...>(
// Type 'string' is not assignable to type 'TTableName'.
// 'string' is assignable to the constraint of type 'TTableName', but 'TTableName' could be instantiated with a different subtype of constraint 'string'. ts(2322)
table: TTableName = 'users',
data?: TInput
)The Solution
What we really want is a way to reference the inferred type of table without having to define a redundant, potentially conflicting, overridable generic. This could be accomplished by allowing the infer keyword to be used as the type of a function argument:
function create<
TOutput = InferOutput<TTableName>,
TInput = InferInput<TTableName>,
>(
table: infer TTableName extends string,
data?: TInput
): Promise<TOutput>This would expose the type TTableName in all of the same contexts that other generics could be used:
- In other generics
- In the return type
- in subsequent arguments
- In the function body
Implementation challenges
"You know nothing, Jon Snow" - Ygritte
I freely admit that my knowledge of TypeScript internals is, even generously... sparse. That said, I do have some thoughts about the potential challenges of implementing this feature.
First of all, it seems that these "named inferred argument types" (for lack of a better term) would most likely need to be processed before everything else to allow them to be referenced everywhere, including in the (ostensibly preceding) generics. I'm not sure how feasible that is, or if the TypeScript parser would easily handle something like that.
💻 Use Cases
What do you want to use this for?
This would be invaluable for library development in a plethora of use cases. Personally, I tend to develop custom database wrappers and query builders, every single one of which has suffered some of the limitations of the current syntax.
What shortcomings exist with current approaches?
- Redundancy when you must manually specify generics which are normally inferred
- Potentially confusing contradictions between the generics and actual arguments
- Inability to specify default values for inferred generics
What workarounds are you using in the meantime?
In the meantime, I am sucking it up and just having the redundancy and being diligent about conflicting types. For the default values problem, there are some possible workarounds, such as defining the default in the function body:
async function create<
TTableName extends string = 'users',
TOutput = InferOutput<TTableName>,
TInput = InferInput<TTableName>,
>(
table?: TTableName,
data?: TInput
): Promise<TOutput> {
const actualTable = table || 'users'
// stub
return {} as unknown as TOutput
}This technically works, but comes with its own limitations.
Activity
MartinJohns commentedon Aug 3, 2025
Duplicate of #59814.
rintaun commentedon Aug 3, 2025
You know, I searched for so many iterations of related items and still didn't come across that one, so my apologies there.
That said, I think this proposal does offer some deeper motivating use cases that were not originally covered in that issue.
One such motivator is the many use cases where it does not make sense for a generic to be able to be overridden. This was somewhat touched on in #59814 (comment):
This is indeed case where there is never a good reason to override
Path, but there is a good reason to override the default ofParams. Unfortunately, because that default needs to usePath, it must be defined beforeParams, and thus must be overridden if one wants to overrideParams.Another such use case is the default value assignment for a parameter with an inferred generic type -- as far as I know, there is not a good workaround for that at all.
MartinJohns commentedon Aug 3, 2025
In the end your actual issue will not be solved because the user can still pass in any
stringtypedpath(including union types), unless you specify that also only literal values are allowed for the parameter. Without that, #26242 or #10571 is simply the better option that fulfills more use-cases (and is likely easier to implement).rintaun commentedon Aug 3, 2025
Let me think this through -- I'm not sure I follow what the issue is with the user being able to pass in any
string-- even union types.With my proposal, this would look something like this:
I can somewhat see how this would be a problem,
Pathwould infer to'/foo/:id' | '/bar/:page'andParamswould presumably thus default to something along the lines of{ id: string } | { page: string }. In that case,SomeComponentwould have to accept one or the other of those params (idorpage) as props -- which definitely doesn't seem like useful behavior, as it doesn't really constrainSomeComponent.I feel like I must be missing something, because that really doesn't seem like a limitation of this proposal, let alone a counterexample.
Do you have an example that shows what you mean by the actual issue not being solved?
MartinJohns commentedon Aug 3, 2025
As I understand it, you have two main motivations:
Not having to provide all generic type arguments when calling the method, which is smarter and more flexible solved with the other issues.
And not allowing the user to explicitly provide a type, which this suggestion would not prevent at all.
Regardless, I see no purpose in further engaging in this issue.
rintaun commentedon Aug 3, 2025
Hmm, that's not quite how I intended this proposal, though I can see how they might be similar. Let me address them both.
EDIT: Sorry for all the typos. I wrote this on my phone 😢
Does this prevent the user from explicitly specifying a type parameter
First, in my opinion, this proposal absolutely would prevent the user from explicitly providing a type for the generic in meaningful ways. This might just be semantics, but when talking about a type system, that seems important.
If I'm understanding you correctly, you are suggesting that these two formations are, for all intents and purposes, identical:
Because you can, essentially, explicitly specify
Pathin both cases, though it looks a bit different:Am I understanding you correctly that this is what you mean when you said that this proposal does not prevent a user from explicitly providing a type?
If so, then I get where you're coming from, but I disagree. These two constructions have tangible differences:
With
routeGenericyou cannot specify a default value forpathin the functional signature; withrouteInferyou can.With
routeGeneric, specifyingPathis 100% redundant -- that is to say, you have to provide apathvariable, regardless, from which the type can be inferred, so I'm the best case these two are simply redundant, and in worse cases it can cause confusing conflicts.The fundamental difference between these two constructions is that on
routeGenericthere are two competing sources of truth for the type ofpath: the genericPathand the actual type of the provided argument. That is made clear in the error message when you try to assign a default value topath:'Path' could be instantiated with a different subtype of constraint 'string'.However, with
routeInfer, there is rightfully only a single source of truth for the type ofpath, so conflicts are not possible.I challenge you to show a single example where it is meaningful to specify
Path(or another generic in an equivalent construction, of course).Is this addressed by other proposals that allow skipping type parameters?
Long story short, no. There are certainly meaningful use cases for such a feature, and I'd love to see it implemented. It could even function as a partial workaround to the use cases this proposal addresses.
But those proposals do not address the dual source of truth problem at all - we are still left with the same limitations leading to conflict.
Again, there are cases where skipping arguments is 100% the right solution, because sometimes you presumably do want to override them.
However, I again assert that there is a class of inferred generic which it is never meaningful to manually override, and that is what this proposal is intended to address.
MartinJohns commentedon Aug 4, 2025
No, you can not. The type is inferred based on the argument provided, so the type provided by the user can be incompatible with the default value.
rintaun commentedon Aug 4, 2025
Can you please provide an example? Because I don't believe there is any way to provide a type for
pathtorouteInferwithout also providing a value. If you provide a value, the default value obviously does not apply, and if you don't provide a value, there is nothing for the default value to conflict with.MartinJohns commentedon Aug 4, 2025
Inferred type would be
"abc", but the value at runtime might beundefined, meaning the default value would be used.rintaun commentedon Aug 4, 2025
Ah, I see what you're getting at. Thanks for the example!
That said, I don't actually see that being an issue. It seems that
Pathcan be inferred (correctly) to"abc" | <default>The difference is that with
routeGenericyou can callrouteGeneric<"abc">()and thus the type ofPathwill conflict with the default. Whereas withrouteInfer, and you can specify the type of the variable passed in topath, but notPathitself.Remember,
Path extends string, so when your provided value hasundefined, that can now be properly inferred away.MartinJohns commentedon Aug 4, 2025
That would be very confusing, since the default value is an implementation detail and not part of the signature.
#60222 (comment)
You can just be explicit and add
"whatever-default"to your type:(infer Path extends string) | "whatever-default"(using your hypothetical syntax) orPath | "whatever-default".Which in the end makes absolutely no difference. Whether the type is explicitly defined as a type argument, or the type argument is inferred using the
_sigil (as proposed in #26242) or if the type of the variable is inferred just like that - the end behavior is in all cases absolutely the same. In all cases the user can still provide an arbitrary type, based on the type of the value that is passed.I have no idea what you're trying to say with that. The type can be inferred just fine regardless of the presence of
undefinedDoesn't matter if
valis typed"abc" | undefinedor just"abc", the inferred type forTis"abc"in both cases.Anyway, we're running in circles. Now I'm really out.
RyanCavanaugh commentedon Aug 18, 2025
I think this proposal conflates a few different scenarios which have all been previously discussed:
While a big hammer to prevent a positional argument seems to fix all of these, we really don't think it's a good idea at all (see #59814) and isn't the right fix for all the other use cases.
typescript-bot commentedon Aug 21, 2025
This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.