The pain this chapter solves

You use the theme object but autocomplete stops working, you cast with `as any` to silence errors, or you write your own types that drift out of sync with the library. The type system becomes an obstacle instead of a tool.

Chapter 11

TypeScript Integration

All exported types

salt-theme-gen ships a complete TypeScript type surface. Every type is exported from the package root:

import type {
  // Top-level
  GeneratedTheme,
  GeneratedThemeMode,

  // Color
  SemanticColors,
  IntentStates,
  StateColors,

  // Surfaces and scales
  SurfaceElevation,
  SpacingScale,
  RadiusScale,
  FontSizeScale,

  // Accessibility
  AccessibilityReport,
  ContrastEntry,

  // Options
  GenerateThemeOptions,
  ThemePreset,
  ColorHarmony,
  SpacingPreset,
  RadiusPreset,
  FontSizePreset,

  // Adjust
  ThemeOverrides,
  ThemeModeOverrides,

  // Diff
  ThemeDiff,
  ThemeModeDiff,
  FieldChange,
} from 'salt-theme-gen';

You rarely need all of these. The three you will use most are GeneratedTheme, GeneratedThemeMode, and GenerateThemeOptions.


Core types

GeneratedTheme

The return type of generateTheme() and adjustTheme():

interface GeneratedTheme {
  light: GeneratedThemeMode;
  dark:  GeneratedThemeMode;
}

GeneratedThemeMode

One mode — light or dark. The shape of theme.light and theme.dark:

interface GeneratedThemeMode {
  colors:           SemanticColors;
  states:           IntentStates;
  surfaceElevation: SurfaceElevation;
  spacing:          SpacingScale;
  radius:           RadiusScale;
  fontSizes:        FontSizeScale;
  accessibility:    AccessibilityReport;
}

SemanticColors

All 21 color tokens as a typed record:

interface SemanticColors {
  primary:     string;
  secondary:   string;
  tertiary:    string;
  quaternary:  string;
  background:  string;
  surface:     string;
  text:        string;
  muted:       string;
  border:      string;
  danger:      string;
  success:     string;
  warning:     string;
  info:        string;
  onPrimary:   string;
  onSecondary: string;
  onTertiary:  string;
  onQuaternary:string;
  onDanger:    string;
  onSuccess:   string;
  onWarning:   string;
  onInfo:      string;
}

IntentStates

The 32 state colors as a nested record:

// These types are not exported from the package — define them locally if needed
type IntentName = keyof GeneratedThemeMode['states'];
// 'primary' | 'secondary' | 'tertiary' | 'quaternary' | 'danger' | 'success' | 'warning' | 'info'

type StateName = keyof GeneratedThemeMode['states']['primary'];
// 'hover' | 'pressed' | 'focused' | 'disabled'

// IntentStates and StateColors are exported from salt-theme-gen:
import type { IntentStates, StateColors } from 'salt-theme-gen';

GenerateThemeOptions

The input type for generateTheme():

interface GenerateThemeOptions {
  preset?:     ThemePreset;    // named preset — takes priority over primary
  primary?:    string;         // hex color — used if no preset is given
  secondary?:  string;         // override auto-derived secondary
  tertiary?:   string;         // override auto-derived tertiary
  quaternary?: string;         // override auto-derived quaternary
  harmony?:    ColorHarmony;   // 'analogous' | 'complementary' | 'triadic' | 'split-complementary' | 'tetradic' | 'monochromatic'
  spacing?:    SpacingPreset;  // 'compact' | 'default' | 'relaxed' | 'spacious'
  radius?:     RadiusPreset;   // 'sharp' | 'default' | 'rounded' | 'pill'
  fontSize?:   FontSizePreset; // 'small' | 'default' | 'large' | 'editorial'
  fontLevel?:  number;         // 8–18, consumed by react-native-salt
}

When both preset and primary are provided, preset takes priority.

ThemePreset

The union of all valid preset strings:

type ThemePreset =
  | 'peacock' | 'ocean'     | 'forest'    | 'sunset'
  | 'cherry-blossom'        | 'arctic'    | 'desert'
  | 'lavender'| 'emerald'   | 'coral-reef'| 'midnight'
  | 'autumn'  | 'rose-gold' | 'sapphire'  | 'mint'
  | 'volcano' | 'twilight'  | 'honey'     | 'storm'
  | 'aurora';

Typing theme consumers

Accepting a mode as a prop

When a component receives a theme mode (light or dark), type it as GeneratedThemeMode:

