const contexts; literals; narrowing; generics;
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:
// 🧙 what sorcery is this?? 🧙
type Narrowable = string | number | boolean | symbol | object | undefined | void | null | {};
declare function foo<N extends Narrowable, T extends { [k: string]: N | T | [] }>(x: T): T;
foo({ a: 1, b: "c", d: ["e", 2, true, { f: "g" }] });
// T inferred as { a: 1; b: "c"; d: ["e", 2, true, { f: "g"; }]; }
Const contexts (#29510) are exactly the knob we want to turn here, and the "as const" syntax
is 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:
declare function bar<T extends object>(x: T): T;
bar({ a: 1, b: "c", d: ["e", 2, true, { f: "g" }] } as const); // burden on function caller
// T inferred as {
// readonly a: 1; readonly b: "c"; d: readonly ["e", 2, true, { readonly f: "g"; }];
// }
bar({ a: 1, b: "c", d: ["e", 2, true, { f: "g" }] }); // oops!!
// T inferred as {
// a: number; b: string; d: (string | number | boolean | { f: string; })[];
// } 😢
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:
declare function baz<T extends const object>(x: T): T; // 🤔
declare function baz<T extends object as const>(x: T): T; // 🤷
declare function baz<const T extends object>(x: T): T; // 😵
declare function baz<const T const extends readonly object as const>(x: T): T; // 🧠💥🤪
baz({ a: 1, b: "c", d: ["e", 2, true, { f: "g" }] });
// T inferred as {
// readonly a: 1; readonly b: "c"; d: readonly ["e", 2, true, { readonly f: "g"; }];
// } ❤🎉
My suggestion meets these guidelines:
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 as const everywhere:
// $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 S extends string to get string literal narrowing to happen in a different type parameter T extends {fieldName: S}; it reminded me that it would be so nice to have some more transparent syntax like T extends {fieldName: const string} (or something) here.
Closed #35821 in lieu of this. To expand upon this issue, I would also like to have support for this in Javascript, as as const is not applicable outside of .ts files.
@jcalz Thank you for the Narrowable workaround.
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 as const everywhere on the caller side:
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:
Argument of type '{ type: string; }' is not assignable to parameter of type 'JsonSchema'.
Type '{ type: string; }' is not assignable to type 'JsonSchemaString'.
Types of property 'type' are incompatible.
Type 'string' is not assignable to type '"string"'.
While the following work:
const int = { type: 'number' } as const;
test(int);
Meaning the caller must do as const everywhere, which is not a reasonable option.
Most helpful comment
Relevant SO question: Dynamically generate return type based on array parameter of objects in TypeScript
My current answer involves adding a dummy type parameter
S extends stringto get string literal narrowing to happen in a different type parameterT extends {fieldName: S}; it reminded me that it would be so nice to have some more transparent syntax likeT extends {fieldName: const string}(or something) here.