DEV Community

Amit Kumar Ranjan
Amit Kumar Ranjan

Posted on

Designing a Clean Theme Architecture in React

Building a Simple and Type-Safe Theme System in TypeScript (Developer Friendly)

When building a UI library or design system, one of the most important pieces is the theme system. A good theme system should provide:

  • Strong TypeScript type safety
  • Autocomplete for theme properties
  • Minimal boilerplate for developers
  • A simple developer experience

However, many theming systems require developers to write extra TypeScript code like module augmentation, global declarations, or duplicate interfaces. This increases complexity and makes the developer experience worse.

In this article, weโ€™ll build a clean and developer-friendly theme system where:

  • The theme object becomes the single source of truth
  • Types are automatically inferred
  • Developers get full autocomplete
  • No unnecessary TypeScript boilerplate is required

The Problem With Traditional Theme Typing

Many developers start by defining a manual interface for colors.

export interface ThemeColors {
  primary: string
  secondary: string
  background: string
  success: string
}
Enter fullscreen mode Exit fullscreen mode

Then they define their color object.

export const Colors = {
  light: {
    primary: "#0056D2",
    secondary: "#00198d",
    background: "#ffffff",
    success: "#28A745",
  },
  dark: {
    primary: "#ed1c24",
    secondary: "#00198d",
    background: "#121212",
    success: "#1DB954",
  }
}
Enter fullscreen mode Exit fullscreen mode

This works, but it has a big drawback.

You are defining the same structure twice:

  1. Once in the interface
  2. Once in the object

If you add a new color, you must update both places.

This leads to:

  • Extra maintenance
  • Possible mismatches
  • More boilerplate

There is a much cleaner approach.


Let TypeScript Infer the Types

Instead of manually writing interfaces, we can let TypeScript infer types directly from the object.

This keeps everything synchronized automatically.


Step 1: Define the Colors Object

export const Colors = {
  light: {
    primary: "#0056D2",
    secondary: "#00198d",
    background: "#ffffff",
    paper: "#ffffff",
    success: "#28A745",
    warning: "#FFC107",
    error: "#FA0228",

    inputBorderHover: "#000",
    topbarBg: "#181818",
    topbarText: "#A1A2A3",
    topbarActiveText: "#FFFFFF",

    inputBackground: "#F7F7F7",
    inputText: "#6a6c6e",
    inputBorder: "#E0E0E0",
    inputFocusBorder: "#0056D2",
    inputLabel: "#1C1C1C",
    inputPlaceholder: "#6E6E6E",
    inputHover: "#1976d2",
    inputFocus: "#1976d2",

    fieldTitleColor: "#59585C",
  },

  dark: {
    primary: "#ed1c24",
    secondary: "#00198d",
    background: "#121212",
    paper: "#1e1e1e",
    success: "#1DB954",
    warning: "#FFB400",
    error: "#E63946",

    inputBorderHover: "#FFFFFF",
    topbarBg: "#181818",
    topbarText: "#A1A2A3",
    topbarActiveText: "#FFFFFF",

    inputBackground: "#F7F7F7",
    inputText: "#d6ced2",
    inputBorder: "#E0E0E0",
    inputFocusBorder: "#0056D2",
    inputLabel: "#1C1C1C",
    inputPlaceholder: "#6E6E6E",
    inputHover: "#1976d2",
    inputFocus: "#d6ced2",

    fieldTitleColor: "#59585C",
  },
} as const
Enter fullscreen mode Exit fullscreen mode

The important part here is:

as const
Enter fullscreen mode Exit fullscreen mode

This tells TypeScript to preserve the exact structure of the object.


Step 2: Generate Types Automatically

Now we can generate types directly from the object.

Theme Mode

export type ThemeMode = keyof typeof Colors
Enter fullscreen mode Exit fullscreen mode

TypeScript automatically generates:

"light" | "dark"
Enter fullscreen mode Exit fullscreen mode

Theme Colors

export type ThemeColors = typeof Colors.light
Enter fullscreen mode Exit fullscreen mode

Now TypeScript automatically knows all color keys.

Example suggestions:

primary
secondary
background
paper
success
warning
error
inputBackground
topbarBg
Enter fullscreen mode Exit fullscreen mode

No manual interface required.


Step 3: Create a Theme Type

Now define a simple theme structure.

export interface Theme {
  mode: ThemeMode
  colors: ThemeColors
}
Enter fullscreen mode Exit fullscreen mode

Step 4: Use the Theme

const theme: Theme = {
  mode: "light",
  colors: Colors.light
}
Enter fullscreen mode Exit fullscreen mode

Now your editor will provide full autocomplete.

Example:

theme.colors.primary
theme.colors.background
theme.colors.inputBackground
theme.colors.topbarBg
Enter fullscreen mode Exit fullscreen mode

If you type something incorrect:

theme.colors.primar
Enter fullscreen mode Exit fullscreen mode

TypeScript will immediately show an error.


Why This Approach Is Better

This pattern provides several important advantages.

Single Source of Truth

The theme object itself defines the types.

You never need to maintain duplicate interfaces.


Zero Boilerplate

No need for:

  • Global module declarations
  • Interface duplication
  • Complex type definitions

Better Developer Experience

Developers get automatic IntelliSense and autocomplete in editors like VS Code.


Easier Maintenance

If you add a new color:

light: {
  primary: "#0056D2",
  accent: "#ff00aa"
}
Enter fullscreen mode Exit fullscreen mode

The types automatically update.

No extra work required.


Final Thoughts

A good theme system should be simple, type-safe, and easy to maintain.

By using TypeScript's type inference, we can build a theme architecture that:

  • Avoids duplicate type definitions
  • Provides full autocomplete
  • Keeps the developer experience clean

The key idea is simple:

Let the theme object define the types.

This pattern works extremely well when building:

  • UI libraries
  • Design systems
  • CSS-in-JS frameworks
  • Component libraries

If you are building a theme system for your project or library, this approach will help you keep the API clean, scalable, and developer-friendly.

Happy coding ๐Ÿš€

Top comments (0)