GV

GeezerVison

The World Wide Web, restored to 800 pixels of glory.

Home|About|Gaming cabinet|Archives|How it works|Starter sites

On this site

  • Sign in
  • ActionsCI/CD and automation
  • CodespacesCloud development environments
  • All Features
  • Documentation
  • Training
  • Certifications
  • Build with Copilot
  • Customize Copilot
  • Integrations
  • Trending repos
  • GitHub Sponsors
  • Developer articles
  • Enterprises

This view

  • Original page
  • New address

GeezerVison is showing github.com — Suggestion: Const contexts for generic type inference · Issue #30680 · microsoft/TypeScript

Suggestion: Const contexts for generic type inference #30680

New issue
Copy link
New issue
Copy link
Closed
#51865
Closed
Suggestion: Const contexts for generic type inference#30680
#51865
Copy link
Labels
In DiscussionNot yet reached consensusNot yet reached consensusSuggestionAn idea for TypeScriptAn idea for TypeScript

Description

@jcalz
Joe Calzaretta (jcalz)
opened on Apr 1, 2019
Issue body actions

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; // 🧠💥🤪

// also possible, see #46937:
declare function baz<T extends object>(x: const 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:

  • 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, etc.)
  • This feature would agree with the rest of TypeScript's Design Goals.

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.

Reacted by Titian Cernicova-Dragomir, Abraham White, jwbay, NN, Nick Clifford, Alec Larson, Umed Khudoiberdiev, Haz, François de Campredon, Sam Beran and 104 more
Reacted by Luiz Felipe Frazão, Bruno Fantauzzi, Alfonso Andrés López Molina and Thundercraft5
Reacted by Titian Cernicova-Dragomir, Alec Larson, Sam Beran, Mehdi Lahlou, Mateusz Kasprzak, Stephen Haberman, Michael Hesler, Luiz Felipe Frazão, Bruno Fantauzzi, Salem and 4 more
Reacted by Titian Cernicova-Dragomir, Alec Larson, Sam Beran, Mateusz Kasprzak, Aravindan Ve, Michael Hesler, Luiz Felipe Frazão, Conor Rice, Gavin Cannizzaro, Bruno Fantauzzi and 7 more
Reacted by Bo Lingen, yossarian, martinwepner, Thundercraft5 and Darryl Noakes
Reacted by Luiz Felipe Frazão, Bruno Fantauzzi, Alfonso Andrés López Molina, Jo, Thundercraft5, Darryl Noakes and Yves M.

