Skip to content

Infer-only function genericsΒ #62182

@rintaun

Description

@rintaun

πŸ” 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.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions