Type Branding и Type Flavoring
01.02.2026 • typescript
Вступление
TypeScript обладает одним интересным качеством — выводом типов из структуры объекта, а не потому что он называется определённым образом (то есть структурную типизацию, а не номинальную). Это позволяет использовать литералы там, где нужно:
interface Point {
x: number;
y: number;
}
function drawLine(from: Point, to: Point): void;
// Тут оба объекта передаются литералами и
// не требуют создания с помощью какого-то класса Point
drawLine({
x: 0,
y: 0
}, {
x: 10,
y: 10
});Или, например, аналогично работают строки:
type LogLevel = 'debug' | 'warning' | 'error';
function log(level: LogLevel): void;
// Проверки типов работают, и мы используем строки напрямую
log('error');Однако иногда это является и минусом. Например, можно использовать параметры не в том месте, где планировалось:
type Mark = string;
type Category = string;
function getItems(mark: Mark, category: Category): Promise<...>;
const mark: Mark = 'Лафет';
const category: Category = 'Сковородки';
// Логическая ошибка! Аргументы передаются в неправильном порядке!
// Ошибок сборки нет
const items = await getItems(category, mark);И TypeScript нам никак с этим не помогает. Несмотря на то, что мы объявили свои типы, никаких ошибок компиляции нет. Это происходит ровно по той же причине – типы равны.
Конечно, самым первым действием я бы предложил для таких случаев использовать объект для аргументов: так назначение параметров будет явно видно, и перепутать будет сильно сложнее.
Однако можем ли мы сделать что-то ещё? Можем.
Type Branding
Некоторую популярность получил способ типизации, который именуется Type Branding. Так как TypeScript использует именно структуру для типизации, то на этом можно сыграть:
type Category = string & {
_type: 'Category';
};
type Mark = string & {
_type: 'Mark';
};
function getItems(mark: Mark, category: Category): Promise<...>;
const category = getCategory();
const mark = getMark();
// Ошибка компиляции! Неправильные аргументы для функции
const items = await getItems(category, mark);В данном примере типы являются строками, которые пересечены с объектами с разными полями, поэтому для TypeScript это разные типы. На самом деле, мы всё ещё используем строки и в рантайме никаких полей у них не появилось, это просто способ “обмануть” TypeScript и заставить его думать нужным нам образом. _type – произвольное имя поля.
Минус очевидный: мы больше не можем использовать литералы без приведения типов для значений:
// Ошибка! Нет поля _type
const category: Category = 'Сковородки';Данная конструкция хорошо работает, если типы варятся в “замкнутой системе”, в которой у нас есть готовые функции по созданию подобных объектов, и нам остаётся только писать “прикладной” код. В том числе подобное может пригодиться авторам библиотек, внутри которых можно спрятать все фокусы с типами, а наружу выдать красивую систему типов.
Type Flavoring
Немного модифицированный подход позволяет использовать литералы:
type Category = string & {
// Поле _type – опциональное
_type?: 'Category';
};
// Работает, ошибок нет
const category: Category = 'Сковородки';Назвали такой подход Type Flavoring. Несмотря на то, что строку можно присвоить, модифицированные типы нельзя присваивать друг другу:
const mark: Mark = 'Лафет';
// Ошибка!
const category: Category = mark;Минус тоже имеется. Мы частично ломаем решение исходной проблемы, так как теперь литералы легко перепутать:
function getItems(mark: Mark, category: Category): Promise<...>;
getItems('Лафет', 'Сковородки');
// Тоже нет ошибки
getItems('Сковородки', 'Лафет');Таким образом, Type Branding является более строгим подходом и всё ещё может быть полезен, когда мы хотим больше контроля.
valibot
В популярной библиотеке для валидации данных valibot есть готовое апи для Type Branding и Flavoring:
import * as v from 'valibot';
// Вызов .brand(...)
const FruitSchema = v.pipe(v.string(), v.brand('Fruit'));
type Fruit = v.InferOutput<typeof FruitSchema>;
// Ошибка!
const fruit: Fruit = 'Апельсин';Если же заменить экшен brand на flavor, то получим Type Flavoring, и использовать литералы будет можно:
import * as v from 'valibot';
// Вызов .flavor(...)
const FruitSchema = v.pipe(v.string(), v.flavor('Fruit'));
type Fruit = v.InferOutput<typeof FruitSchema>;
// Нет ошибки
const fruit: Fruit = 'Апельсин';zod
В ещё одной популярной библиотеке для валидации zod есть аналогичный метод brand:
import * as z from 'zod';
// .brand(...)
const Fruit = z.string().brand('Fruit');
type Fruit = z.infer<typeof Fruit>;
// Ошибка
const fruit: Fruit = 'Апельсин';Метода flavor нет, однако на просторах интернета люди уже обошли это:
import * as z from 'zod';
type FruitFlavor = string & {
_type?: 'Fruit';
};
function isFruit(name: string): name is FruitFlavor {
// Для пример, проверим на пустоту строки
return Boolean(name);
}
// Сделали .refine(...) вместо .brand(...)
const Fruit = z.string().refine(isFruit);
type Fruit = z.infer<typeof Fruit>;
// Ошибки нет
const fruit: Fruit = 'Апельсин';refine — способ добавить кастомную проверку к полю, при этом используется выходной тип функции. Поэтому результатом будет тип FruitFlavor.
Итого
Мы рассмотрели два способа сделать более строгую типизацию, чтобы структурная типизация TypeScript не позволяла нам использовать одни значения вместо других. Можно использовать это не только для строк, но и для чисел, объектов или более сложных структур. Подход не новый и используется в разных библиотеках. У каждого из двух подходов есть свои плюсы и минусы, а также свои места для применения. Возможно, Type Branding и Type Flavoring пригодятся в вашей следующей библиотеке, которая будет написана завтра?
Обсудить в Telegram



