-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Suggestion: Const contexts for generic type inference #30680
Comments
This would allow forcing a generic parameter to be inferred as a tuple instead of an array. 👍 |
This would be incredible. I have an API for json schema which currently requires the caller to type // $ExpectType Schema<string>
schema({
type: ['string']
} as const)
// $ExpectType Schema<string | number>
schema({
type: ['string', 'number']
} as const) |
Relevant SO question: Dynamically generate return type based on array parameter of objects in TypeScript My current answer involves adding a dummy type parameter |
Closed #35821 in lieu of this. To expand upon this issue, I would also like to have support for this in Javascript, as |
@jcalz Thank you for the I need this as well, to infer string literals or enum values in generics. |
Another relevant SO question: https://stackoverflow.com/questions/64056538/type-signature-that-infers-all-values |
What is the status of this? I have the same issue as @sberan: inferring the type from a JSON Schema would currently require interface JsonSchemaNumber {
type: 'number';
}
interface JsonSchemaString {
type: 'string';
}
type JsonSchema = JsonSchemaNumber | JsonSchemaString;
type SchemaToType<Schema extends JsonSchema> =
Schema extends { type: 'string' } ? string :
Schema extends { type: 'number' } ? number :
unknown;
function test<T extends JsonSchema>(schema: T): SchemaToType<T> | void {
if (schema) {}
}
const int = { type: 'number' };
test(int); Fails on function calling parameter with error:
While the following work: const int = { type: 'number' } as const;
test(int); Meaning the caller must do |
Hi all, I think that I have finally solved this puzzle. We can now control const contexts on the behalf of the user. There we go: declare function foo<A>(x: Narrow<A>): A;
declare function bar<A extends object>(x: Narrow<A>): A;
const test0 = foo({a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]});
// `A` inferred : {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]}
const test1 = bar({a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]});
// `A` inferred : {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]}
const test3 = foo('hello jcalz <3');
// `A` inferred : 'hello jcalz <3'
type Cast<A, B> = A extends B ? A : B;
type Narrowable =
| string
| number
| bigint
| boolean;
type Narrow<A> = Cast<A,
| []
| (A extends Narrowable ? A : never)
| ({ [K in keyof A]: Narrow<A[K]> })
>;
// 🧙 what sorcery is this now?? 🧙 This will land soon in https://github.com/millsp/ts-toolbelt
|
maybe make |
Another SO question: Converting values in a dictionary to literal values |
+1 |
Edited to reflect slightly different approach as mentioned in #46937 |
It would be great and make TS less verbose |
#48240 provides an excellent example of type annotations. now I would prefer thus we can also have |
@millsp 's solution solved most of my problems like a charm! But I want to share one corner case to push this issue forward, which is providing a library to anonymous people. Although the declare function foo<A>(x: Narrow<A>): A;
declare function bar<A extends object>(x: Narrow<A>): A;
const param0 = {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]};
const test0 = foo(param0);
// not inferred as : {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]}
// but inferred as : {a: number, b: string, d: (string | number | boolean | {f: string[]})[]}
type Cast<A, B> = A extends B ? A : B;
type Narrowable =
| string
| number
| bigint
| boolean;
type Narrow<A> = Cast<A,
| []
| (A extends Narrowable ? A : never)
| ({ [K in keyof A]: Narrow<A[K]> })
>; If it's an internal project, you can ask your teammates to always pass arguments directly. But when you are providing a library, people may not follow your advice always. What I expect with this proposal is that following code fails to compile because passed argument can be modified (thus the types cannot be narrowed). declare function baz<const T extends object>(x: T): T; // I don't have a preference in which style
const param0 = {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]};
const test0 = foo(param0); // it shouldn't compile It should accept const argument. declare function baz<const T extends object>(x: T): T;
const param0 = {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]} as const;
const test0 = foo(param0); // inferred as : {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]} Also it would be nice if it automatically asserts argument as const when it's passed directly to the function (as originally proposed in this issue). declare function baz<const T extends object>(x: T): T;
const test0 = foo({a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]}); // inferred as : {a: 1, b: 'c', d: ['e', 2, true, {f: ['g']}]} |
Fwiw I'm upgrading my project to TypeScript 4.9 and @millsp 's I had been using a pattern like:
Where the 2nd param to
Which would get But now with TS 4.9.3, I can fix/force it by adding
Which is going to be really verbose. As anyone noticed Thanks! ... Huh, I'm surprised, the 1st "poke something simple and see what happens" got it by to working for my specific use cases by removing
What seems odd is I have to remove both |
It would be best if you could share a full inspectable TS playground with a report like this. It's hard to know what exactly might have changed if you don't share a repro case. |
Ah hey @Andarist ! Yeah, that's a very fair ask; I was admittedly being lazy and hoping someone would have already hit an error and had a new/updated snippet. I'll work on a playground-able repro of my issue. |
Search Terms
const contexts; literals; narrowing; generics;
Suggestion
Allow const contexts for generic type parameter inference.
Often I've written and seen generic functions where the type parameter inference is intended to be as narrow as possible. Accomplishing this involves several "tricks" with generic constraints. It is more reminiscent of alchemy than I would like, and places quite a burden on the function signature and anyone with the misfortune of having to read and understand it:
Const contexts (#29510) are exactly the knob we want to turn here, and the "
as const
" syntaxis succinct, understandable, and non-mind-bending. Unfortunately, the only way to use this in
generic functions is from the caller's side, which is hard to guarantee:
The suggestion here is to get the best of both worlds by allowing a const context to be specified in the generic type parameter declaration:
Related issues
#29510: const contexts
#10676: generics infer literals "T extends string | number | boolean"
#27179: generics infer tuples "T extends U[] | [U]"
#13347: probably can't make generics infer readonly
#16896: please narrow all object literals as much as possible
Checklist
My suggestion meets these guidelines:
EDIT: mentioning slightly different approach from #46937 where modifier goes on the type parameter instead of its constraint. It's not obvious to me if one has an advantage over the other.
The text was updated successfully, but these errors were encountered: