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 — Support Const Type Constraint · Issue #41114 · microsoft/TypeScript

Support Const Type Constraint #41114

New issue
Copy link
New issue
Copy link
Closed
#51865
Closed
Support Const Type Constraint#41114
#51865
Copy link
Labels
Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureSuggestionAn idea for TypeScriptAn idea for TypeScript

Description

@woshiguabi
woshiguabi
opened on Oct 15, 2020
Issue body actions

Search Terms

Const Type Constraint Literal

Suggestion

Add new syntax const T or a global generic type Constant<T = any> = T extends const T ? T : never;

type ConstString = const string is same as type ConstString = Constant<string>.

  • If T extends string | number | boolean | symbol | bigint | null | undefined, requires T to be a constant.
  • If T is object or Array, requires every member of T to be a constant.
  • If T is enum, nothing to do, because enum is same as const enum

The difference with union literal is that type 'foo' | 'bar' can only be 'foo' or 'bar', but type const string can be any constant string.

Use Cases

  • Sometimes we want to constrain a type or a parameter to be literal.
  • It can help the TS compiler to recognize constant types better

Examples

let foo = 'foo'; // string
const bar= 'bar'; // 'bar'

const notConstant: const string = foo; // error
const trulyConstant: const string = bar; // success

type Baz = { baz: string } ;
const baz1: const Baz = { baz: 'baz' }; // success
const baz2: Baz = { baz: 'baz' }; // not a constant
const baz3: Baz  = { baz: 'baz' } as const; // current available, same as baz1

type ConstArray<T> = const Array<T>;
// same as
type AnotherCosntArray<T> = Array<const T>;

function tuple<T extends ConstArray>(...args: T) {
  return args;
}
const t1 = tuple(foo); // error!
const t2 = tuple(bar); // success! typeof t2 should be ['bar']
const t3 = tuple('a', 1, true, ['1'], baz1) // ['a', 1, true, ['1'], { baz: 'baz' }];
const t4 = tuple(baz2) // error!

let variable: const string = 'variable'; // this is possible
variable = 'another variable'; // success
variable = document.title; // error

let reactiveObject: const { foo: string } = { foo: 'foo' }; // success;
reactiveObject.foo = 'bar' // success

In fact, const only affects the assignment behavior, and a const type is considered to be the same as a non const type when read it.
A const type doesn't mean immutable, just means every primitive value should be literal, so maybe should call it literal type.

Alternatives

  • Only add extends const syntax, and add global generic type Constant<T = any> = T extends const ? T : never;
  • Use finally or static or literal(not exist in current keywords) keyword instead of const
  • Use type Literal instead of type Constant.

Related issues

#30680 Suggestion: Const contexts for generic type inference

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.
Reacted by Michael Borislav, Harry Solovay, NN, Guillaume Esquevin, Remi Rousselet, whzx5byb, Rafael Marques, Malcolm Still, jp2masa, Ilya Radchenko and 8 more

