-
Notifications
You must be signed in to change notification settings - Fork 12.9k
Description
π Search Terms
generic infer
generic typeof
typeof arguments
infer-only generics
infer arguments
unoverridable 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.