import type { GeneratedThemeMode } from 'salt-theme-gen';

interface ButtonProps {
  label:    string;
  intent?:  'primary' | 'danger';
  theme:    GeneratedThemeMode;
}

function Button({ label, intent = 'primary', theme }: ButtonProps) {
  return (
    <button style={{
      backgroundColor: theme.colors[intent],
      color:           theme.colors[`on${capitalize(intent)}`],
      borderRadius:    `${theme.radius.md}px`,
    }}>
      {label}
    </button>
  );
}

A theme context

Sharing the full theme through React context:

import { createContext, useContext } from 'react';
import type { GeneratedTheme } from 'salt-theme-gen';

const ThemeContext = createContext<GeneratedTheme | null>(null);

function useTheme(): GeneratedTheme {
  const ctx = useContext(ThemeContext);
  if (!ctx) throw new Error('useTheme must be used within ThemeProvider');
  return ctx;
}

// In a component
function Card() {
  const theme = useTheme();
  const mode  = theme.light; // or derive from user preference

  return (
    <div style={{
      background:   mode.surfaceElevation.card,
      borderRadius: `${mode.radius.lg}px`,
      padding:      `${mode.spacing.xl}px`,
    }}>
      ...
    </div>
  );
}

Typing intent-based components

Using IntentName to type intent-driven props:

import type { GeneratedThemeMode } from 'salt-theme-gen';

type IntentName = keyof GeneratedThemeMode['states']; // defined locally

interface BadgeProps {
  label:   string;
  intent:  IntentName;
  mode:    GeneratedThemeMode;
}

function Badge({ label, intent, mode }: BadgeProps) {
  const bg    = mode.colors[intent];
  const color = mode.colors[`on${intent.charAt(0).toUpperCase()}${intent.slice(1)}` as keyof typeof mode.colors];

  return <span style={{ background: bg, color }}>{label}</span>;
}

// TypeScript enforces valid intents — no 'purpl' typos
<Badge intent="danger" label="Error" mode={theme.light} />
<Badge intent="success" label="Saved" mode={theme.light} />

Typing state-driven styles

Using StateName to build accessible interactive styles:

import type { GeneratedThemeMode } from 'salt-theme-gen';

type IntentName = keyof GeneratedThemeMode['states']; // defined locally
type StateName  = keyof GeneratedThemeMode['states']['primary']; // defined locally

function getStateColor(
  mode:   GeneratedThemeMode,
  intent: IntentName,
  state:  StateName,
): string {
  return mode.states[intent][state];
}

// Fully typed — TypeScript catches invalid state names
getStateColor(theme.light, 'primary', 'hover');    // ✓
getStateColor(theme.light, 'danger',  'pressed');  // ✓
getStateColor(theme.light, 'primary', 'clicking'); // ✗ type error

Typing the accessibility report

Iterating the report with full type safety:

import type { AccessibilityReport, ContrastEntry } from 'salt-theme-gen';

function auditTheme(report: AccessibilityReport): void {
  const entries = Object.entries(report) as [string, ContrastEntry][];

  const failures = entries.filter(([, e]) => e.level === 'fail');
  const aaa      = entries.filter(([, e]) => e.level === 'AAA');

  console.log(`AAA: ${aaa.length}/25 checks`);
  console.log(`Failures: ${failures.length}/25 checks`);

  for (const [check, entry] of failures) {
    console.warn(`FAIL: ${check} — ratio ${entry.ratio.toFixed(1)}`);
  }
}

Typing adjustTheme overrides

adjustTheme() accepts a DeepPartial<GeneratedTheme>. You can write the override type explicitly when building override objects:

import type { GeneratedTheme } from 'salt-theme-gen';

type DeepPartialTheme = {
  light?: Partial<{
    colors:          Partial<GeneratedTheme['light']['colors']>;
    spacing:         Partial<GeneratedTheme['light']['spacing']>;
    radius:          Partial<GeneratedTheme['light']['radius']>;
    fontSizes:       Partial<GeneratedTheme['light']['fontSizes']>;
    surfaceElevation:Partial<GeneratedTheme['light']['surfaceElevation']>;
  }>;
  dark?: Partial<{
    colors:          Partial<GeneratedTheme['dark']['colors']>;
    spacing:         Partial<GeneratedTheme['dark']['spacing']>;
    // ...
  }>;
};

