Сайт использует сookies для хранения данных. Продолжая использовать сайт, вы даёте согласие на работу с этими файлами.

ОК
💻
Технологии
Опубликовано:
14.09.2026
Обновлено:
14.09.2026

Design Tokens в TypeScript: строгая типизация тем и автокомплит CSS-переменных

Тимофей Ищенко

Коротко: Руководство по строгой типизации дизайн-токенов и тем в TypeScript. Настройка автокомплита CSS-переменных, валидация контрактов и работа с DTCG JSON.

CSS-переменные (CSS Custom Properties) прочно закрепились в качестве стандарта для построения тем и дизайн-систем в вебе. Однако при масштабировании кодовой базы работа с ними через сырые строковые литералы вроде var(--color-bg-primary) быстро становится источником скрытых ошибок. Опечатка в одном символе не вызывает падения сборщика, но ломает отображение интерфейса в продакшене.

Сквозная интеграция дизайн-токенов со статической типизацией TypeScript решает эту проблему: разработчик получает валидацию контракта темы во время компиляции, автокомплит названий переменных в IDE и строгую синхронизацию между дизайн-системой и кодом.


Зачем типизировать токены и CSS-переменные

Проблема «магических строк» и рассинхронизации дизайна с кодом

Когда переменные описываются вручную в CSS или передаются как произвольные строки в стили, возникают типовые риски:

  1. Отсутствие валидации при сборке: если дизайнер переименовал токен --color-surface-card в --color-surface-panel, TypeScript и компиляторы стилей промолчат. Ошибка проявится только визуально в браузере.
  2. Высокая когнитивная нагрузка: разработчик вынужден регулярно сверяться со справочником токенов или инспектором Figma, копируя названия свойств вручную.
  3. Неполные темы: при добавлении новой темы (например, высококонтрастной или темной) легко пропустить несколько ключей, что приведет к выпадению дефолтных стилей.

Цели типизации: 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

Для построения гибкой архитектуры используется трехслойная модель:

  1. Global (Base/Primitive): сырые значения без контекста использования (blue-500: #2563eb, space-4: 16px).
  2. Semantic (Alias): смысловые токены, привязанные к роли в интерфейсе (surface-primary: {blue-500}, text-error: {red-600}). Именно этот слой меняется при переключении тем.
  3. 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 пайплайн.

Источники

Это авторская статья, основанная на личном опыте и субъективном взгляде автора. Заметили ошибку или битую ссылку? Сообщите нам: info@codesrc.ru - мы оперативно исправим. Спасибо, что помогаете делать блог лучше.
Следите за нами в соцсетях:

Читайте также