Repository navigation
Literal String Union Autocomplete #29729
Description
Activity
- addedDesign LimitationConstraints of the existing architecture prevent this from being fixedConstraints of the existing architecture prevent this from being fixed
on Feb 4, 2019 RyanCavanaugh commented
on Feb 4, 2019 MemberMore actions'black' | 'red' | 'green' | 'yellow' | 'blue' | stringFrom the compiler's point of view, this is just a very fancy way of writing
string. By the time we're looking for suggestions atborderColor, all the literals are lost.You could write something like this:

Naturally this doesn't stop you from writing
"bluck". You might want to track #6579Reacted by Wessel Kronemeijer, Alexander, lsdsjy, Anatole, Tomáš Wróbel, Alexey Berezin, dongwook.kim, Yichuan Shen, mengxingshike2012, Fog3211 and 28 moreReacted by 绯一, Harvey Zhao, Abhijeet Singh and Matthew DeanReacted by Oliver MolnarReacted by Matthew Robertson, Lucas Castro, Colin McDonnell, Jhonny Moreira and Minh NguyenReacted by Aldila and Code ITBy the time we're looking for suggestions at borderColor, all the literals are lost.
Why not improve the compiler to keep more metadata around?
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.
Reacted by Sindre Sorhus, Dimitri B., Valentin Radu, c-vetter, Anatole, Alexey Berezin, Merlin Fisher, picode7, Piotr Witek, Ethan Resnick and 7 moreIt 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
Reacted by Shinigami, Jan Doležel, jaipresto, J, the-unbearable-lightness-of-being, Francisco Javier Sucre González, Mohamed Lamine Allal, Giora Guttsait, Jonathan Madelaine, Sergey Makarov and 39 moreReacted by 绯一Would be great if this could be implemented. It has the potential to improve even core Node.js APIs. The hash.digest
encondigparam, for example should accept plain strings but there are values available on all platforms that could be enumerated for ease of use.Reacted by Sindre Sorhus, MrTarantula, Linus Unnebäck, Shinigami, zhao5363, Chen Asraf, Lauren Yim, Luiz Felipe Frazão, Mulverine, Merlin Fisher and 7 moreHey 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)!Reacted by Lauren Yim, likeanormaldude, Luis Durão, Mathis Le Bonniec, Lev Izraelit, João Pedro, Hugo, Katsuma Ito, Benoît Rouleau, Hiroki Osame and 27 moreReacted by Fran Hodl, Pine, Max Milton, Eskild Diderichsen, Fredrik Nicol, the-unbearable-lightness-of-being, Guillaume, Giora Guttsait, Jonathan Madelaine, Sergey Makarov and 23 moreI 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?
Reacted by zhao5363I think I might have found a solution:
Use a type like my
UnpackedLiteralUniontype to unpack the actual type of theLiteralUnion: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"; } }
Reacted by Mikis Woodwintermanuth Your solution not work for me can you know why ?
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
awill have typestringbecause both"hello"and"world"inheritstring.let a: ("hello" | "world") | (string & {});
In this code-snippet
awill be of type"hello" | "world" | (string & {})because though both"hello"and"world"inheritstring, they do not inheritstring & {}, which means"hello" | "world"andstring & {}are treated as distinguishable types.Hope this helped you understanding.
Reacted by Ahmed Elywa, Dominik Mahnkopf, Miguel Lo-A-Foe, Mohamed Lamine Allal, Nathan, Arash Motamedi, Kristóf Poduszló, YoungJoo Han, andrew, Yuriy Galanter and 73 moreReacted by Ahmed Elywa, Nathan, Ankeet Maini, Apostolos Tsakpinis, Inger-Marie Nilsen, Erick Riva, jon lepage, Kyℓe Hensel, Giovanni Petris, nokazn and 31 moreReacted by Bhargav Sangani, viex and Bassim ShahidyFor those interested, there is a workaround for this called
LiteralUnionin type-fest.Reacted by __, Cezar Andrew Villegas Santarin, Simon Siefke, LeoKu, GabenGar, Mikis Woodwinter, Bendegúz Hajnal, Ilya Pakhomov, Darryl Noakes, Konstantin Pozin and 8 moreRyanCavanaugh commented
on May 21, 2020 MemberMore actionsI 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;
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?49 remaining items
These Solutions all do not work if you try to use them for a index signature
Reacted by mwojslaw and Gyorgy KallaiI 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 }
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 }
I have the similar problem, do you solved this problem?
I cannot believe, what a beautiful 🎁
Reacted by NemoSteinHow is this solved now?
Reacted by Toni VillenaRyanCavanaugh commented
on Feb 25, 2024 MemberMore actionsThe 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".
Reacted by Nano Miratus / Anton Stiglitz and MulverineReacted by Amit Beckenstein, Toni Villena, Sergey Dushevski, nokazn, Alexander, Abhijeet Singh, hrmny, Alex Crișan, aviraccoon, Nano Miratus / Anton Stiglitz and 1 moreis this https://www.typescriptlang.org/play/?#code/FAFwngDgpgBAsgUQCoAkDyARAyjAvDAIgHFkCYAfQgBTSyTIHoGYA6N4USWNAIwCsoAYxAAmPDADewGDJgQATgHsIALhgADACQTEqTFgC+MbQQYBDCAEsGANwCMZSqYvWbIx4XNXbAZg8AKAGcQeUsAOwBzGAAySQMASgN1AG5gAw5BRTDgmDM1XgFhMXwpWTklVUJGZgA5RVyAVxBFTIBbCAAbKBAoNKA related to this issue?
is this https://www.typescriptlang.org/play/?#code/FAFwngDgpgBAsgUQCoAkDyARAyjAvDAIgHFkCYAfQgBTSyTIHoGYA6N4USWNAIwCsoAYxAAmPDADewGDJgQATgHsIALhgADACQTEqTFgC+MbQQYBDCAEsGANwCMZSqYvWbIx4XNXbAZg8AKAGcQeUsAOwBzGAAySQMASgN1AG5gAw5BRTDgmDM1XgFhMXwpWTklVUJGZgA5RVyAVxBFTIBbCAAbKBAoNKA related to this issue?
yes, as a workaround you can use
type-fest'sLiteralUnionMark 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.
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.
it works if you move the template literal type into
LiteralUnion<>(instead of nestingLiteralUnioninside the template literal type)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 toString, the return type ofnew 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?
Reacted by Yuriy and Tim- added a commit that references this issue
on May 16, 2026




Autocomplete works for literal string unions, but adding a union of
stringnegates 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
Expected behavior:
Actual behavior:
Playground Link: https://stackblitz.com/edit/typescript-bwyyab
Related Issues: #12687 #13614