TLDR;
To get better autocomplete suggestions in TypeScript when using union types with string literals, intersect the string type with an empty object. This allows TypeScript to retain literal types longer and provide useful suggestions.
type Color = "primary" | "secondary" | (string & {});
This technique improves developer experience by enhancing autocomplete for components like this:
<Icon color="primary" />;
<Icon color="secondary" />;
<Icon color="#fff" />;
When working with TypeScript, one of my goals is to create components that are both flexible and developer-friendly. A perfect example of this is when I needed to build an Icon component where users could specify the icon’s color. I wanted to provide our brand’s known colors like “primary” and “secondary” while also allowing any custom colo
Defining Flexible Types in TypeScript
To achieve this, I started by defining a Color type that included our brand colors and any string:
type Color = "primary" | "secondary" | string;
Next, I applied this type to my Icon component:
type IconProps = {
color: Color;
};
const Icon = ({ color }: IconProps) => {
// Component logic...
};
Using the Icon component was straightforward:
<Icon color="primary" />;
<Icon color="#fff" />;
The Autocomplete Challenge
However, I ran into an issue. When using the Icon component, TypeScript didn’t suggest “primary” or “secondary” as color options. This lack of autocomplete hindered the developer experience.
The Solution: Intersecting Types
After some research, I found an interesting solution. By intersecting the string type with an empty object, I could maintain the literal types in Color:
type Color = "primary" | "secondary" | (string & {});
With this adjustment, TypeScript now suggested “primary” and “secondary” when using the Icon component, enhancing the developer experience:
<Icon color="primary" />;
<Icon color="secondary" />;
<Icon color="#fff" />;
Understanding Why This Works
This technique works due to a quirk in the TypeScript compiler. Normally, when you create a union between a string literal type and string, TypeScript widens the type to just string. For instance:
type Color = "primary" | "secondary" | string;
type Color = string;
TypeScript forgets about the literal types “primary” and “secondary”. By intersecting string with an empty object, we trick TypeScript into retaining the literal types longer:
type Color = "primary" | "secondary" | (string & {});
type Color = "primary" | "secondary" | (string & {});
This keeps “primary” and “secondary” as part of the type and enables autocomplete suggestions. The type string & {} behaves the same as string, so any string value can still be passed to the Icon component.
Is This Solution Robust?
Although this solution might seem unconventional, it is recognized and tested by the TypeScript team. While they might eventually update TypeScript to handle this scenario natively, this trick is a reliable solution for now.
Common Pitfalls
Here are some common pitfalls to watch out for:
- Omitting the Intersection: Without intersecting
stringwith{}, TypeScript won’t provide the autocomplete suggestions. - Misinterpreting the Technique: Understand that
string & {}is essentiallystringbut retains the literal types for better autocomplete. - Over-reliance on Autocomplete: Autocomplete is helpful but not a substitute for thorough testing.
Summary
To allow any string while still providing suggestions for known literals, use string & {}. This technique prevents TypeScript from collapsing string | "literal" into just string. It behaves the same as string but includes the added benefit of autocomplete suggestions.
Additional Resources
Frequently Asked Questions
Can this technique be used with other types?
Yes, this technique can be applied to other types where you want to retain literal types in unions.
Does this technique impact performance?
No, it doesn’t affect runtime performance. It’s purely a compile-time feature for enhancing developer experience.
What if TypeScript changes in the future?
The TypeScript team is aware of this technique. If they update the language to support this natively, you can update your code accordingly.


Share your thoughts