Repository navigation
Introduce "must-infer" generics to restrict user-specified type overridesΒ #59814
Description
π Search Terms
must-infer generic
immutable generic
avoid override generic
β 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
TypeScript currently allows developers to specify generic types explicitly, which can override TypeScriptβs inferred types. While this flexibility is powerful, there are cases where it is essential to prevent the user from altering the generic type, ensuring that the type inference mechanism retains full control.
I propose adding a "must-infer" modifier for generics, where the TypeScript compiler would infer the type without allowing any user overrides. The syntax could be as follows:
function test<infer X>(value: X): X {
return value;
}This approach enforces that X is determined solely by the TypeScript compiler based on the input value, and it cannot be explicitly set by the user.
For example:
test<number>(123); // Error: X cannot be set by the userπ Motivating Example
This feature enhances type safety by ensuring that the generic type is always determined by TypeScriptβs inference system, which is particularly useful in cases where the generic type must match the input type exactly. This prevents potential misuse or errors that could arise from explicitly overriding the inferred type.
π» Use Cases
In my real-world scenario, I have a method that needs to capture the string representing the name of an attribute and the type of its value. To achieve this, I do the following:
class Example<Attributes extends object = object> {
private attributes = new Map<string, any>()
public attribute<Name extends string, Type>(name: Name, value: Type) {
this.attributes.set(name, value);
return this as Example<Attributes & { [K in Name]: Type }>;
}
}
const example = new Example()
.attribute("user", "John Doe")
.attribute("age", 33);
// typeof example ~= Example<{ user: string; age: number }>However, note that I can "subvert" the type by overriding the generic:
.attribute<number | string>("age", 33)
// typeof example ~= Example<{ user: string; age: number | string }>This is not the expected behavior. Even worse, it allows the user to manipulate my generics, while I intended to use them solely to "pass through" the input type to the output. This restricts future maintenance; if I ever want to change or remove a generic, I wouldnβt be able to do so.
In this case, the proposal would be something like:
public attribute<infer Name extends string, infer Type>(name: Name, value: Type) { ... }From there, TypeScript itself would infer the types of Name and Type, and the user wouldnβt be able to modify them. This would allow me to maintain the freedom to change my generics without worrying about breaking changes.
My other idea was to simply use typeof, but that doesnβt work well because it expands the type too broadly. For example:
public attribute(name: string, value: any) {
this.attributes.set(name, value);
return this as Example<Attributes & { [K in typeof name]: typeof value }>;
}
// typeof example ~= Example<{ [x: string]: any }>This doesnβt achieve the desired precision, as it leads to overly generalized types.
Activity
MartinJohns commentedon Aug 31, 2024
I don't see the point. You can still change the type by having an explicitly typed variable and pass in the value that way.
jcalz commentedon Aug 31, 2024
Please edit to show the actual undesirable case, which looks like
.attribute<string, number | string>("age", 33)orattribute<"age", number | string>("age", 33)or something that matches your example.If I wanted such a thing, I'd probably write something like
which would make any manual specification unlikely to occur unless someone really wanted to (and if they did, they'd probably find some way around any such feature, such as shown in the prev comment).
Playground link
I don't know that the use case shown here is compelling enough for this feature. The only time I've wanted to prevent manual specification of type arguments is when the type parameter has a default for when inference fails (as a workaround for #58977), so you'd get something like
Playground link
But I don't know that this is compelling enough either.
rentalhost commentedon Aug 31, 2024
Okay, let me try to explain my use case and the limitation that prevents me from achieving my goal. Maybe it's possible today, and you can help me, but I haven't been able to solve it yet.
TypeScript allows the use of
typeofto get the type of a parameter, but it always returns a broad type, not the specific type. Or rather, it will return the type corresponding to the parameter, and not the type of the argument. For instance, if I have something like:To address this, we can use generics so that TypeScript can determine the type based on the argument:
This is my goal. However, to achieve it, I need to declare generics that can be modified by the user. This adds an extra layer of care when determining how my generics will work in the future. But my intention isn't to make my function flexible enough for the user to decide what to do β I just want to use this important TypeScript feature internally.
So, I want to propose the possibility of creating 'invisible' generics, intended for local use and automatically determined by TypeScript (as he already does today, however, in a "transparent way", so to speak).
Why is this important?
I donβt want to worry about how I manage my generics internally in future updates, as I'm intentionally keeping them out of the user's control. I just want to rely on what TypeScript infers.
Maybe I'm talking about an "improved
typeof", but I understand the importance of how it currently works.Essentially, I just need the input type because I need to 'pass forward' exactly the type that was received. This is necessary because Iβm building a schema builder that works like this:
To achieve this,
.attribute()captures thename(as genericName) and thetype(as genericType), and then returns this retyped to consider the added attributes. Something likereturn this as Example<T & { [K in Name]: ...<Type> }>.However, this forces me to use generics where I don't actually want to use them β I only need to know what is being passed in and account for that within the function to return a new type based on the userβs input.
jcalz commentedon Sep 1, 2024
Let's say this feature existed. What would you do when someone writes
const x: number = 123; example(x);and the "internal" generic is inferred asnumberand not123? If it's okay, then why isn'texample<number>(123)okay? If it's not okay, then the feature doesn't help you.rentalhost commentedon Sep 1, 2024
Joe Calzaretta (@jcalz) the goal is to use the same existing feature as generics, but without exposing it directly to the user, since its use would be exclusively internal.
This approach also helps reduce the complexity of the function signature externally. Instead of seeing a signature like
example<number>(value: number)(note hownumberis redundant here), we would just seeexample(value: number). In this case,numberis inferred fromconst x: numberfrom your example.So in your example, the type would indeed be inferred as
number, not as123(which is not the goal anyway).The main objective, then, is to simply pass along the type received as an argument without needing to expose the existence of a generic, which serves only that purpose.
While generics can solve this, they add an unnecessary maintenance layer in certain cases. For instance, if I define the order as
<Name extends string, Type extends JsonValue>, I wouldnβt be able to change it (whether itβs the order, adding new elements before or in between, or retyping it) without causing a breaking change, even though this flexibility was never intended for the user. Think of it as a private method/property in a class.Using generics as they are today allows the user to provide the generic type themselves. This prevents me from continuing to maintain my function in the intended way. The function only used generics in this manner because it was the only possible way to solve my problem.
sparecycles commentedon Sep 1, 2024
Speaking on motivation, let me chime in with an extension of the example for
NoInferWhich allows for
With the proposed feature
The parameters could still be typecast, ... but this is arguably more natural since the order of the
<...>type parameters is no longer an artifact of the API implementation.This feels related to the problem Java solves with
?types. e.g., to deal with the exposed "implementation detail" in the generic type signature.MartinJohns commentedon Sep 1, 2024
But you can still always do:
I get the use case to control the public interface to reduce breaking changes when you decide to change the generic arguments, but you can't prevent users from providing their own types.
sparecycles commentedon Sep 2, 2024
Yep! That's exactly the point that the following
as Lights[]example trying to make.David Rodrigues (@rentalhost) identified a DRY opportunity in the type representation (with maybe a win for locality as well).
I'm actually a little more interested in avoiding
<...>for that reason since it encourages types to flow through the parameters themselves rather than function signatures.RyanCavanaugh commentedon Sep 3, 2024
TL;DR we're not going to add something we'd immediately ban on DefinitelyTyped
Inference is inference - it is ultimately guided by heuristics that try to answer the question of what the user wanted the type parameter to be, and absolutely can be "wrong" in the sense of not what the user wanted. If your functions' default inference is "good enough" that the inference is always what the user wanted, then you don't need this feature; users don't write type arguments just to help fill up their hard drives. If the default inference isn't good enough - then what? Forcing the user to upcast or downcast because a library author decided they're not allowed to provide manual type arguments is just making a bad inference someone else's problem, and would rightly be called an antipattern.
typescript-bot commentedon Sep 8, 2024
This issue has been marked as "Declined" and has seen no recent activity. It has been automatically closed for house-keeping purposes.
NuckChorris commentedon Jan 31, 2025
Ryan Cavanaugh (@RyanCavanaugh) What about a case where one of the generic type params is exclusively used to generate the default for the actual generic parameter? For example:
I have a utility type (
ExtractParams) which provides a defaultParamsbased on the type of thepathargument, but there's no reason to ever override thePathparameter β it's only used for that defaultParamstype. Because of thePathparameter, though, consumers who wish to overrideParamshave to use it likeroute<'{/:optional}', { id?: string }>('{/:optional}', OptionalComponent);passing the string to both the generic type and the function call. It's also not possible to invert and structure asroute<Params, Path>with consumers ignoring thePathparam due toTS2744: Type parameter defaults can only reference previously declared type parameters.With the proposed concept, I could do something like this:
I don't really care if the consumer overrides the Path type manually like
route<...>('' as string, OptionalComponent)and gets bad results, I just want my generic type to be ergonomic. An alternative solution for my needs could be some syntax like this:Grabbing a reference to the const type of the argument without declaring a generic parameter