As a best-selling author, I invite you to explore my books on Amazon. Don't forget to follow me on Medium and show your support. Thank you! Your support means the world!
Building effective design systems relies on strong typing for consistency and developer experience. TypeScript elevates this process by providing static type checking that catches errors early in the development cycle.
I've spent years working with TypeScript-based design systems, and I've found several techniques that dramatically improve both code quality and team productivity.
Component Typing Architecture
The foundation of any TypeScript design system starts with well-defined component interfaces. When I create component props, I ensure they clearly communicate both required and optional values:
interface ButtonProps {
variant: 'primary' | 'secondary' | 'ghost';
size?: 'small' | 'medium' | 'large';
isDisabled?: boolean;
onClick?: () => void;
children: React.ReactNode;
}
For components with different modes of operation, I use discriminated unions to create variant-specific prop sets:
type BaseInputProps = {
label: string;
name: string;
required?: boolean;
};
type TextInputProps = BaseInputProps & {
type: 'text' | 'email' | 'password';
maxLength?: number;
};
type NumberInputProps = BaseInputProps & {
type: 'number';
min?: number;
max?: number;
};
type InputProps = TextInputProps | NumberInputProps;
For flexible components that need to handle different data types, I leverage generics:
interface SelectProps<T> {
options: Array<{value: T, label: string}>;
value: T;
onChange: (value: T) => void;
}
// Usage
<Select<string>
options={[{value: 'opt1', label: 'Option 1'}]}
value="opt1"
onChange={(val) => console.log(val)} // val is typed as string
/>
Prop Validation Patterns
While TypeScript offers compile-time checks, runtime validation adds another layer of protection:
import * as z from 'zod';
const ButtonSchema = z.object({
variant: z.enum(['primary', 'secondary', 'ghost']),
size: z.enum(['small', 'medium', 'large']).optional().default('medium'),
isDisabled: z.boolean().optional().default(false),
onClick: z.function().optional(),
children: z.any()
});
type ButtonProps = z.infer<typeof ButtonSchema>;
export const Button = (props: ButtonProps) => {
// Parse props at runtime
const validatedProps = ButtonSchema.parse(props);
// Now use validatedProps safely
const { variant, size, isDisabled, onClick, children } = validatedProps;
// Component implementation
};
I've found that combining static and runtime validation creates the most reliable components, especially when receiving props from external sources.
Theme Type Safety
A design system's theme should be strongly typed to prevent inconsistencies:
type ColorToken = 'primary100' | 'primary200' | 'primary300' | 'neutral100' | 'neutral200' | 'neutral300';
type SpacingToken = 'xs' | 'sm' | 'md' | 'lg' | 'xl';
type FontSizeToken = 'xs' | 'sm' | 'md' | 'lg' | 'xl' | 'xxl';
interface Theme {
colors: Record<ColorToken, string>;
spacing: Record<SpacingToken, string>;
fontSizes: Record<FontSizeToken, string>;
}
// Theme implementation
export const theme: Theme = {
colors: {
primary100: '#e6f7ff',
primary200: '#91caff',
primary300: '#1677ff',
neutral100: '#ffffff',
neutral200: '#f5f5f5',
neutral300: '#000000',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
},
fontSizes: {
xs: '12px',
sm: '14px',
md: '16px',
lg: '18px',
xl: '20px',
xxl: '24px',
}
};
I create utility types to access these tokens with autocomplete:
type ColorProps = {
color?: ColorToken;
backgroundColor?: ColorToken;
};
const getColor = (token: ColorToken) => theme.colors[token];
const getSpacing = (token: SpacingToken) => theme.spacing[token];
Strict Component Composition
Type-safe component composition is essential for building complex UIs. I use higher-order components that preserve the original component's type information:
function withTheme<T extends object>(
Component: React.ComponentType<T & ThemeProps>
): React.FC<Omit<T, keyof ThemeProps>> {
return (props) => {
const theme = useTheme();
return <Component {...props as T} theme={theme} />;
};
}
// The original prop types are preserved
const ThemedButton = withTheme(Button);
I also create composition utilities that ensure children components match expected types:
interface TabProps {
label: string;
value: string;
children: React.ReactNode;
}
interface TabsProps {
value: string;
onChange: (value: string) => void;
children: React.ReactElement<TabProps>[];
}
const Tabs: React.FC<TabsProps> = ({ children, value, onChange }) => {
// Implementation
};
const Tab: React.FC<TabProps> = ({ children }) => {
// Implementation
};
// Usage with type checking
<Tabs value="tab1" onChange={(val) => console.log(val)}>
<Tab label="Tab 1" value="tab1">Content 1</Tab>
<Tab label="Tab 2" value="tab2">Content 2</Tab>
{/* TypeScript error: Type '{ children: string; }' is not assignable to type 'ReactElement<TabProps>' */}
{"This would cause a type error"}
</Tabs>
Pattern Documentation
Generating documentation directly from TypeScript definitions ensures it stays in sync with the code:
/**
* Button component for user interaction
* @example
* <Button variant="primary" onClick={() => alert('Clicked!')}>Click Me</Button>
*/
interface ButtonProps {
/** The visual style of the button */
variant: 'primary' | 'secondary' | 'ghost';
/** Optional button size - defaults to 'medium' if not provided */
size?: 'small' | 'medium' | 'large';
/** Whether the button is disabled */
isDisabled?: boolean;
/** Click handler function */
onClick?: () => void;
/** Button content */
children: React.ReactNode;
}
I use tools like TypeDoc or Storybook with the Docs addon to automatically extract these comments into living documentation.
Style Extraction
For CSS-in-JS solutions, I create type-safe style utilities:
import { css } from '@emotion/react';
type StyleProps = {
color?: ColorToken;
margin?: SpacingToken | SpacingToken[];
padding?: SpacingToken | SpacingToken[];
fontSize?: FontSizeToken;
};
// Helper that converts tokens to CSS
const createStyles = (props: StyleProps) => css`
${props.color && `color: ${getColor(props.color)};`}
${props.fontSize && `font-size: ${theme.fontSizes[props.fontSize]};`}
${props.margin && Array.isArray(props.margin)
? `margin: ${props.margin.map(m => getSpacing(m)).join(' ')};`
: props.margin && `margin: ${getSpacing(props.margin)};`
}
${props.padding && Array.isArray(props.padding)
? `padding: ${props.padding.map(p => getSpacing(p)).join(' ')};`
: props.padding && `padding: ${getSpacing(props.padding)};`
}
`;
// Usage
const styledComponent = css`
${createStyles({
color: 'primary300',
margin: ['md', 'lg'],
padding: 'sm',
fontSize: 'md'
})}
`;
Testing Type Compliance
To ensure components meet their type contracts at runtime, I implement specialized testing utilities:
import { render } from '@testing-library/react';
function testPropTypes<P>(
Component: React.ComponentType<P>,
validProps: P,
propVariations: Partial<Record<keyof P, any[]>>
) {
// Test with valid props
test('renders with valid props', () => {
const { container } = render(<Component {...validProps} />);
expect(container).toBeTruthy();
});
// Test with each prop variation
Object.entries(propVariations).forEach(([propName, values]) => {
values.forEach(value => {
test(`renders with ${String(propName)}=${String(value)}`, () => {
const props = { ...validProps, [propName]: value };
const { container } = render(<Component {...props} />);
expect(container).toBeTruthy();
});
});
});
}
// Usage
testPropTypes(
Button,
{ variant: 'primary', children: 'Click me' },
{
variant: ['secondary', 'ghost'],
size: ['small', 'medium', 'large'],
isDisabled: [true, false]
}
);
I've also created a comprehensive example that combines many of these techniques:
// Advanced component with theme integration, generics, and discriminated unions
import React from 'react';
// Theme types
interface ThemeTokens {
colors: {
primary: string;
secondary: string;
warning: string;
error: string;
success: string;
info: string;
grey50: string;
grey100: string;
grey500: string;
grey900: string;
};
spacing: {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
xxl: string;
};
fontSizes: {
xs: string;
sm: string;
md: string;
lg: string;
xl: string;
};
borderRadius: {
sm: string;
md: string;
lg: string;
pill: string;
};
}
// Card component with flexible content type
type CardBaseProps = {
title: string;
subtitle?: string;
elevation?: 0 | 1 | 2 | 3;
fullWidth?: boolean;
padding?: keyof ThemeTokens['spacing'];
borderRadius?: keyof ThemeTokens['borderRadius'];
};
type StandardCardProps<T> = CardBaseProps & {
variant: 'standard';
data: T;
renderContent: (data: T) => React.ReactNode;
};
type ActionCardProps<T> = CardBaseProps & {
variant: 'action';
data: T;
primaryAction: {
label: string;
onClick: (data: T) => void;
};
secondaryAction?: {
label: string;
onClick: (data: T) => void;
};
};
type StatusCardProps<T> = CardBaseProps & {
variant: 'status';
data: T;
status: 'warning' | 'error' | 'success' | 'info';
statusMessage: string;
};
export type CardProps<T> = StandardCardProps<T> | ActionCardProps<T> | StatusCardProps<T>;
export const Card = <T,>({
title,
subtitle,
elevation = 1,
fullWidth = false,
padding = 'md',
borderRadius = 'md',
...props
}: CardProps<T>): React.ReactElement => {
// Theme values from context (simplified for example)
const theme: ThemeTokens = {
colors: {
primary: '#1677ff',
secondary: '#6c757d',
warning: '#ffc107',
error: '#dc3545',
success: '#28a745',
info: '#17a2b8',
grey50: '#f8f9fa',
grey100: '#e9ecef',
grey500: '#adb5bd',
grey900: '#212529',
},
spacing: {
xs: '4px',
sm: '8px',
md: '16px',
lg: '24px',
xl: '32px',
xxl: '48px',
},
fontSizes: {
xs: '12px',
sm: '14px',
md: '16px',
lg: '18px',
xl: '20px',
},
borderRadius: {
sm: '4px',
md: '8px',
lg: '16px',
pill: '9999px',
},
};
// Common card styles
const cardStyle: React.CSSProperties = {
backgroundColor: theme.colors.grey50,
borderRadius: theme.borderRadius[borderRadius],
padding: theme.spacing[padding],
boxShadow: elevation > 0 ? `0 ${elevation * 2}px ${elevation * 4}px rgba(0, 0, 0, 0.1)` : 'none',
width: fullWidth ? '100%' : 'auto',
overflow: 'hidden',
};
// Header styles
const headerStyle: React.CSSProperties = {
marginBottom: theme.spacing.md,
};
const titleStyle: React.CSSProperties = {
fontSize: theme.fontSizes.lg,
margin: 0,
color: theme.colors.grey900,
};
const subtitleStyle: React.CSSProperties = {
fontSize: theme.fontSizes.sm,
margin: `${theme.spacing.xs} 0 0 0`,
color: theme.colors.grey500,
};
// Render card content based on variant
let cardContent: React.ReactNode;
let statusBar: React.ReactNode;
if (props.variant === 'standard') {
cardContent = props.renderContent(props.data);
} else if (props.variant === 'action') {
const { primaryAction, secondaryAction, data } = props;
const buttonBaseStyle: React.CSSProperties = {
padding: `${theme.spacing.sm} ${theme.spacing.md}`,
borderRadius: theme.borderRadius.sm,
border: 'none',
cursor: 'pointer',
fontSize: theme.fontSizes.sm,
fontWeight: 'bold',
};
const primaryButtonStyle: React.CSSProperties = {
...buttonBaseStyle,
backgroundColor: theme.colors.primary,
color: theme.colors.grey50,
};
const secondaryButtonStyle: React.CSSProperties = {
...buttonBaseStyle,
backgroundColor: 'transparent',
color: theme.colors.primary,
};
cardContent = (
<div style={{ display: 'flex', gap: theme.spacing.sm, justifyContent: 'flex-end' }}>
{secondaryAction && (
<button
style={secondaryButtonStyle}
onClick={() => secondaryAction.onClick(data)}
>
{secondaryAction.label}
</button>
)}
<button
style={primaryButtonStyle}
onClick={() => primaryAction.onClick(data)}
>
{primaryAction.label}
</button>
</div>
);
} else if (props.variant === 'status') {
const { status, statusMessage } = props;
const statusColors = {
warning: theme.colors.warning,
error: theme.colors.error,
success: theme.colors.success,
info: theme.colors.info,
};
statusBar = (
<div style={{
backgroundColor: statusColors[status],
padding: theme.spacing.sm,
color: status === 'warning' ? theme.colors.grey900 : theme.colors.grey50,
fontSize: theme.fontSizes.sm,
fontWeight: 'bold',
marginBottom: theme.spacing.md,
}}>
{statusMessage}
</div>
);
}
return (
<div style={cardStyle}>
{props.variant === 'status' && statusBar}
<div style={headerStyle}>
<h3 style={titleStyle}>{title}</h3>
{subtitle && <p style={subtitleStyle}>{subtitle}</p>}
</div>
{cardContent}
</div>
);
};
// Usage examples
interface UserData {
id: number;
name: string;
email: string;
}
// Standard card example
const StandardCardExample = () => {
const userData: UserData = {
id: 1,
name: "John Doe",
email: "john@example.com"
};
return (
<Card<UserData>
variant="standard"
title="User Information"
subtitle="Basic profile details"
data={userData}
elevation={2}
renderContent={(data) => (
<div>
<p><strong>Name:</strong> {data.name}</p>
<p><strong>Email:</strong> {data.email}</p>
</div>
)}
/>
);
};
// Action card example
const ActionCardExample = () => {
const userData: UserData = {
id: 1,
name: "John Doe",
email: "john@example.com"
};
return (
<Card<UserData>
variant="action"
title="User Actions"
subtitle="Manage user account"
data={userData}
elevation={1}
primaryAction={{
label: "Edit User",
onClick: (data) => console.log(`Editing user ${data.id}`)
}}
secondaryAction={{
label: "Delete",
onClick: (data) => console.log(`Deleting user ${data.id}`)
}}
/>
);
};
// Status card example
const StatusCardExample = () => {
const userData: UserData = {
id: 1,
name: "John Doe",
email: "john@example.com"
};
return (
<Card<UserData>
variant="status"
title="Account Status"
data={userData}
status="success"
statusMessage="Account verified successfully"
elevation={1}
/>
);
};
My experience has shown that investment in type safety pays dividends as design systems grow. Teams that adopt these TypeScript techniques report fewer bugs, faster development cycles, and improved developer confidence.
When implementing a design system with TypeScript, I recommend starting small and gradually building up your type complexity. Begin with basic interfaces and add advanced features like generics and conditional types as your team becomes more comfortable with the system.
The payoff comes when developers can confidently use components without constantly referring to documentation, knowing the type system will catch potential errors before they reach production.
101 Books
101 Books is an AI-driven publishing company co-founded by author Aarav Joshi. By leveraging advanced AI technology, we keep our publishing costs incredibly low—some books are priced as low as $4—making quality knowledge accessible to everyone.
Check out our book Golang Clean Code available on Amazon.
Stay tuned for updates and exciting news. When shopping for books, search for Aarav Joshi to find more of our titles. Use the provided link to enjoy special discounts!
Our Creations
Be sure to check out our creations:
Investor Central | Investor Central Spanish | Investor Central German | Smart Living | Epochs & Echoes | Puzzling Mysteries | Hindutva | Elite Dev | JS Schools
We are on Medium
Tech Koala Insights | Epochs & Echoes World | Investor Central Medium | Puzzling Mysteries Medium | Science & Epochs Medium | Modern Hindutva
Top comments (0)