
Union types in TypeScript let a value be one of several types. A common case is a prop that should accept a set of known string literals — like a button size of "xs" | "sm" | "md" | "lg" — while still allowing an arbitrary string for flexibility.
The catch: if we simply add string to the union, we lose the autocomplete on those literal values. Let's look at the pattern that fixes it, and then wrap it in a reusable helper.
The Problem — and the Fix#
If we use a bare string in the union, we lose the autocomplete on the size property. Instead, we can subtract the known literals from string:
import React from "react";
/**
* If we use bare string we lose the autocomplete on size property
*/
type ButtonSize =
| "xs"
| "sm"
| "md"
| "lg"
| Omit<string, "xs" | "sm" | "md" | "lg">;
interface ButtonProps {
size: ButtonSize;
}
export const Button = (props: ButtonProps) => {
return <button className={`text-${props.size}`}></button>;
};
const Main = () => {
return (
<>
<Button size="sm"></Button>
<Button size="some other string"></Button>
</>
);
};
Because the literal members are still present in the union, the editor suggests "xs" | "sm" | "md" | "lg" while the Omit<string, ...> part keeps arbitrary strings valid.
Wrapping It in a Helper#
Repeating that pattern everywhere is tedious. We can extract it into a reusable type helper:
/**
* A type helper for fixing autocomplete issue in union types
*/
type UnionAutocompleteHelper<T extends string> = T | Omit<string, T>;
type ButtonSize2 = UnionAutocompleteHelper<"xs" | "sm" | "md" | "lg">;
interface ButtonProps {
size: ButtonSize2;
}
export const Button2 = (props: ButtonProps) => {
return <button className={`text-${props.size}`}></button>;
};
const Main2 = () => {
return (
<>
<Button size="sm"></Button>
<Button size="some other string"></Button>
</>
);
};
Now UnionAutocompleteHelper<T> can be reused for any union of string literals where you want both autocomplete and the freedom to pass an arbitrary string — a small utility that pays off across a whole codebase's developer experience.
Source: typescript-101