Коротко: Руководство по строгой типизации дизайн-токенов и тем в TypeScript. Настройка автокомплита CSS-переменных, валидация контрактов и работа с DTCG JSON.
CSS-переменные (CSS Custom Properties) прочно закрепились в качестве стандарта для построения тем и дизайн-систем в вебе. Однако при масштабировании кодовой базы работа с ними через сырые строковые литералы вроде var(--color-bg-primary) быстро становится источником скрытых ошибок. Опечатка в одном символе не вызывает падения сборщика, но ломает отображение интерфейса в продакшене.
Сквозная интеграция дизайн-токенов со статической типизацией TypeScript решает эту проблему: разработчик получает валидацию контракта темы во время компиляции, автокомплит названий переменных в IDE и строгую синхронизацию между дизайн-системой и кодом.
Зачем типизировать токены и CSS-переменные
Проблема «магических строк» и рассинхронизации дизайна с кодом
Когда переменные описываются вручную в CSS или передаются как произвольные строки в стили, возникают типовые риски:
- Отсутствие валидации при сборке: если дизайнер переименовал токен
--color-surface-cardв--color-surface-panel, TypeScript и компиляторы стилей промолчат. Ошибка проявится только визуально в браузере. - Высокая когнитивная нагрузка: разработчик вынужден регулярно сверяться со справочником токенов или инспектором Figma, копируя названия свойств вручную.
- Неполные темы: при добавлении новой темы (например, высококонтрастной или темной) легко пропустить несколько ключей, что приведет к выпадению дефолтных стилей.
Цели типизации: IntelliSense, валидация контракта темы и защита от опечаток
Сквозная типизация закрывает эти проблемы, обеспечивая:
- Интеллектуальный автокомплит (IntelliSense): подсказка доступных переменных при вводе в React-компонентах, CSS-in-JS и вспомогательных утилитах.
- Строгий контракт темы: гарантия того, что
darkThemeиlightThemeреализуют абсолютно идентичный набор семантических токенов. - Безопасный рефакторинг: возможность переименования токенов с предсказуемой подсветкой всех мест использования в кодовой базе.
Анатомия токенов: от стандарта DTCG к типам данных
Структура токенов (DTCG JSON) и единый источник правды
Сообщество Design Tokens Community Group (DTCG) в рамках W3C стандартизировало формат описания токенов. В спецификации токен представляет собой платформенно-агностичный JSON-объект, содержащий как минимум имя, $value и $type:
{
"color": {
"brand": {
"primary": {
"$value": "#2563eb",
"$type": "color",
"$description": "Основной акцентный цвет бренда"
}
}
}
}
Хранение токенов в едином JSON позволяет автоматически генерировать из них артефакты для Web (CSS, SCSS, JS, TS), iOS (Swift) и Android (Kotlin).
Разделение уровней токенов: Global, Semantic, Component
Для построения гибкой архитектуры используется трехслойная модель:
- Global (Base/Primitive): сырые значения без контекста использования (
blue-500: #2563eb,space-4: 16px). - Semantic (Alias): смысловые токены, привязанные к роли в интерфейсе (
surface-primary: {blue-500},text-error: {red-600}). Именно этот слой меняется при переключении тем. - Component Tokens: свойства конкретных компонентов (
button-primary-bg: {surface-primary}).
Пайплайн трансформации: экспорт токенов в CSS и d.ts
Чтобы токены стали доступны в кодовой базе, исходный JSON проходит через этап сборки.
DTCG JSON (Tokens Studio / Figma)
│
▼
[ Style Dictionary v4 / Custom Codegen ]
│
├──► globals.css (:root { --color-brand-primary: #2563eb; })
└──► tokens.d.ts (export type CssVariable = '--color-brand-primary' | ...;)
Style Dictionary v4 против кастомных генераторов
Для компиляции токенов чаще всего используются два подхода:
- Style Dictionary (v4+): индустриальный стандарт с поддержкой формата DTCG. Трансформирует токены в CSS-переменные, JSON-мапы и декларации TypeScript без необходимости писать собственные парсеры.
- Кастомные Node.js-скрипты: в небольших проектах достаточно написать легковесный скрипт на TypeScript, который считывает JSON, рекурсивно обходит дерево и генерирует плоский
.cssи соответствующий.d.ts.
TypeScript-магия: генерация плоских CSS-переменных и автокомплита
Если структура токенов представлена в виде константного объекта TypeScript (as const), сгенерировать типы CSS-переменных можно непосредственно через систему типов без внешних генераторов.
Рекурсивное сплющивание структуры через Template Literal Types
Преобразуем древовидный объект токенов в union-тип строк вида --color-brand-primary:
type Join<K, P> = K extends string | number
? P extends string | number
? `${K}-${P}`
: never
: never;
type Leaves<T> = T extends object
? { [K in keyof T]-?: Join<K, Leaves<T[K]>> }[keyof T]
: '';
// Исходный объект токенов
const themeTokens = {
color: {
brand: {
primary: '#2563eb',
secondary: '#475569',
},
surface: {
background: '#ffffff',
card: '#f8fafc',
},
},
spacing: {
sm: '8px',
md: '16px',
lg: '24px',
},
} as const;
// Тип ключей пути: "color-brand-primary" | "color-surface-background" | ...
type TokenPath = Leaves<typeof themeTokens>;
// Итоговый union-тип CSS-переменных
export type CssVariable = `--${TokenPath}`;
Создание типобезопасного хелпера tokenVar
Для безопасного формирования выражений var(...) создается типизированная функция-хелпер:
export type CSSVarFunction = `var(${CssVariable})` | `var(${CssVariable}, ${string | number})`;
export function tokenVar(variable: CssVariable, fallback?: string | number): CSSVarFunction {
return fallback !== undefined ? `var(${variable}, ${fallback})` : `var(${variable})`;
}
// Пример использования:
const validColor = tokenVar('--color-brand-primary'); // Корректно
// const errorColor = tokenVar('--color-brand-primari');
// Ошибка TS: Argument of type '"--color-brand-primari"' is not assignable to parameter of type 'CssVariable'.
Типизация многотемности и проверка контракта через satisfies
При поддержке нескольких тем (Light, Dark, High Contrast) необходимо гарантировать, что все они содержат абсолютно идентичный набор семантических токенов.
Контракт темы и исключение пропущенных токенов
Оператор satisfies позволяет валидировать соответствие объекта интерфейсу темы, сохраняя точные литеральные типы:
// Базовый контракт темы
type ColorTokenContract = {
surface: {
page: string;
card: string;
};
text: {
primary: string;
muted: string;
};
};
export const lightTheme = {
surface: {
page: '#ffffff',
card: '#f8fafc',
},
text: {
primary: '#0f172a',
muted: '#64748b',
},
} as const satisfies ColorTokenContract;
export const darkTheme = {
surface: {
page: '#0f172a',
card: '#1e293b',
},
text: {
primary: '#f8fafc',
muted: '#94a3b8',
},
// Если пропустить поле (например, text.muted), компилятор выдаст ошибку несоответствия контракту
} as const satisfies ColorTokenContract;
Runtime-переключение тем через data-атрибуты
Сгенерированные переменные подключаются в CSS через селекторы тем:
:root, [data-theme="light"] {
--surface-page: #ffffff;
--surface-card: #f8fafc;
--text-primary: #0f172a;
--text-muted: #64748b;
}
[data-theme="dark"] {
--surface-page: #0f172a;
--surface-card: #1e293b;
--text-primary: #f8fafc;
--text-muted: #94a3b8;
}
Интеграция в React: CSS Modules, Styled Components и инлайн-стили
Расширение интерфейса React.CSSProperties
По умолчанию React не предоставляет автокомплит для кастомных CSS-переменных в свойстве style. Это решается расширением глобального интерфейса через Module Augmentation:
// types/react-css.d.ts
import 'react';
import { CssVariable } from './tokens';
declare module 'react' {
interface CSSProperties {
[key: CssVariable]: string | number | undefined;
}
}
Теперь инлайн-стили получают строгую проверку типов и автокомплит:
import React from 'react';
import { tokenVar } from './tokens';
interface CardProps {
padding?: string;
children: React.ReactNode;
}
export const Card: React.FC<CardProps> = ({ padding = '16px', children }) => {
return (
<div
style={{
backgroundColor: tokenVar('--color-surface-card'),
color: tokenVar('--color-text-primary'),
// Строгая типизация динамической переменной
'--spacing-card-pad': padding,
}}
>
{children}
</div>
);
};
Производительность компилятора: как не перегрузить TS Server
Глубокие рекурсивные условные типы (Recursive Conditional Types) могут существенно замедлять работу IDE и компилятора tsc на больших объемах данных.
| Подход | Плюсы | Минусы | Рекомендация |
|---|---|---|---|
| Вычисление типов в коде (In-code Types) | Не требует шага сборки, работает напрямую из .ts файлов. |
Высокая нагрузка на TSServer при глубокой вложенности объектов (глубина > 4). | Подходит для небольших проектов и простых дизайн-систем. |
Предгенерация .d.ts (Build-time Codegen) |
Мгновенный отклик IDE, плоские литеральные типы без рекурсий. | Требует генерации при изменении исходных JSON-токенов. | Рекомендуется для библиотек компонентов и масштабных UI-китов. |
Если в дизайн-системе больше 300 токенов, предпочтительно использовать генерацию плоских .d.ts через Style Dictionary или собственный скрипт сборки. Это предотвратит подвисания языкового сервера TypeScript.
FAQ
1. Зачем типизировать CSS-переменные в TypeScript, если есть расширения для редакторов кода?
Плагины редактора выполняют поиск по открытым файлам стилей на основе эвристик. Они не гарантируют валидацию на этапе CI/CD сборки, не проверяют полноту контрактов тем и часто дают сбои в монорепозиториях.
2. Как обрабатывать токены с динамической прозрачностью (opacity)?
В спецификации DTCG и CSS-переменных рекомендуется сохранять цветовые токены в современных форматах или разбивать на каналы (например, --color-primary-rgb: 37 99 235). Это позволяет безопасно управлять прозрачностью: rgb(var(--color-primary-rgb) / 0.5).
3. Можно ли объединить этот подход с Tailwind CSS?
Да. Конфигурация Tailwind (tailwind.config.ts) может ссылаться на типизированные токены: colors: { primary: 'var(--color-brand-primary)' }. Это обеспечивает работу подсказок как в утилитарных классах, так и при ручном написании стилей.
4. Поддерживается ли работа со стандартным форматом Tokens Studio?
Да. Tokens Studio сохраняет токены в формате DTCG JSON. Этот файл напрямую обрабатывается сборщиком (например, Style Dictionary v4), который на выходе генерирует плоский CSS и типизацию .d.ts.
5. Что делать, если нужно использовать внешние CSS-переменные сторонних библиотек?
Вы можете объединить системные токены с внешними через union-тип:
type ExternalCssVars = `--radix-${string}` | `--toastify-${string}`;
export type AppCssVariables = CssVariable | ExternalCssVars;
Чеклист внедрения
- [ ] Организовать единый JSON-источник токенов (в стандарте DTCG).
- [ ] Настроить пайплайн компиляции в CSS Custom Properties (
:root, селекторы тем). - [ ] Сгенерировать union-тип допустимых CSS-переменных (
--token-name). - [ ] Реализовать типизированный хелпер
tokenVar()для безопасного чтения значений. - [ ] Расширить интерфейс
React.CSSPropertiesчерез Module Augmentation для поддержки кастомных свойств. - [ ] Валидировать альтернативные темы с помощью оператора
satisfies. - [ ] Включить проверку типов (
tsc --noEmit) в CI/CD пайплайн.




.svg.webp)



