Inertia react conventions
Agent skills for Laravel and PHP development. Installable via npx skills@latest add ceilidhboy/skills
npx -y skills add ceilidhboy/skills --skill inertia-react-conventionsAssembled from the repository path, not quoted from the project. Check it against their README if it does not work.
One thing to look at
- 0 stars0 stars. Stars are a popularity signal and not a quality one, but at this level it is likely that nobody has read this closely except its author, and you would be relying on your own review.
What its author says it does
Copied from the file, not written here
React component architecture and project conventions for Inertia.js v2 applications. Covers directory structure, structural vs markup components, DRY patterns, prop naming, and project-specific patterns.
SKILL.md
30.0 KB, as published. Nobody here has run it
Inertia.js v2 + React Development
When to Activate This Skill
Activate this skill whenever you are:
- Creating or modifying React pages in
resources/js/pages/ - Creating or modifying React components in
resources/js/components/ - Working with Inertia.js patterns like
<Link>,<Form>,useForm, orrouter - Implementing deferred props, infinite scrolling, lazy loading, polling, or prefetching
- Working with TypeScript in React components
- Organizing component structure and imports
Component Organization Structure
Directory Hierarchy
Components are organized hierarchically based on ownership and reusability:
resources/js/components/
├── clients/ # Page-specific components (kebab-case directory)
│ ├── index.ts # Barrel export
│ ├── SearchBar.tsx # Parent component (PascalCase)
│ ├── search-bar/ # Subcomponents directory (kebab-case)
│ │ ├── index.ts
│ │ └── SearchInput.tsx # Subcomponent (PascalCase)
│ ├── Table.tsx # Parent component (PascalCase)
│ └── table/ # Subcomponents directory (kebab-case)
│ ├── index.ts
│ ├── TableRow.tsx # Subcomponent (PascalCase)
│ ├── TableCell.tsx # Subcomponent (PascalCase)
│ └── table-row/ # Nested subcomponents (kebab-case)
│ ├── index.ts
│ └── TableRowActions.tsx
├── shared/ # Shared components (used across pages)
│ ├── NavMain.tsx
│ └── NavFooter.tsx
└── ui/ # UI primitives (shadcn/ui, etc.)
├── Button.tsx
└── Card.tsx
resources/js/pages/
├── Clients.tsx # Page component (PascalCase)
└── Dashboard.tsx
Key Pattern: The parent component file is a peer of its subcomponent directory, not inside it.
- ✅
SearchBar.tsxis a peer ofsearch-bar/directory - ✅
Table.tsxis a peer oftable/directory - ❌ NOT
SearchBar.tsxinsidesearch-bar/directory
Naming Conventions
- Directories: Always kebab-case (e.g.,
clients,search-bar,table) - Component Files: PascalCase, matching the component function name (e.g.,
SearchBar.tsx,Table.tsx)- ✅
SearchBar.tsxexportsfunction SearchBar() { ... } - ✅
Table.tsxexportsfunction Table() { ... } - ❌ NOT
search-bar.tsx(kebab-case files are for utilities, not components)
- ✅
- Page Files: PascalCase (e.g.,
Dashboard.tsx,Clients.tsx) - Utility/Helper Files: kebab-case (e.g.,
format-date.ts,api-client.ts) - Component Functions: PascalCase, drop redundant prefixes
- ✅
SearchBar(notClientsSearchBar) - ✅
Table(notClientsTable)
- ✅
- Types: PascalCase with
Propssuffix (e.g.,SearchBarProps)
Barrel Exports
Every component directory must have an index.ts file:
// resources/js/components/clients/index.ts
export { SearchBar } from './SearchBar';
export { Table } from './table';
This enables clean imports:
import { SearchBar, Table } from '@/components/clients';
Recursive Subcomponent Structure
If a component has subcomponents, create a kebab-cased subdirectory alongside the parent component file. The parent component is a peer of its subcomponent directory:
Correct Structure:
resources/js/components/dashboard/
├── Campaigns.tsx # ✅ Parent component (PascalCase)
├── campaigns/ # ✅ Subcomponents directory (kebab-case)
│ ├── index.ts
│ ├── Cards.tsx
│ └── cards/ # Nested subcomponents
│ ├── index.ts
│ ├── Card.tsx
│ └── card/
│ ├── index.ts
│ └── CardActions.tsx
├── RecentActivity.tsx # ✅ Parent component (PascalCase)
└── recent-activity/ # ✅ Subcomponents directory (kebab-case)
├── index.ts
├── Layout.tsx
├── Header.tsx
├── Activities.tsx
└── activity-item/ # Nested subcomponents
├── index.ts
├── ActivityItem.tsx
└── Icon.tsx
Incorrect Structure (DO NOT DO THIS):
resources/js/components/dashboard/recent-activity/
├── ActivityItem.tsx # ❌ WRONG: Parent inside its own directory
├── activity-item/
│ ├── Icon.tsx
│ └── index.ts
└── ...
The parent component file should always be at the same level as its kebab-case subcomponent directory.
Layout File Naming
Layout components should always be named Layout.tsx — never prefixed with the parent component name.
Correct:
├── RecentActivity.tsx
└── recent-activity/
├── Layout.tsx # ✅ Just "Layout", no prefix
└── Header.tsx
Incorrect:
├── RecentActivity.tsx
└── recent-activity/
├── RecentActivityLayout.tsx # ❌ WRONG: Redundant prefix
└── Header.tsx
The prefix is redundant because the file is already inside the recent-activity/ directory — context is clear from the path.
Component Composition Architecture: Structural Components vs. Markup Components
React components should follow a clear architectural pattern that separates concerns and keeps components focused and maintainable. There are two fundamental types of components:
1. Structural Components (Pure Composition)
Goal: Structural components should aim to be entirely composed of subcomponents with zero HTML markup. They are the "glue" that connects and coordinates subcomponents.
Purpose: Structural components determine the relationships between components in the application. They define how components connect, what data flows between them, and how they interact. The actual rendering of those relationships is delegated to markup components. This separation means:
- Structural components handle composition and relationships, i.e. the high-level structure of the application
- Markup components handle rendering and HTML structure
Characteristics:
- Import and compose subcomponents
- Handle data flow and state management
- Pass props to subcomponents
- Determine component relationships and hierarchy
- No HTML elements (no
<div>,<span>,<p>, etc.) - No inline styling or Tailwind classes
- Purely compositional - reads like a blueprint of the component structure
Example: Structural Component
// ✅ GOOD: Pure composition, no HTML markup
export function RecentActivity({ activities }: RecentActivityProps) {
return (
<Layout>
<Header title="Recent Activity" />
<Activities items={activities} />
</Layout>
);
}
Bad Example: Structural component with HTML Markup
// ❌ BAD: Mixing composition with HTML markup
export function RecentActivity({ activities }: RecentActivityProps) {
return (
<div className="space-y-4">
<div className="flex items-center justify-between">
<h2 className="text-lg font-semibold">Recent Activity</h2>
</div>
<Activities items={activities} />
</div>
);
}
2. Markup Components (HTML Structure)
Markup components contain the actual HTML elements and styling. They appear in two places in the component hierarchy:
A. Layout Components
Layout components wrap other components and provide the HTML structure, spacing, and styling. They are typically peers of the Structural component in the subcomponent directory.
Characteristics:
- Contain HTML wrapper elements (
<div>,<section>, etc.) - Apply spacing, layout, and structural styling
- Accept
childrenprop to wrap subcomponents - Keep markup focused on layout, not content
Example: Layout Component
// resources/js/components/dashboard/recent-activity/Layout.tsx
export function Layout({ children }: { children: ReactNode }) {
return (
<div className="space-y-4 rounded-lg border border-slate-200 bg-white p-6 dark:border-slate-700 dark:bg-slate-800">
{children}
</div>
);
}
B. Leaf Node Components
Leaf nodes are the deepest level of the component hierarchy. They contain the actual HTML elements that render content (text, icons, buttons, etc.).
Characteristics:
- Contain HTML elements that display content
- Apply styling to individual elements
- No subcomponents (or only UI primitives like
<Button>,<Badge>) - Focused on rendering a single piece of content
- CRITICAL: Extract repeated markup patterns into reusable leaf components to avoid duplication
Example: Leaf Node Component
// resources/js/components/dashboard/recent-activity/activity-item/ActivityItem.tsx
export function ActivityItem({ activity }: ActivityItemProps) {
return (
<div className="flex items-start gap-3 border-b border-slate-200 pb-3 last:border-b-0 dark:border-slate-700">
<Icon type={activity.type} />
<div className="flex-1">
<p className="text-sm font-medium text-slate-900 dark:text-white">
{activity.title}
</p>
<p className="text-xs text-slate-500 dark:text-slate-400">
{activity.timestamp}
</p>
</div>
</div>
);
}
Example: Extracting Repeated Markup Patterns
When you notice the same markup structure repeating multiple times, extract it into a reusable leaf component:
// ❌ BAD: Massive duplication - same markup repeated 4 times
export function JobDetails({ job }: JobDetailsProps) {
return (
<div className="flex flex-col gap-6">
{/* Company - repeated structure */}
<div className="flex items-start gap-3">
<IconSquare icon={Building2} size="md" rounded="md" />
<div>
<p className="text-xs font-medium text-slate-500">Company</p>
<p className="mt-1 text-sm text-slate-900">{job.company.name}</p>
<p className="text-xs text-slate-600">{job.company.site}</p>
</div>
</div>
{/* Address - SAME STRUCTURE DUPLICATED */}
<div className="flex items-start gap-3">
<IconSquare icon={MapPin} size="md" rounded="md" />
<div>
<p className="text-xs font-medium text-slate-500">Address</p>
<p className="mt-1 text-sm text-slate-900">{job.company.address}</p>
</div>
</div>
{/* Due Date - SAME STRUCTURE DUPLICATED */}
<div className="flex items-start gap-3">
<IconSquare icon={Calendar} size="md" rounded="md" />
<div>
<p className="text-xs font-medium text-slate-500">Due Date</p>
<p className="mt-1 text-sm text-slate-900">{job.dueDateForPlacement}</p>
</div>
</div>
{/* Duration - SAME STRUCTURE DUPLICATED */}
<div className="flex items-start gap-3">
<IconSquare icon={Clock} size="md" rounded="md" />
<div>
<p className="text-xs font-medium text-slate-500">Duration</p>
<p className="mt-1 text-sm text-slate-900">{job.duration}</p>
</div>
</div>
</div>
);
}
Problem with the above:
- Any change to the icon styling requires 4 edits
- Any change to the text styling requires 4 edits
- Hard to maintain consistency
- Violates DRY principle
// ✅ GOOD: Extract the repeated pattern into a reusable leaf component
// resources/js/components/job/job-information/InformationItem.tsx
type InformationItemProps = {
icon: LucideIcon;
label: string;
children: ReactNode;
};
export function InformationItem({ icon, label, children }: InformationItemProps) {
return (
<div className="flex items-start gap-3">
<IconSquare icon={icon} size="md" rounded="md" bgColor="bg-slate-400/25" iconColor="text-slate-600" />
<div>
<p className="text-xs font-medium text-slate-500 dark:text-slate-400">
{label}
</p>
<div className="mt-1 text-sm text-slate-900 dark:text-white">
{children}
</div>
</div>
</div>
);
}
// Now use it in JobDetails - clean and DRY
export function JobDetails({ job }: JobDetailsProps) {
return (
<div className="flex flex-col gap-6">
<InformationItem icon={Building2} label="Company">
<p>{job.company.name}</p>
<p className="text-xs text-slate-600 dark:text-slate-400">{job.company.site}</p>
</InformationItem>
<InformationItem icon={MapPin} label="Address">
<p>{job.company.address}</p>
</InformationItem>
<InformationItem icon={Calendar} label="Due Date for Placement">
<p>{job.dueDateForPlacement}</p>
</InformationItem>
<InformationItem icon={Clock} label="Duration">
<p>{job.duration}</p>
</InformationItem>
</div>
);
}
Benefits:
- Single source of truth for the markup structure
- Changes to styling only need to be made once
- Easy to maintain consistency
- Follows DRY principle
- Makes the parent component more readable
Component Hierarchy Example
Here's how these three types work together:
RecentActivity.tsx (Structural - Pure Composition)
├── Layout.tsx (Markup - Layout Component)
├── Header.tsx (Markup - Leaf Node)
├── Activities.tsx (Structural - Composes ActivityItem)
│ └── activity-item/
│ ├── ActivityItem.tsx (Markup - Leaf Node)
│ └── Icon.tsx (Markup - Leaf Node)
Reading the Structural component tells you the structure:
// RecentActivity.tsx - You can see the entire structure at a glance
export function RecentActivity({ activities }: RecentActivityProps) {
return (
<Layout>
<Header title="Recent Activity" />
<Activities items={activities} />
</Layout>
);
}
Benefits of This Architecture
- Clarity: Structural components are easy to understand - they show the structure without HTML noise
- Maintainability: Changes to layout or styling are isolated in layout/leaf components
- Reusability: Subcomponents can be reused in different contexts
- Testability: Each component has a single responsibility
- Scalability: Easy to add new subcomponents without cluttering the orchestrator
When to Create a Layout Component
Create a layout component when:
- Multiple subcomponents need to be wrapped in the same HTML structure
- The wrapper has styling or spacing that's specific to that component
- You want to keep the orchestrator purely compositional
Example: When to extract a layout component
// ❌ BAD: Structural component has HTML markup
export function JobInformation({ job }: JobInformationProps) {
return (
<div className="space-y-8">
<div className="flex gap-6">
<JobDetails job={job} />
<Description description={job.description} />
</div>
<RequiredSkills skills={job.requiredSkills} />
</div>
);
}
// ✅ GOOD: Extract layout, keep orchestrator pure
export function JobInformation({ job }: JobInformationProps) {
return (
<Layout>
<JobDetails job={job} />
<Description description={job.description} />
<RequiredSkills skills={job.requiredSkills} />
</Layout>
);
}
// resources/js/components/job/job-information/Layout.tsx
export function Layout({ children }: { children: ReactNode }) {
return (
<div className="space-y-8">
{children}
</div>
);
}
Key Principles
- Clear Ownership: Page-specific components grouped with their page
- Minimal Naming: Hierarchical structure allows dropping redundant prefixes
- Clean Imports: Barrel exports eliminate long import paths
- Scalability: Easy to add new pages and components
- Reusability: Shared components clearly separated
DRY Principle: Avoid Code Duplication
CRITICAL: Never create duplicate or near-duplicate code. Always extract common code into reusable components, functions, or utilities.
When to Extract Common Code
Extract common code when you notice:
- Identical or near-identical JSX structures across multiple components
- Repeated logic patterns (e.g., pagination controls, form handling, data filtering)
- Duplicate styling patterns that appear in multiple places
- Similar prop handling across different components
How to Extract Common Code
1. Extract Components for Shared UI Patterns
When multiple components have similar JSX structures, extract a reusable component and parameterize the differences.
Bad: Duplicate pagination controls in two different components
// Campaigns.tsx
<div className="flex items-center gap-2">
<span className="text-xs font-medium bg-slate-200 px-2 py-1 rounded-full">
{campaigns.total}
</span>
<div className="flex items-center gap-1">
<Link href={onPreviousUrl} disabled={currentPage === 1}>
<ChevronLeft className="h-4 w-4" />
</Link>
<span>{currentPage}/{lastPage}</span>
<Link href={onNextUrl} disabled={currentPage === lastPage}>
<ChevronRight className="h-4 w-4" />
</Link>
</div>
</div>
// RecentActivity.tsx - IDENTICAL CODE DUPLICATED
<div className="flex items-center gap-2">
<span className="text-xs font-medium bg-slate-200 px-2 py-1 rounded-full">
{activities.total}
</span>
<div className="flex items-center gap-1">
<Link href={onPreviousUrl} disabled={currentPage === 1}>
<ChevronLeft className="h-4 w-4" />
</Link>
<span>{currentPage}/{lastPage}</span>
<Link href={onNextUrl} disabled={currentPage === lastPage}>
<ChevronRight className="h-4 w-4" />
</Link>
</div>
</div>
Good: Extract into a reusable component
// shared/PaginationControls.tsx
export function PaginationControls({
currentPage,
lastPage,
total,
onPreviousUrl,
onNextUrl,
}: PaginationControlsProps) {
return (
<div className="flex items-center gap-2">
<span className="text-xs font-medium bg-slate-200 px-2 py-1 rounded-full">
{total}
</span>
<div className="flex items-center gap-1">
<Link href={onPreviousUrl} disabled={currentPage === 1}>
<ChevronLeft className="h-4 w-4" />
</Link>
<span>{currentPage}/{lastPage}</span>
<Link href={onNextUrl} disabled={currentPage === lastPage}>
<ChevronRight className="h-4 w-4" />
</Link>
</div>
</div>
);
}
// Campaigns.tsx - Now uses the shared component
<PaginationControls
currentPage={campaigns.current_page}
lastPage={campaigns.last_page}
total={campaigns.total}
onPreviousUrl={onPreviousUrl}
onNextUrl={onNextUrl}
/>
// RecentActivity.tsx - Also uses the shared component
<PaginationControls
currentPage={activities.current_page}
lastPage={activities.last_page}
total={activities.total}
onPreviousUrl={onPreviousUrl}
onNextUrl={onNextUrl}
/>
2. Extract Functions for Shared Logic
When multiple components have the same logic, extract it into a utility function or custom hook.
Bad: Duplicate pagination calculation logic
// Campaigns.tsx
const totalPages = Math.ceil(campaigns.length / campaignsPerPage);
const startIndex = currentPage * campaignsPerPage;
const endIndex = startIndex + campaignsPerPage;
const currentItems = campaigns.slice(startIndex, endIndex);
// RecentActivity.tsx - SAME LOGIC DUPLICATED
const totalPages = Math.ceil(activities.length / activitiesPerPage);
const startIndex = currentPage * activitiesPerPage;
const endIndex = startIndex + activitiesPerPage;
const currentItems = activities.slice(startIndex, endIndex);
Good: Extract into a utility function
// lib/pagination.ts
export function getPaginatedItems<T>(
items: T[],
currentPage: number,
itemsPerPage: number
) {
const totalPages = Math.ceil(items.length / itemsPerPage);
const startIndex = currentPage * itemsPerPage;
const endIndex = startIndex + itemsPerPage;
const paginatedItems = items.slice(startIndex, endIndex);
return { paginatedItems, totalPages };
}
// Campaigns.tsx
const { paginatedItems, totalPages } = getPaginatedItems(
campaigns,
currentPage,
campaignsPerPage
);
// RecentActivity.tsx
const { paginatedItems, totalPages } = getPaginatedItems(
activities,
currentPage,
activitiesPerPage
);
3. Parameterize Differences
When extracting common code, identify what varies between uses and make those differences parameters.
Example: Generic component with parameterized differences
// ✅ GOOD: Differences are parameterized
export function PaginationControls({
currentPage,
lastPage,
total,
onPreviousUrl,
onNextUrl,
}: PaginationControlsProps) {
// Common structure, parameterized values
return (
<div className="flex items-center gap-2">
<span className="text-xs font-medium bg-slate-200 px-2 py-1 rounded-full">
{total}
</span>
{/* ... pagination buttons using parameters ... */}
</div>
);
}
When NOT to Extract
Don't extract code if:
- It's only used in one place (no duplication yet)
- Extracting would make the code harder to understand (premature abstraction)
- The code is trivial (e.g., a single line)
However, if you're creating a new component and notice it's very similar to an existing one, extract the common parts before creating the new component. This prevents duplication from being introduced in the first place.
Checklist Before Creating New Components
Before creating a new component, ask yourself:
- Does a similar component already exist?
- Can I reuse an existing component with different props?
- Can I extract common UI patterns into a shared component?
- Can I extract common logic into a utility function or hook?
- Are there duplicate styling patterns I should consolidate?
If you answer "yes" to any of these, refactor first before creating the new component.
Prop Naming: Avoid Redundant Prefixes
When naming props, avoid repeating context that's already clear from the component name. The component name provides semantic meaning, so prop names should be concise.
Bad: Redundant prefix repeats component context
// Component name already indicates this is about active jobs
function ActiveBadge({ activeJobsCount }: Props) { ... }
Good: Prop name is concise, context comes from component name
function ActiveBadge({ count }: Props) { ... }
Another example:
- ❌
<SearchBar searchQuery={query} />— "search" is redundant - ✅
<SearchBar query={query} />— context is clear from component name
This keeps code DRY and makes props more reusable across different contexts.
Currency Formatting
formatCurrency Utility
The formatCurrency() utility in resources/js/lib/infrastructure/currency.ts converts pence amounts to formatted currency strings with the pound symbol and thousand separators.
Location: resources/js/lib/infrastructure/currency.ts
Function Signature:
export function formatCurrency(amountInCents: number): string
Usage:
import { formatCurrency } from '@/lib/infrastructure/currency';
formatCurrency(154040) // Returns: "£1,540.40"
formatCurrency(50) // Returns: "£0.50"
formatCurrency(0) // Returns: "£0.00"
Key Features:
- Converts pence (integer) to pounds with 2 decimal places
- Adds thousand separators (e.g., "£1,540.40" not "£1540.40")
- Always displays exactly 2 decimal places (e.g., "£0.50" not "£0.5")
- Adds the pound symbol (£) prefix
When to Use:
- Displaying monetary values from the backend (which are stored as integers in pence)
- Formatting invoice amounts, billing totals, or any financial metrics
- In wrapper components that need to format data before passing to generic display components
Backend Storage Convention:
All monetary values in the database are stored as integers representing pence to avoid floating-point precision errors. When displaying these values to users, always use formatCurrency() to convert them to the proper currency format.
Branded Types for Type-Safe Currency Values
Overview
Branded types provide compile-time type safety for currency values, preventing accidental mixing of different currencies or units. This is a TypeScript pattern that uses a private unique symbol to create nominal typing in a structurally-typed language.
Location: resources/js/types/currency.ts
The Problem Branded Types Solve
Without branded types, all currency values are just plain numbers:
// ❌ UNSAFE: No way to distinguish between different currencies
const sterlingAmount: number = 1500; // Is this pence or pounds?
const dollarAmount: number = 5000; // Is this cents or dollars?
// Easy to accidentally mix them
const total = sterlingAmount + dollarAmount; // TypeScript allows this!
The Solution: Branded Types
Branded types create distinct types for each currency:
// ✅ SAFE: Each currency is a distinct type
const sterlingAmount: SterlingPence = asCurrency<SterlingPence>(1500);
const dollarAmount: DollarCents = asCurrency<DollarCents>(5000);
// TypeScript prevents mixing them
const total = sterlingAmount + dollarAmount; // ❌ Type error!
Available Branded Types
import {
asCurrency,
type SterlingPence,
type DollarCents,
type EuroCents,
type SupportedCurrency,
type Metric
} from '@/types/currency';
Branded Currency Types:
SterlingPence— British pounds in pence units (e.g., 1500 = £15.00)DollarCents— US dollars in cents units (e.g., 5000 = $50.00)EuroCents— Euros in cents units (e.g., 10000 = €100.00)
Union Types:
SupportedCurrency— Union of all supported currencies (closed set)
The Metric Discriminated Union
For dashboard metrics that can be either currency amounts or counts, use the Metric discriminated union from resources/js/components/dashboard/shared/types.ts:
import type { Metric } from '@/components/dashboard/shared';
type Metric =
| { kind: 'sterling'; amount: SterlingPence }
| { kind: 'dollar'; amount: DollarCents }
| { kind: 'euro'; amount: EuroCents }
| { kind: 'count'; count: number };
This combines:
- A discriminator field (
kind) that identifies the metric type - Branded types for currency values to ensure type safety
Location: resources/js/components/dashboard/shared/types.ts
The Metric type is dashboard-specific (not in the currency types file) because it includes a count option which is not a currency value.
Usage Pattern: Boundary Conversion
Convert backend plain numbers to branded types at the component boundary:
import { asCurrency, type SterlingPence, type Metric } from '@/types/currency';
function MyComponent({ value }: { value: number }) {
// Convert at the boundary
const metric: Metric = {
kind: 'sterling',
amount: asCurrency<SterlingPence>(value)
};
// Now metric is type-safe throughout the component
return <Summary metric={metric} />;
}
Using Metrics in Components
When a component receives a Metric, use the discriminator to handle different types:
function Summary({ metric }: { metric: Metric }) {
const displayValue = metric.kind === 'count'
? String(metric.count)
: formatCurrency(metric.amount);
return <div>{displayValue}</div>;
}
TypeScript automatically narrows the type based on the discriminator, so:
- When
metric.kind === 'count', TypeScript knowsmetric.countexists - When
metric.kind === 'sterling', TypeScript knowsmetric.amountisSterlingPence
Extending with New Currencies
To add a new currency (e.g., YenSen):
-
Add the branded type in
resources/js/types/currency.ts:export type YenSen = number & { readonly [__brand]: 'YenSen' }; -
Update SupportedCurrency union:
export type SupportedCurrency = SterlingPence | DollarCents | EuroCents | YenSen; -
Update Metric union:
export type Metric = | { kind: 'sterling'; amount: SterlingPence } | { kind: 'dollar'; amount: DollarCents } | { kind: 'euro'; amount: EuroCents } | { kind: 'yen'; amount: YenSen } | { kind: 'count'; count: number };
TypeScript will automatically report exhaustiveness errors in all switch statements and type guards that don't handle the new currency, forcing you to update all affected code.
Key Benefits
- ✅ Compile-time safety: Prevents mixing different currencies
- ✅ Exhaustiveness checking: Adding new currencies forces updates everywhere
- ✅ Zero runtime overhead: Branded types are erased at runtime
- ✅ Self-documenting: Type names clearly indicate the unit (Pence, Cents)
- ✅ Fail-fast: Invalid data throws errors immediately, never silently converts