Skip to content

Literal String Union Autocomplete #29729

Description

@MrTarantula

Autocomplete works for literal string unions, but adding a union of string negates autocomplete entirely. This has been brought up before but I believe there is enough value in this feature to be reconsidered.

My use case is to have a union of string literals for several colors, but also allow hex codes without having to add 16.7 million string literals.

TypeScript Version: 3.4.0-dev.20190202

Search Terms: Literal string union autocomplete

Code

interface Options {
  borderColor: 'black' | 'red' | 'green' | 'yellow' | 'blue' | string
};

const opts: Options = {borderColor: 'red'};

Expected behavior:

image

Actual behavior:

image

Playground Link: https://stackblitz.com/edit/typescript-bwyyab

Related Issues: #12687 #13614

Activity

  1. RyanCavanaugh commented on Feb 4, 2019

    @RyanCavanaugh
    Member

    'black' | 'red' | 'green' | 'yellow' | 'blue' | string

    From the compiler's point of view, this is just a very fancy way of writing string. By the time we're looking for suggestions at borderColor, all the literals are lost.

    You could write something like this:
    image

    Naturally this doesn't stop you from writing "bluck". You might want to track #6579

  2. sindresorhus commented on Feb 4, 2019

    @sindresorhus

    By the time we're looking for suggestions at borderColor, all the literals are lost.

    Why not improve the compiler to keep more metadata around?

  3. MrTarantula commented on Feb 4, 2019

    @MrTarantula
    Author

    You could write something like this:

    It may accomplish the same behavior, but that's not intuitive to me at all. I doubt I could have gotten there on my own, and I know I would have trouble explaining it to someone newly coming to TS from JS.

  4. spcfran commented on Mar 11, 2019

    @spcfran

    It would be great to have this built in, although understand it may be difficult to implement on the compiler.

    In the meantime, a generic workaround based on Ryan Cavanaugh (@RyanCavanaugh)'s solution might help:

    type LiteralUnion<T extends U, U = string> = T | (U & { zz_IGNORE_ME?: never })
    
    type Color = LiteralUnion<'red' | 'black'>
    
    var c: Color = 'red'                    // Has intellisense
    var d: Color = 'any-string'             // Any string is OK
    var d: Color = { zz_IGNORE_ME: '' }     // { zz_IGNORE_ME } placeholder is at the bottom of intellisense list and errors because of never 
    
    type N = LiteralUnion<1 | 2, number> // Works with numbers too

    See it in action here

  5. BendingBender commented on Mar 12, 2019

    @BendingBender

    Would be great if this could be implemented. It has the potential to improve even core Node.js APIs. The hash.digest encondig param, for example should accept plain strings but there are values available on all platforms that could be enumerated for ease of use.

  6. neurolag commented on Jun 26, 2019

    @neurolag
    Contributor

    Hey Guys
    Though the issue still isn't solved but I found out that in TS 3.5.1 you don't even have to create some weird property in order to get the workaround done:

    type LiteralUnion<T extends U, U = string> = T | (U & {});
    let x: LiteralUnion<"hello" | "world">;
    x = "hey";

    While this code is perfectly valid, "hello", and "world" are still showing up in the autocompletion.
    Thank you for this great fix, Ryan Cavanaugh (@RyanCavanaugh) and Fran Hodl (@spcfran)!

  7. frenic commented on Nov 16, 2019

    @frenic

    I noticed a problem with type guards using this hack:

    type LiteralUnion<T extends U, U = string> = T | (U & {});
    
    function something(arg: LiteralUnion<'a' | 'b'>): 'a' {
      if (arg === 'a') {
        return arg; // Type '(string & {}) | "a"' is not assignable to type '"a"'
      }
    }

    Is there a way around this?

  8. neurolag commented on Nov 18, 2019

    @neurolag
    Contributor

    I think I might have found a solution:

    Use a type like my UnpackedLiteralUnion type to unpack the actual type of the LiteralUnion:

    type LiteralUnion<T extends U, U = string> = T | (U & {});
    type UnpackedLiteralUnion<T> = T extends LiteralUnion<any, infer U> ? U : never
    
    function something(arg: LiteralUnion<'a' | 'b'>): 'a' {
        let unpackedArg = arg as UnpackedLiteralUnion<typeof arg>;
    
        if (unpackedArg === "a") {
            return unpackedArg;
        }
        else {
            return "a";
        }
    }
  9. AhmedElywa commented on Dec 20, 2019

    @AhmedElywa

    manuth Your solution not work for me can you know why ?

    image

  10. neurolag commented on Dec 20, 2019

    @neurolag
    Contributor

    Ahmed Elywa (@AhmedElywa) it's because you're using (U & never).
    Edit: After a year or something I finally noticed that my very own example was incorrect... sorry, pal 😅😂

    Here's the longer explanation:

    let x: (string & never); // x has type `never`
    let y: number | (string & never); // y has type `number`
    let z: ("Hello" | "world") | (string & never); // z has type `"Hello" | "world"`

    In order to get the solution to work you have to use U & {}.
    Following might give you an idea why this solution works.

    How the solution works

    let a: ("hello" | "world") | string;

    In this snippet a will have type string because both "hello" and "world" inherit string.

    let a: ("hello" | "world") | (string & {});

    In this code-snippet a will be of type "hello" | "world" | (string & {}) because though both "hello" and "world" inherit string, they do not inherit string & {}, which means "hello" | "world" and string & {} are treated as distinguishable types.

    Hope this helped you understanding.

  11. papb commented on Mar 22, 2020

    @papb

    For those interested, there is a workaround for this called LiteralUnion in type-fest.

  12. kotarella1110 commented on Mar 23, 2020

    @kotarella1110

    Is there a way to use this hack with the object key?

    スクリーンショット 2020-03-23 10 13 58

    TypeScript Playground

  13. RyanCavanaugh commented on May 21, 2020

    @RyanCavanaugh
    Member

    I have questions

    type SuggestingString = "foo" | "bar" | string;
    type Hmm<T> = T extends "foo" ? true : false;
    // x: false (today)
    // x: true | false or false (if this feature exists) ?
    type X = Hmm<SuggestingString>;

    Should we just do something like this?

    /**
     * @suggest "foo", "bar", "baz"
     */
    type SuggestingString = string;
  14. neurolag commented on May 21, 2020

    @neurolag
    Contributor

    Humh... especially when pursuing the goal to have non-contractual property key completion (for example keyof Employee | string) this solution would cause one to end up with something like this:

    /**
     * @suggest "PostalCode", "FullName", "FirstName", "LastName"
     */

    Unless it's possible to do something like this:

    class Employee
    {
      // [...]
    }
    
    /**
     * @suggest keyof Employee
     */
    type SuggestingString = string;

    Would that be an option? Is it possible for typescript language-server to treat the content of a tsdoc-tag (in this case suggest) as actual typescript-code?

  15. 49 remaining items

  16. jogibear9988 commented on Sep 17, 2023

    @jogibear9988

    These Solutions all do not work if you try to use them for a index signature

  17. Lonli-Lokli commented on Oct 18, 2023

    @Lonli-Lokli

    I am trying to get the same behaviour as part of object, ie kind of const guard, but with no success. But I think my issue is a little bit different, is there a corresponding TS issue?

    import { LiteralUnion } from 'type-fest';
    
    
    type Pet = LiteralUnion<'dog' | 'cat', string>;
    
    type DogHome = {
      citizens: Pet & 'dog',
      canDo: 'wow'
    }
    
    type CatHome = {
      citizens: Pet & 'cat',
      canDo: 'meuw'
    }
    
    type UnknownHome = {
      citizens: string;
      canDo?: string;
      phone?: string;
    }
    
    type AllHomes = DogHome | CatHome | UnknownHome;
    
    const home: AllHomes = {
      citizens: 'cat',
      phone: 'fff' // <- only canDo should be available
    }

    TS-play

  18. Hansenleee commented on Jan 11, 2024

    @Hansenleee

    I am trying to get the same behaviour as part of object, ie kind of const guard, but with no success. But I think my issue is a little bit different, is there a corresponding TS issue?

    import { LiteralUnion } from 'type-fest';
    
    
    type Pet = LiteralUnion<'dog' | 'cat', string>;
    
    type DogHome = {
      citizens: Pet & 'dog',
      canDo: 'wow'
    }
    
    type CatHome = {
      citizens: Pet & 'cat',
      canDo: 'meuw'
    }
    
    type UnknownHome = {
      citizens: string;
      canDo?: string;
      phone?: string;
    }
    
    type AllHomes = DogHome | CatHome | UnknownHome;
    
    const home: AllHomes = {
      citizens: 'cat',
      phone: 'fff' // <- only canDo should be available
    }

    TS-play

    I have the similar problem, do you solved this problem?

  19. Lonli-Lokli commented on Feb 24, 2024

    @Lonli-Lokli

    I cannot believe, what a beautiful 🎁

  20. jogibear9988 commented on Feb 24, 2024

    @jogibear9988

    How is this solved now?

  21. NWYLZW commented on Feb 24, 2024

    @NWYLZW

    How is this solved now?

    no

    image
    image

    It still need add string & {}.

  22. RyanCavanaugh commented on Feb 25, 2024

    @RyanCavanaugh
    Member

    The bulk "Close" action I applied to Design Limitation issues doesn't let you pick what kind of "Close" you're doing, so don't read too much into any issue's particular bit on that. I wish GitHub didn't make this distinction without a way to specify it in many UI actions!

    The correct state for Design Limitation issues is closed since we don't have any plausible way of fixing them, and "Open" issues are for representing "work left to be done".

  23. sidwebworks commented on Jun 5, 2024

    @sidwebworks

    Mark Fulton (@mfulton26) it does not seem to work even with this helper. Adding both unions into a single string literal causes the whole thing to become just string.

  24. iartemiev commented on Jan 8, 2025

    @iartemiev

    If I'm understanding correctly, the reason the suggested solutions work is that a union of a primitive type and some variation of an empty object produces the wrapper/boxed object type corresponding to that primitive.

    For example, string & Record<never, never> appears to be equivalent to String, the return type of new String('').

    This prevents the union from getting reduced to the wider type, as the compiler will not reduce a literal to an object type. However, assignability is preserved, so we're still able to assign an arbitrary string value in this case.

    type Str1 = 'foo' | string;
    //    ^? string
    
    type Str2 = 'bar' | String;
    //    ^? String | 'bar' 
    
    let a: Str2;
    a = 'foo'; // 'foo' suggested via autocomplete
    a = 'bar'; // arbitrary string does not error

    Is this the correct interpretation or am I making some incorrect assumptions along the way?

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

    Design LimitationConstraints of the existing architecture prevent this from being fixed

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions