Skip to content

Infer-only function generics #62182

Closed as not planned
Closed as not planned

Description

🔍 Search Terms

  • generic infer
  • generic typeof
  • typeof arguments
  • infer-only generics
  • infer arguments
  • unoverridable generics

✅ Viability Checklist

⭐ 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

  1. MartinJohns commented on Aug 3, 2025

    @MartinJohns
    Contributor

    Duplicate of #59814.

  2. rintaun commented on Aug 3, 2025

    @rintaun
    Author

    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):

      route<infer Path extends string, Params = ExtractParams<Path>>(
        path: Path,
        component: React.ComponentType<Params>,
      ): Route<Params>;

    This is indeed case where there is never a good reason to override Path, but there is a good reason to override the default of Params. Unfortunately, because that default needs to use Path, it must be defined before Params, and thus must be overridden if one wants to override Params.

    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.

  3. MartinJohns commented on Aug 3, 2025

    @MartinJohns
    Contributor

    In the end your actual issue will not be solved because the user can still pass in any string typed path (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).

  4. rintaun commented on Aug 3, 2025

    @rintaun
    Author

    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:

    route<Params = ExtractParams<Path>>(
      path: infer Path extends string,
      component: React.ComponentType<Params>,
    ): Route<Params>;
    
    const dynamicPath: '/foo/:id' | '/bar/:page' = genPath()
    
    route(path: dynamicPath, component: SomeComponent)

    I can somewhat see how this would be a problem, Path would infer to '/foo/:id' | '/bar/:page' and Params would presumably thus default to something along the lines of { id: string } | { page: string }. In that case, SomeComponent would have to accept one or the other of those params (id or page) as props -- which definitely doesn't seem like useful behavior, as it doesn't really constrain SomeComponent.

    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?

  5. MartinJohns commented on Aug 3, 2025

    @MartinJohns
    Contributor

    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.

  6. rintaun commented on Aug 3, 2025

    @rintaun
    Author

    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:

    routeGeneric<Path extends string, Params = ExtractParams<Path>>(
      path: Path,
      component: React.ComponentType<Params>,
    ): Route<Params>;
    routeInfer<Params = ExtractParams<Path>>(
      path: infer Path extends string,
      component: React.ComponentType<Params>,
    ): Route<Params>;

    Because you can, essentially, explicitly specify Path in both cases, though it looks a bit different:

    routeGeneric<`/${string}/:id`, { id: string }>(...)
    routeInfer<{ id: string }>(someVar as `/${string}/:id`)

    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:

    1. With routeGeneric you cannot specify a default value for path in the functional signature; with routeInfer you can.

    2. With routeGeneric, specifying Path is 100% redundant -- that is to say, you have to provide a path variable, 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 routeGeneric there are two competing sources of truth for the type of path: the generic Path and the actual type of the provided argument. That is made clear in the error message when you try to assign a default value to path: '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 of path, 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.

  7. MartinJohns commented on Aug 4, 2025

    @MartinJohns
    Contributor
    1. With routeGeneric you cannot specify a default value for path in the functional signature; with routeInfer you can.

    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.

  8. rintaun commented on Aug 4, 2025

    @rintaun
    Author
    1. With routeGeneric you cannot specify a default value for path in the functional signature; with routeInfer you can.

    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.

    Can you please provide an example? Because I don't believe there is any way to provide a type for path to routeInfer without 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.

  9. MartinJohns commented on Aug 4, 2025

    @MartinJohns
    Contributor
    declare const path: "abc" | undefined;
    routeInfer(path);

    Inferred type would be "abc", but the value at runtime might be undefined, meaning the default value would be used.

  10. rintaun commented on Aug 4, 2025

    @rintaun
    Author

    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 Path can be inferred (correctly) to "abc" | <default>

    The difference is that with routeGeneric you can call routeGeneric<"abc">() and thus the type of Path will conflict with the default. Whereas with routeInfer, and you can specify the type of the variable passed in to path, but not Path itself.

    Remember, Path extends string, so when your provided value has undefined, that can now be properly inferred away.

  11. MartinJohns commented on Aug 4, 2025

    @MartinJohns
    Contributor

    It seems that Path can be inferred (correctly) to "abc" | <default>

    That would be very confusing, since the default value is an implementation detail and not part of the signature.

    #60222 (comment)

    For the most part, TS views parameter defaults as an implementation detail of the function, not an intrinsic part of the signature.

    You can just be explicit and add "whatever-default" to your type: (infer Path extends string) | "whatever-default" (using your hypothetical syntax) or Path | "whatever-default".


    The difference is that with routeGeneric you can call routeGeneric<"abc">() and thus the type of Path will conflict with the default. Whereas with routeInfer, and you can specify the type of the variable passed in to path, but not Path itself.

    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.

    Remember, Path extends string, so when your provided value has undefined, that can now be properly inferred away.

    I have no idea what you're trying to say with that. The type can be inferred just fine regardless of the presence of undefined

    function foo<T extends string>(arg: T | "default" = "default") { }
    
    declare const val: "abc" | undefined
    foo(val);

    Doesn't matter if val is typed "abc" | undefined or just "abc", the inferred type for T is "abc" in both cases.


    Anyway, we're running in circles. Now I'm really out.

  12. RyanCavanaugh commented on Aug 18, 2025

    @RyanCavanaugh
    Member

    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.

  13. typescript-bot commented on Aug 21, 2025

    @typescript-bot
    Contributor

    This issue has been marked as "Duplicate" and has seen no recent activity. It has been automatically closed for house-keeping purposes.

  14. locked as resolved and limited conversation to collaborators on Oct 22, 2025
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    DuplicateAn existing issue was already created

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions