В TypeScript типы, как правило, достаточно точны, что оставляет мало места для неоднозначности. Хотя такая строгость полезна для объектов, при работе с объединениями она может ощущаться ограничивающей. В этой статье мы рассмотрим, как создавать открытые union-типы в TypeScript.
Объединения (unions) особенно полезны, когда вам нужна гибкость в типах параметров или вы работаете с литеральными значениями. Они отлично подходят, если вы хотите задать общий тип, например string, но при этом предоставить конкретные варианты значений для автодополнения.
Пример с цветами
Допустим, у вас есть функция, принимающая цвет. Можно просто указать тип string, но почему бы не задать конкретные допустимые значения?
type Colors = 'red' | 'blue' | 'yellow';
function paint(color: Colors) {
console.log(color);
}
paint('red'); // Работает
paint('green'); // Ошибка: аргумент типа '"green"' не соответствует типу 'Colors'.
Это работает хорошо — пока кто-то не попробует передать новый цвет. Такой параметр не будет соответствовать типу Colors. И вот тут на помощь приходят открытые объединения. Что, если мы хотим, чтобы основным типом оставалась string, но с подсказками?
Наивный подход: добавляем string
type Colors = 'red' | 'blue' | 'yellow' | string;
// ^? type Colors = string
К сожалению, TypeScript пока не поддерживает автодополнение в таком случае (см. issue #29729, но обещают в версии 5.3! 🤞). Компилятор просто упрощает объединение до string, так как все литералы наследуются от string.
Решение: string & {}
Лучший способ — использовать пересечение string & {}:
type Colors = 'red' | 'blue' | 'yellow' | (string & {});
function paint(color: Colors) {
console.log(color);
}
paint('red'); // Работает
paint('green'); // Тоже работает
Это работает! 🎉 Такой подход обеспечивает гибкое автодополнение:

Почему это работает?
Чтобы предотвратить "схлопывание" литеральных типов в string, можно использовать базовый тип string в пересечении с {} или Record<never, never>. В глазах компилятора string и string & {} считаются разными, даже если технически они эквивалентны:
type AreTheSame = string extends string & {} ? true : false;
// ^? true
⚠️ Важно:
Пустые пересечения сводятся кnever(например,string & number).
Аналогично для number
То же самое можно сделать для числовых типов:
type Gap = 0 | 8 | 16 | (number & {});
function doSomething(gap: Gap) {
console.log(gap);
}
doSomething(0); // OK
doSomething(100); // OK