// Build overrides separately, typed
const brandOverrides: DeepPartialTheme = {
  light: {
    colors:  { primary: 'oklch(0.52 0.20 218)' },
    spacing: { md: 14 },
  },
  dark: {
    colors:  { primary: 'oklch(0.68 0.20 218)' },
    spacing: { md: 14 },
  },
};

Using parseThemeJSON

parseThemeJSON() returns GeneratedTheme directly — there is no discriminated union. TypeScript types the return as GeneratedTheme and the function throws on invalid input:

import { parseThemeJSON } from 'salt-theme-gen';
import type { GeneratedTheme } from 'salt-theme-gen';

function loadTheme(raw: unknown): GeneratedTheme {
  // parseThemeJSON throws if the data is invalid — no discriminated union
  return parseThemeJSON(raw);
}

No casting needed — the return type is GeneratedTheme directly. Let the caller handle the thrown error as appropriate for their context.


Strict mode patterns

In a strict TypeScript project ("strict": true in tsconfig.json), a few patterns keep things clean:

Prefer import type

// Types are erased at compile time — always import as type
import type { GeneratedTheme, GeneratedThemeMode } from 'salt-theme-gen';

// Runtime functions — imported normally
import { generateTheme, adjustTheme, diffTheme, parseThemeJSON } from 'salt-theme-gen';

Narrow mode selection

Rather than indexing theme['light'] directly, a typed mode selector avoids string index errors:

type ThemeMode = 'light' | 'dark';

function getMode(theme: GeneratedTheme, mode: ThemeMode): GeneratedThemeMode {
  return theme[mode];
}

Exhaustive intent handling

If you write a switch on IntentName, TypeScript can enforce exhaustiveness:

import type { GeneratedThemeMode } from 'salt-theme-gen';

type IntentName = keyof GeneratedThemeMode['states']; // defined locally

function intentLabel(intent: IntentName): string {
  switch (intent) {
    case 'primary':    return 'Primary';
    case 'secondary':  return 'Secondary';
    case 'tertiary':   return 'Tertiary';
    case 'quaternary': return 'Quaternary';
    case 'danger':     return 'Danger';
    case 'success':    return 'Success';
    case 'warning':    return 'Warning';
    case 'info':       return 'Info';
    default: {
      const _exhaustive: never = intent;
      throw new Error(`Unhandled intent: ${_exhaustive}`);
    }
  }
}

If the library adds a new intent in a future version, this switch becomes a compile error — telling you exactly which call site needs updating.


Module augmentation

If you extend your theme with custom tokens (not recommended for most projects, but occasionally necessary), you can augment the types:

// types/salt-theme-gen.d.ts
import 'salt-theme-gen';

declare module 'salt-theme-gen' {
  interface SemanticColors {
    brand: string;       // custom token
    accent: string;      // custom token
  }
}

After augmentation, theme.light.colors.brand is typed and autocompleted. Your custom tokens live in the same type as the generated ones.

Note: augmenting the type does not add the token to parseThemeJSON() validation or diffTheme() tracking. For custom tokens you maintain those guarantees yourself.


tsconfig recommendations

For projects using salt-theme-gen with full strict type checking:

{
  "compilerOptions": {
    "strict": true,
    "noUncheckedIndexedAccess": true,
    "exactOptionalPropertyTypes": true,
    "moduleResolution": "bundler"
  }
}

noUncheckedIndexedAccess is the one that catches the most bugs with theme code — it forces you to handle the case where an object index might return undefined, which Record<string, string> lookups can do at runtime even when TypeScript says they can’t.


Completing the guide

You now have the full picture:

ChapterWhat you learned
1The problem, OKLCH, and what one function call produces
2Install and first theme in 5 minutes
3Every field in GeneratedTheme — colors, states, scales, accessibility
4All 20 presets and custom hex input
5Spacing, radius, and font size scales — exact values and combinations
6The 25 WCAG checks, auto-correction, and accessible component patterns
7Color harmony strategies — how accents are derived
8adjustTheme() for post-generation precision
9diffTheme() for design audits and CI regression checks
10parseThemeJSON() for safe round-trips through storage and APIs
11Full TypeScript type surface and strict patterns

The Integrations section starts next — applying your theme in React, Next.js, Vue, Astro, SvelteKit, and every other platform. Each guide opens with the developer pain it solves and ends with a working StackBlitz demo.

You built this site. Every color, spacing value, and radius on this page is a salt-theme-gen token from the Ocean preset. Open DevTools, inspect :root, and you’ll see every variable generated by the same generateTheme({ preset: ‘ocean’ }) call you’ve been reading about.