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
}
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",
}
}
This works, but it has a big drawback.
You are defining the same structure twice:
- Once in the interface
- 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
The important part here is:
as const
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
TypeScript automatically generates:
"light" | "dark"
Theme Colors
export type ThemeColors = typeof Colors.light
Now TypeScript automatically knows all color keys.
Example suggestions:
primary
secondary
background
paper
success
warning
error
inputBackground
topbarBg
No manual interface required.
Step 3: Create a Theme Type
Now define a simple theme structure.
export interface Theme {
mode: ThemeMode
colors: ThemeColors
}
Step 4: Use the Theme
const theme: Theme = {
mode: "light",
colors: Colors.light
}
Now your editor will provide full autocomplete.
Example:
theme.colors.primary
theme.colors.background
theme.colors.inputBackground
theme.colors.topbarBg
If you type something incorrect:
theme.colors.primar
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"
}
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)