Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Literal String Union Autocomplete #29729

Closed
MrTarantula opened this issue Feb 4, 2019 · 69 comments
Closed

Literal String Union Autocomplete #29729

MrTarantula opened this issue Feb 4, 2019 · 69 comments
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed

Comments

@MrTarantula
Copy link

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

@RyanCavanaugh RyanCavanaugh added the Design Limitation Constraints of the existing architecture prevent this from being fixed label Feb 4, 2019
@RyanCavanaugh
Copy link
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

@sindresorhus
Copy link

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?

@MrTarantula
Copy link
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.

@spcfran
Copy link

spcfran commented Mar 11, 2019

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

@BendingBender
Copy link

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.

@manuth
Copy link
Contributor

manuth commented Jun 26, 2019

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, @RyanCavanaugh and @spcfran!

@frenic
Copy link

frenic commented Nov 16, 2019

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?

@manuth
Copy link
Contributor

manuth commented Nov 18, 2019

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";
    }
}

@AhmedElywa
Copy link

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

image

@manuth
Copy link
Contributor

manuth commented Dec 20, 2019

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

@papb
Copy link

papb commented Mar 22, 2020

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

@kotarella1110
Copy link

kotarella1110 commented Mar 23, 2020

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

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

TypeScript Playground

@Hansenleee
Copy link

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?

@Lonli-Lokli
Copy link

I cannot believe, what a beautiful 🎁

@jogibear9988
Copy link

How is this solved now?

@NWYLZW
Copy link

NWYLZW commented Feb 24, 2024

How is this solved now?

no

image
image

It still need add string & {}.

@RyanCavanaugh
Copy link
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".

@sidwebworks
Copy link

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

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Design Limitation Constraints of the existing architecture prevent this from being fixed
Projects
None yet
Development

No branches or pull requests