Activity

  1. Ryan Cavanaugh (RyanCavanaugh)
    added
    SuggestionAn idea for TypeScriptAn idea for TypeScript
    In DiscussionNot yet reached consensusNot yet reached consensus
    on Apr 1, 2019
  2. aleclarson commented on Apr 5, 2019

    @aleclarson
    Alec Larson (aleclarson)
    on Apr 5, 2019
    More actions

    This would allow forcing a generic parameter to be inferred as a tuple instead of an array. 👍

    Reacted by Zachiah Sawyer, Ethan Standel, James Bromwell, Jarrod Davis, Corentin Girard and Maciek Sakrejda
  3. sberan commented on May 4, 2019

    @sberan
    Sam Beran (sberan)
    on May 4, 2019
    More actions

    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)
    Reacted by Leo, falfia falfiya, Trevin Miller, Thundercraft5, roblframpton and Maciek Sakrejda
  4. jcalz commented on Dec 22, 2019

    @jcalz
    Joe Calzaretta (jcalz)
    on Dec 22, 2019
    ContributorAuthor
    More actions

    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.

    Reacted by Titian Cernicova-Dragomir and Pilou
  5. arciisine commented on Dec 22, 2019

    @arciisine
    Timothy Soehnlin (arciisine)
    on Dec 22, 2019
    More actions

    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.

  6. dirkluijk commented on Jan 23, 2020

    @dirkluijk
    Dirk Luijk (dirkluijk)
    on Jan 23, 2020
    Contributor
    More actions

    Joe Calzaretta (@jcalz) Thank you for the Narrowable workaround.

    I need this as well, to infer string literals or enum values in generics.

  7. jwulf commented on Sep 30, 2020

    @jwulf
    Josh Wulf (jwulf)
    on Sep 30, 2020
    More actions

    Another relevant SO question: https://stackoverflow.com/questions/64056538/type-signature-that-infers-all-values

    Reacted by Bruno Fantauzzi
  8. cyrilletuzi commented on Oct 15, 2020

    @cyrilletuzi
    Cyrille Tuzi (cyrilletuzi)
    on Oct 15, 2020
    More actions

    What is the status of this? I have the same issue as Sam Beran (@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.

    Reacted by Jonas, Bo Lingen, roblframpton and Maciek Sakrejda
  9. millsp commented on Dec 30, 2020

    @millsp
    pierre (millsp)
    on Dec 30, 2020
    Contributor
    More actions

    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?? 🧙‍

    Play it

    This will land soon in https://github.com/millsp/ts-toolbelt

    The advantage of this vs as const is that it can be used dynamically and works all the way down to [email protected] (inclusive)

    Not sure if Const is an appropriate name here since it does not make anything const, that's up to you to use Readonly. Maybe calling it Self or Narrow would be more concise. It solves the same problem described in the OP, nevertheless.

    Reacted by Tyler Murphy and YiJie
    Reacted by Toni Villena, Stephen Haberman, Sam Beran, Steven "Slam" Lam, tienvudev, Jens Döllmann, Mateusz Burzyński, Jack Scott, Aria Buckles, Fernando Rojo and 11 more
    Reacted by Toni Villena, Sam Beran, Asger Hallas, tienvudev, Lenz Weber-Tronic, Jack Scott, Fernando Rojo, Samer, Sagnik Pradhan, roblframpton and 4 more
    Reacted by Toni Villena, Sam Beran, tienvudev and Thundercraft5
    Reacted by reiv, Sam Beran, tienvudev, Jo and Thundercraft5
  10. KisaragiEffective commented on Jun 1, 2021

    @KisaragiEffective
    Kisaragi (KisaragiEffective)
    on Jun 1, 2021
    More actions

    maybe make Narrowed<T> is intrinsic and implement in tsc?
    If so, the imaginary - function x<T>(a: Narrowed<T>): foo can be implemented well.

    Reacted by __ and Mateusz Burzyński
  11. jcalz commented on Jun 8, 2021

    @jcalz
    Joe Calzaretta (jcalz)
    on Jun 8, 2021
    ContributorAuthor
    More actions

    Another SO question: Converting values in a dictionary to literal values

  12. leoblum commented on Jun 25, 2021

    @leoblum
    Leo (leoblum)
    on Jun 25, 2021
    More actions

    +1

    Reacted by Bo Lingen, egon, David Enke, Mindful Design and Kisaragi
  13. jcalz commented on Nov 28, 2021

    @jcalz
    Joe Calzaretta (jcalz)
    on Nov 28, 2021
    ContributorAuthor
    More actions

    Edited to reflect slightly different approach as mentioned in #46937

  14. captain-yossarian commented on Feb 5, 2022

    @captain-yossarian
    yossarian (captain-yossarian)
    on Feb 5, 2022
    More actions

    It would be great and make TS less verbose

  15. shigma commented on Apr 6, 2022

    @shigma
    Shigma (shigma)
    on Apr 6, 2022
    More actions

    #48240 provides an excellent example of type annotations. now I would prefer <const T>.

    thus we can also have in const T and in out const T constraints.

    Reacted by martinwepner
    Reacted by YiJie
  16. yujiosaka commented on Jun 21, 2022

    @yujiosaka
    yujiosaka (yujiosaka)
    on Jun 21, 2022
    More actions

    pierre (@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 Narrow type works when you pass the argument directly to functions,
    it does not work, of course, when the argument is defined beforehand without const assertion.

    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]> })
    >;

    Playground

    If it's an internal project, you can ask your teammates to always pass arguments directly.
    It's certainly much easier than forcing them to pass as const in every function call.

    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']}]}
    Reacted by falfia falfiya and Thundercraft5
  17. ExE-Boss commented on Jul 5, 2022

    @ExE-Boss
    ExE Boss (ExE-Boss)
    on Jul 5, 2022
    Contributor
    More actions

    See also:

    • Support readonly type operator to allow correct definition of Object.freeze #10725
  18. stephenh commented on Nov 16, 2022

    @stephenh
    Stephen Haberman (stephenh)
    on Nov 16, 2022
    More actions

    Fwiw I'm upgrading my project to TypeScript 4.9 and pierre (@millsp) 's Const solution is no longer working for me...

    I had been using a pattern like:

        const a1 = await em.load(Author, "1", { publisher: {}, books: { reviews: "book" } });
        expect(a1.publisher.get).toEqual(undefined);
    

    Where the 2nd param to em.load would stay a const by using the Const type:

      public async load<T extends Entity, H extends LoadHint<T>>(
        type: EntityConstructor<T>,
        id: string,
        populate: Const<H>,
      ): Promise<Loaded<T, H>>;
    

    Which would get a1 inferred as Loaded<Author, { publisher: {}, books: ... }>, which allowed the a1.publisher.get call to be legal.

    But now with TS 4.9.3, a1 is instead getting inferred as Loaded<Author, LoadHint<Author>>, which makes the .get call break.

    I can fix/force it by adding as const to call sites:

        const a1 = await em.load(Author, "1", { publisher: {}, books: { reviews: "book" } } as const);
    

    Which is going to be really verbose.

    As anyone noticed Const not working for them on TS 4.9, and if so, any working fixes?

    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 void and null from the Narrowable:

    type Narrowable = string | number | boolean | symbol | object | undefined | {} | [];
    export type Const<N> =
      | N
      | {
          [K in keyof N]: N[K] extends Narrowable ? N[K] | Const<N[K]> : never;
        };
    

    What seems odd is I have to remove both void and null; if I leave either, then it goes back to not working. ...so, that's good for me I guess, b/c I don't need void or null in the load hints that I'm Const-izing. 🤔 🤷

    Reacted by Toni Villena
  19. Andarist commented on Nov 16, 2022

    @Andarist
    Mateusz Burzyński (Andarist)
    on Nov 16, 2022
    Contributor
    More actions

    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.

  20. stephenh commented on Nov 16, 2022

    @stephenh
    Stephen Haberman (stephenh)
    on Nov 16, 2022
    More actions

    Ah hey Mateusz Burzyński (@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.

    Reacted by pierre and Kisaragi
  21. Anders Hejlsberg (ahejlsberg)
    closed this as completedin #51865on Dec 16, 2022
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    In DiscussionNot yet reached consensusNot yet reached consensusSuggestionAn idea for TypeScriptAn idea for TypeScript

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions

      You are visitor 01402503 — GeezerVison does not store the pages you look up.
      Best viewed with Netscape Navigator 4.7 or Internet Explorer 6 at 800×600. No cookies required.