-
Notifications
You must be signed in to change notification settings - Fork 12.8k
Introduce "must-infer" generics to restrict user-specified type overrides #59814
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. Weβll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Comments
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. const value = 33 as number | string;
foo.attribute("age", value) |
Please edit to show the actual undesirable case, which looks like If I wanted such a thing, I'd probably write something like public attribute<
X extends "Please don't manually specify these generic type arguments",
Name extends string, Type>(name: Name, value: Type) {
this.attributes.set(name, value);
return this as Example<Attributes & { [K in Name]: Type }>;
} 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). 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 function foo<T = string>(f: () => T = () => ("abc" as T)): T {
return f();
}
console.log(foo(()=>123).toFixed(2)); // "123.00" π
console.log(foo().toUpperCase()); // ABC π
console.log(foo<number>().toFixed(2)); // RUNTIME ERROR! π But I don't know that this is compelling enough either. |
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 function example(value: unknown): typeof value {
return value;
}
example(123); // type is unknown
example("abc"); // type is unknown To address this, we can use generics so that TypeScript can determine the type based on the argument: function example<T>(value: T): T {
return value;
}
example(123); // type is number 123, specifically
example("abc"); // type is string "abc", specifically 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). function example<internal T>(value: T): T {
return value;
}
example(123); // type is number 123, specifically
example("abc"); // type is string "abc", specifically
example<number>(123); // Error: type T is internal 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 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: const User = schema("user")
.attribute("name", String)
.attribute("age", Number)
typeof User.getAttributes() is { name: string, age: number } To achieve this, 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. function example<infer N extends string>(name> N): Uppercase<N>; // or
function example(name: infer N extends string): Uppercase<N>; // or
// typeof example("abc") === "ABC"
function example<T>(name: infer N extends string, type: T) { ... }
// example("abc", 123) will have generic T = number, N is internally "string" but not accessible to userland. |
Let's say this feature existed. What would you do when someone writes |
@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 So in your example, the type would indeed be inferred as 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 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. |
Speaking on motivation, let me chime in with an extension of the example for
Which allows for createStreetLight<"red"|"yellow"|"green"|"blue">(["red", "yellow", "green"], "blue"); // OK, but... ugh With the proposed feature function createStreetLightProposed(
colors: (infer C extends string)[],
defaultColor?: C,
)
createStreetLightProposed(["red", "yellow", "green"], "blue"); // Should Error, and there's no <...> way to override The parameters could still be typecast, ... but this is arguably more natural since the order of the type Lights = "red"|"yellow"|"green"|"blue";
createStreetLightProposed(["red", "yellow", "green"] as Lights[], "blue"); // OK
// note that this also works for the existing NoInfer example...
createStreetLight(["red", "yellow", "green"] as Lights[], "blue"); // OK This feels related to the problem Java solves with
|
But you can still always do: const colors: ("red" | "yellow" | "green" | "blue")[] = ["red", "yellow", "green"];
createStreetLightProposed(colors, "blue"); 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. |
Yep! That's exactly the point that the following @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 |
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. |
This issue has been marked as "Declined" and has seen no recent activity. It has been automatically closed for house-keeping purposes. |
@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: route<Path extends string, Params = ExtractParams<Path>>(
path: Path,
component: React.ComponentType<Params>,
): Route<Params>; I have a utility type ( With the proposed concept, I could do something like this: route<infer Path extends string, Params = ExtractParams<Path>>(
path: Path,
component: React.ComponentType<Params>,
): Route<Params>;
// The "Path" parameter is nonexistent
route<{ id?: string }>('{/:optional}', OptionalComponent) I don't really care if the consumer overrides the Path type manually like route<Params = ExtractParams<const typeof path>>(
path: string,
component: React.ComponentType<Params>,
): Route<Params> Grabbing a reference to the const type of the argument without declaring a generic parameter |
π Search Terms
must-infer generic
immutable generic
avoid override generic
β Viability Checklist
β 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:
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:
π 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 itsvalue
. To achieve this, I do the following:However, note that I can "subvert" the type by overriding the generic:
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:
From there, TypeScript itself would infer the types of
Name
andType
, 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:This doesnβt achieve the desired precision, as it leads to overly generalized types.
The text was updated successfully, but these errors were encountered: