Typescript: Suggestion: Const contexts for generic type inference

Created on 1 Apr 2019  ·  7Comments  ·  Source: microsoft/TypeScript

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:

// 🧙 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"; }]; 
// } ❤🎉

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:

  • [x] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
In Discussion Suggestion

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 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.

All 7 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 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.

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.

Was this page helpful?
0 / 5 - 0 ratings

Related issues

jbondc picture jbondc  ·  3Comments

blendsdk picture blendsdk  ·  3Comments

DanielRosenwasser picture DanielRosenwasser  ·  3Comments

seanzer picture seanzer  ·  3Comments

bgrieder picture bgrieder  ·  3Comments