Activity

  1. MartinJohns commented on Oct 15, 2020

    @MartinJohns
    Martin Johns (MartinJohns)
    on Oct 15, 2020
    Contributor
    More actions

    Sometimes we want to constrain a type or a parameter to be literal.

    Can you provide a real world example where this would be useful?

  2. woshiguabi commented on Oct 15, 2020

    @woshiguabi
    woshiguabi
    on Oct 15, 2020
    Author
    More actions

    Sometimes we want to constrain a type or a parameter to be literal.

    Can you provide a real world example where this would be useful?

    function tuple<T extends Array<string | number>>(...args: T) {
      return args;
    }
    const t1 = tuple('a', 'b'); // ['a', 'b']
    let bar = 'bar';
    const t2 = tuple('foo', bar); // ['foo', string]

    For intellisense, parameter must be literal.

  3. Constantiner commented on Oct 15, 2020

    @Constantiner
    Konstantin Kovalev (Constantiner)
    on Oct 15, 2020
    More actions

    Why not to use just:

    function tuple<T extends Array<string | number>>(...args: T) {
      return args;
    }
    const t1 = tuple('a', 'b'); // ['a', 'b']
    let bar = 'bar' as const;
    const t2 = tuple('foo', bar); // ['foo', 'bar']

    ?

  4. woshiguabi commented on Oct 15, 2020

    @woshiguabi
    woshiguabi
    on Oct 15, 2020
    Author
    More actions

    Why not to use just:

    function tuple<T extends Array<string | number>>(...args: T) {
      return args;
    }
    const t1 = tuple('a', 'b'); // ['a', 'b']
    let bar = 'bar' as const;
    const t2 = tuple('foo', bar); // ['foo', 'bar']

    ?

    tuple may come from package.
    The main goal is to constrain function parameters must be literal.
    Now we have no way to diff const type and variable type.

  5. MartinJohns commented on Oct 15, 2020

    @MartinJohns
    Martin Johns (MartinJohns)
    on Oct 15, 2020
    Contributor
    More actions

    The main goal is to constrain function parameters must be literal.

    But you didn't provide an example where this would be useful.

    Reacted by Ryan Cavanaugh
  6. Ryan Cavanaugh (RyanCavanaugh)
    added
    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this feature
    SuggestionAn idea for TypeScriptAn idea for TypeScript
    on Oct 16, 2020
  7. tjjfvi commented on Oct 22, 2020

    @tjjfvi
    T6 (tjjfvi)
    on Oct 22, 2020
    Contributor
    More actions

    This could be accomplished with the conditionally assignable types discussed in #40779.

    type StrictSubtype<T> = match U extends T => T extends U ? false : true;
    type ConstString = StrictSubtype<string>;
    Reacted by woshiguabi
  8. harrysolovay commented on Oct 22, 2020

    @harrysolovay
    Harry Solovay (harrysolovay)
    on Oct 22, 2020
    More actions

    The main use case––seems to be––to help library developers ensure that consumers are passing narrowed objects:

    // BAD
    acceptsNarrowed({
      a: "A",
      b: true,
      c: 3,
    });
    
    // GOOD
    acceptsNarrowed({
      a: "A",
      b: true,
      c: 3,
    } as const);

    This feature would really shine! I've run into this problem many times. The following would be incredibly useful to me personally.

    declare function acceptsNarrowed<O extends Constant<Record<PropertyKey, any>>>(o: O): void;
    // or
    declare function acceptsNarrowed<O extends const Record<PropertyKey, any>>(o: O): void;
    Reacted by Slick Kilmister, Michael Borislav, Martin Heidegger, Remi Rousselet, Maxim Kot, Philipp Uhl and BrodBlox09
  9. KilianKilmister commented on Oct 25, 2020

    @KilianKilmister
    Slick Kilmister (KilianKilmister)
    on Oct 25, 2020
    More actions

    The main goal is to constrain function parameters must be literal.

    But you didn't provide an example where this would be useful.

    I have to agree with Harry Solovay (@harrysolovay). I run into problems around this regularly, usually with some complex type-converting breaking down when it resieves non-literal string or number (especially when using the upcoming TempateString-literal type).

    Also, i think this could solve many of the issues related to the notoriously strange symbol type. just to mention a few:

    • precise symbol being returned from a function
    • specifying a precise symbol in function parameters
    • dynamic object keying with symbols
    • symbol indexing

    This would make it possible to give symbols to an API that can be referenced at a later point, both internally and by the API consumer.

    // has the incredibly useless returntype of `{[x: string]: string }`
    function someApi <T extends symbol>(someSymbol: T) {
      return {[someSymbol]: 'dynamic symbol key'}
    }
    
    // return type is `symbol`, making it impossible to reference it again in the typeSystem.
    function GetDynamicallyUniqueSymbol () {
      const sym = Symbol('unique')
      return sym
    }
    
  10. woshiguabi commented on Oct 25, 2020

    @woshiguabi
    woshiguabi
    on Oct 25, 2020
    Author
    More actions

    After a few thinking, I think there are still some unresolved problems in the proposal.


    Object and Array type probably shouldn't extends const to every member, because of prototype.

    const foo = (arr: Constant<Array<string>>) => arr;

    If object member extends const, Array.prototype.length will be const, so this function cannot be called.

    Use this instead.

    const bar = (arr: Array<Constant<string>>) => arr;

    What about funciton type? Seems function type should not be affected by const.

    We need more discussion.


    This could be accomplished with the conditionally assignable types discussed in #40779.

    type StrictSubtype<T> = match U extends T => T extends U ? false : true;
    type ConstString = StrictSubtype<string>;

    T6 (@tjjfvi) That will be a greet solution! I think generic is much better than a new syntax.

  11. KilianKilmister commented on Oct 26, 2020

    @KilianKilmister
    Slick Kilmister (KilianKilmister)
    on Oct 26, 2020
    More actions

    woshiguabi

    That will be a greet solution! I think generic is much better than a new syntax.

    I have to be nitpickey here, but this is also new syntax. I also don't think this would address the issue in this thread. They do have overlap tho.

    const foo = (arr: Constant<Array<string>>) => arr;

    If object member extends const, Array.prototype.length will be const, so this function cannot be called.

    I don't see a reason why this couldn't be called. I would interpret that as: the function only takes arrays with a known length (so eg. no string[]), but the specific length doesn't matter.
    But it's good that you're brining these thing up.

    What about funciton type? Seems function type should not be affected by const.

    I have now idea how const should behave on a function directly. Maybe make everything that can be a litteral require a literal? But i don't think that would be a good idea as people could do the same with the individual params/return types. I don't think it should be usable on a function type, but it should stay similarly limited like a as const assertion.

    There has been a want for something like this (or like the other proposal) for quite some time. There was a suggestion about extending the unique type to handle most of these things. I can't find the issue which suggested it right now, but it was quite popular and if i remember correctly, there were steps being taken to implement it.I think it was ultimatly shafted for some newly discovered technical reason.
    If anybody knows which issue i'm talking about and happens to find it, it could potentially add quite a bit to these discussions. Might also be worth evaluating wether it could be implemented now, because it was discussed in length it could save us a lot of effort as would have to discuss very similar thing eventually

  12. woshiguabi commented on Oct 28, 2020

    @woshiguabi
    woshiguabi
    on Oct 28, 2020
    Author
    More actions

    Slick Kilmister (@KilianKilmister)

    I have to be nitpickey here, but this is also new syntax. I also don't think this would address the issue in this thread. They do have overlap tho.

    😜Thanks much! I mean no need for another new syntax.

    https://github.com/tc39/proposal-record-tuple
    Notice this stage 2 proposal, maybe we will finally achieve our goal through this

  13. Andarist commented on Apr 11, 2021

    @Andarist
    Mateusz Burzyński (Andarist)
    on Apr 11, 2021
    Contributor
    More actions

    Ryan Cavanaugh (@RyanCavanaugh) what's the process for adding this to the language by the external contributor? is this proposal non-destructive enough that a PR could be made and there would be a high chance of merging it after refining the details during the code review process?

  14. malcolmstill commented on Aug 17, 2022

    @malcolmstill
    Malcolm Still (malcolmstill)
    on Aug 17, 2022
    More actions

    I'm trying to do something that I believe the above suggestion would allow.

    Let's say I want to use https://github.com/vultix/ts-results, but I'd like to:

    • restrict all errors in my program to be a particular shape
    • get exhaustive checking of an error type with a kind or type field

    If I wrapts-results Err function so that it takes <E extends string> (giving a new MyErr type say), I can do:

    function double(a: number) {
        if (a < 0.0) {
            return Err({type: 'ERROR_LESS_THAN_ZERO' as const});
        } else if (a > 100.0) {
            return Err({type: 'ERROR_MORE_THAN_ONE_HUNDRED' as const});
        }    
    
        return Ok(a * 2.0);
    }

    ...and I am able to get completion and exhaustive checking of those type tags. I.e. the return type inference gives:

    function double(a: number): MyErr<{
        type: "ERROR_LESS_THAN_ZERO";
    }> | MyErr<{
        type: "ERROR_MORE_THAN_ONE_HUNDRED";
    }> | TSResults.Ok<number>

    But the problem is if I forget to add an as const on any of my errors I get this:

    function double(a: number): MyErr<{
        type: string;
    }> | TSResults.Ok<number>

    ...and I've lost the discriminated union of type tags.

    I'd love some way to say <E extends const string> (instead of <E extends string>) and so be forced to add as const on the type: field (or even better, not need to add the as const at all)

  15. tjjfvi commented on Aug 17, 2022

    @tjjfvi
    T6 (tjjfvi)
    on Aug 17, 2022
    Contributor
    More actions

    Malcolm Still (@malcolmstill) You can use the Narrow utility type for that:

    type Narrow<T> =
      | (T extends infer U ? U : never)
      | Extract<T, number | string | boolean | bigint | symbol | null | undefined | []>
      | ([T] extends [[]] ? [] : { [K in keyof T]: Narrow<T[K]> })
    
    declare function Err<E extends { type: string }>(error: Narrow<E>): E;
    
    const e = Err({ type: 'ERROR_LESS_THAN_ZERO' }) // no `as const` needed
    //    ^? - { type: "ERROR_LESS_THAN_ZERO" }
  16. malcolmstill commented on Aug 18, 2022

    @malcolmstill
    Malcolm Still (malcolmstill)
    on Aug 18, 2022
    More actions

    T6 (@tjjfvi) thanks, as you say Narrow lets me get rid of the as const, but let me clarify why that isn't sufficient for what I'm trying to do. If I write double in a different way:

    function double(a: number, someString: string) {
        if (a < 0.0) {
            return Err({type: 'ERROR_LESS_THAN_ZERO' as const});
        } else if (a > 100.0) {
            return Err({type: 'ERROR_MORE_THAN_ONE_HUNDRED' as const});
        }  else if (a > 50.0) {
            return Err({type: someString}); // I would like this to be a compile error because I want to enforce that type is const
        }
    
        return Ok(a * 2.0);
    }

    The above compiles, but collapses the return type to:

    function double(a: number): MyErr<{
        type: string;
    }> | TSResults.Ok<number>

    ...I want it instead to not compile, and force me to make type: const

  17. knownasilya commented on Sep 24, 2022

    @knownasilya
    Ilya Radchenko (knownasilya)
    on Sep 24, 2022
    More actions

    A use case I've run into needs this sort of functionality. https://www.typescriptlang.org/pt/play?#code/C4TwDgpgBACghgJwM4QKoIDbwXAtkgHgFdMA+KAXihIyggA9gIA7AEySgAMASAbwEtmAMwgIoYOMAAWAXwAUfQSLEB7MMH4rmcLJNkBKTgCgoUAPyxEKdLpz4CE6eQBklhBp0Fs1zNjyE1DS0deCdSEygALmpMOkYWdi5FYVEoJGBEYBkAemTlKAQIdJljUwtvNF9EfwJ0zJc3H1sawvTw02iaOKY2Dk5IvNSJOxKIi14oAG0AayhBcWrcAF1o9IRBAHMoGQjo3hkAbiMjAGMMOCQOACUVIiYEAgAVboTriBOVBFZa4HXmDYANGtNqRyLwIqZcHAwNFHkcIVAPsw1kQTsBPnIoTCoI99FBwaZCYTpPwkAA6LGUKBYo5EnYItQsAhQADSL16UGmEBAKiEONIckcUkiLIBCzsSGiFRsfnsj0mLKWpH00QAbip+Kx8QiiYVgCRmLTCTt6Uj0gVbvcqcwIAB3KA3O6iOQEwkAIxUKmmkqgAHJsh6vUhfQCEb7A96yRHfdF-RGkNlIhGAJKsX1GGRQC6IrTpfTHBCW0RkxnMOThz2R6Ni-b50654AWp0IABM1rtDqLCBdCPjsYDleDoaJFaDUcrMb9A6DiZTaYzdcLzZbJcgZdHVYnNZk+aAA

    Basically it's hard to infer from strings when the input isn't a const.

    Linked code
    
    type ParseUrlParams = url extends `${infer path}(${infer optionalPath})`
      ? ParseUrlParams & Partial>
      : url extends `${infer start}/${infer rest}`
      ? ParseUrlParams & ParseUrlParams
      : url extends `:${infer param}`
      ? { [k in param]: string }
      : {};
    

    class Router<T extends Record<string,string>> {
    map: T;

    constructor(map: T) {
        this.map = map;
    }
    
    open< K extends keyof T>(path:K, params: ParseUrlParams<T[K]>): void {
        return;
    }
    

    }

    const router = new Router({
    books: '/books',
    'books.book': '/books/:bookId'
    } as const)

    router.open('books.book', {})

    const router2 = new Router({
    books: '/books',
    'books.book': '/books/:bookId'
    })

    router2.open('books.book', {})

  18. Flutterish commented on Nov 25, 2022

    @Flutterish
    Peri (Flutterish)
    on Nov 25, 2022
    More actions

    I have a use case for this where I provide a const literal as a function parameter which decodes data into the format which the literal specifies - for example decode( someDataView, { someData: 'u16', otherData: 'string' } as const ). It gives really weird errors when the format is not const.

  19. zen0wu commented on Dec 4, 2022

    @zen0wu
    Zeno Wu (zen0wu)
    on Dec 4, 2022
    More actions

    (Centralizing the conversation from #51745)

    I want to define an API for users to specify a fixed path in an object, consider

    type Obj = {
      a: {
        b: {
          c: "123"
        }
      }
    }
    
    type Get<T, K> = K extends keyof T ? NonNullable<T[K]> : never
    
    type GetPath<T, P> = P extends PathKey[]
    	? P extends []
    		? T // If P is empty, return T.
    		: P extends [infer K, ...infer Rest]
    		? GetPath<Get<T, K>, Rest> // Else, recurse.
    		: never
    	: never
    
    function set<T, P extends string[]>(obj: T, path: P, value: GetPath<T, P>) {}
    
    set(
      obj, 
      ["a", "b", "c"],
      value
    )

    Currently, typescript would infer this set call's P as string[], hence value will be inferred as never.

    This however, works when P extends string, so one way of doing this would be

    function set<T, P extends string>(obj: T, path: [P], value: unknown) {}
    function set<T, P1 extends string, P2 extends string>(obj: T, path: [P1, P2], value: unknown) {}
    function set<T, P1 extends string, P2 extends string, P3 extends string>(obj: T, path: [P1, P2, P3], value: unknown) {}
    // ...

    This feels both cumbersome and not scalable.

    With the proposed change, it'll look like

    funciton set<T, P extends const string[]>(obj: T, path: P, value: GetPath<T, P> {}

    On the other hand, sometimes people do want their generics to be inferred as string, not the literal passed in.

    Allowing a const bound would allow developers to express precisely what they want.

    Reacted by ec-son
  20. Andarist commented on Dec 6, 2022

    @Andarist
    Mateusz Burzyński (Andarist)
    on Dec 6, 2022
    Contributor
    More actions

    Zeno Wu (@zen0wu) This one actually can be done quite easily even today:
    TS playground

    The only drawback to this is that I have no idea how to make autocomplete work for this - without computing all possible paths eagerly.

  21. zen0wu commented on Dec 6, 2022

    @zen0wu
    Zeno Wu (zen0wu)
    on Dec 6, 2022
    More actions

    Mateusz Burzyński (@Andarist) good to know, thank you!

    just so that i understand correctly, doing [...P] kind of enforces TS to infer it as a tuple type? is that a recommended best practice?

  22. Andarist commented on Dec 6, 2022

    @Andarist
    Mateusz Burzyński (Andarist)
    on Dec 6, 2022
    Contributor
    More actions

    just so that i understand correctly, doing [...P] kind of enforces TS to infer it as a tuple type?

    Yes

    is that a recommended best practice?

    Not sure. It's what I do though :P it seems to me like a way better approach than the older Tuple constraint workaround (type Tuple<T> = T[] | [T])

  23. zen0wu commented on Dec 7, 2022

    @zen0wu
    Zeno Wu (zen0wu)
    on Dec 7, 2022
    More actions

    just so that i understand correctly, doing [...P] kind of enforces TS to infer it as a tuple type?

    Yes

    is that a recommended best practice?

    Not sure. It's what I do though :P it seems to me like a way better approach than the older Tuple constraint workaround (type Tuple<T> = T[] | [T])

    Mateusz Burzyński (@Andarist) Actually I tried this out. It has this weird behavior where.. it works when the definition of the variable and the type is in the same file, but when they're in different files, this breaks (TS falls back to [string] on P), I don't understand why...

  24. Andarist commented on Dec 8, 2022

    @Andarist
    Mateusz Burzyński (Andarist)
    on Dec 8, 2022
    Contributor
    More actions

    Not sure if I understand - this only impacts how T might be inferred. If your variable is in a different file then it doesn't participate in the inference on the call site.

    If you'd like to ensure that you can't pass any values that are not literals etc - then this might get more tricky. You can try to use this remapping technique with the presented Validate:
    TS playground

    In more complex scenarios it might be tricky to actually validate your things this way but usually you can get good results

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

    Awaiting More FeedbackThis means we'd like to hear from more people who would be helped by this featureThis means we'd like to hear from more people who would be helped by this featureSuggestionAn 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.