This week I pulled out my ExperienceTeaser component and replaced it with ExperienceGraph — a timeline visualization that shows career progression more clearly than a plain list ever could. It felt like a straightforward component swap, but halfway through I realized the data underneath was wrong. Not broken-wrong. Shaped-wrong. The old structure had grown up around the old component, and the new one needed something different.
That's the thing nobody warns you about when you refactor UI: the data model is almost always coupled to the component it was built for. You just can't see it until you try to hand that data to something new.
The Invisible Coupling
My experience data started life as a flat array of entries, each one a plain object with a handful of string fields. That was fine for ExperienceTeaser, which just iterated the list and rendered a title, a company name, and a date range per row. Simple consumer, simple shape.
// What the old component silently assumed
interface ExperienceItem {
title: string;
company: string;
period: string; // "Jan 2023 – Present"
description: string;
}
const experience: ExperienceItem[] = [
{
title: "Full-Stack Engineer",
company: "Acme Corp",
period: "Jan 2023 – Present",
description: "Built and maintained internal tooling.",
},
// ...
];
Notice period — a single formatted string. That works fine when you're just printing it. The moment you want to compute with it — draw a proportional timeline bar, calculate duration, sort by recency — you're stuck parsing a human-readable string. The old component never needed to do any of that, so the bad shape went unnoticed for months.
ExperienceGraph needed to know start dates, end dates, and which roles belonged to the same company. The flat list couldn't express any of that without manual parsing.
Restructuring the Model
The fix was to separate the facts from the formatting, and to lift company into the structural layer instead of leaving it as a repeated string field.
interface ExperienceRole {
title: string;
startDate: string; // ISO date: "2023-01"
endDate: string | null; // null = current position
highlights: string[];
}
interface ExperienceEntry {
company: string;
url?: string;
roles: ExperienceRole[];
}
const experience: ExperienceEntry[] = [
{
company: "Acme Corp",
roles: [
{
title: "Full-Stack Engineer",
startDate: "2023-01",
endDate: null,
highlights: [
"Redesigned internal deployment pipeline",
"Reduced build times by extracting shared modules",
],
},
],
},
];
A few things changed here that matter:
period → startDate + endDate. Dates as ISO strings instead of a preformatted range. The component can format them however it wants; the data doesn't make that decision anymore. If I ever want to show "2 years, 4 months" instead of "Jan 2023 – Present," I change one formatting function, not the data file.
description → highlights[]. A single blob of prose is hard to render in a compact visualization. A list of short statements is more composable — I can show three bullets in a timeline card, or collapse to just the title if space is tight.
Company as the top-level grouping. When you work two roles at the same company, a flat list can't represent that without repeating the company name (and risking inconsistency). The nested structure makes that relationship explicit.
TypeScript Did the Heavy Lifting
Here's where TypeScript actually earned its keep. As soon as I updated data/experience.ts to use ExperienceEntry[], the compiler immediately flagged every file that consumed the old shape. I didn't have to grep for experience.period across the codebase — the type errors did it for me.
This is the real value of typing your data models tightly: refactors become findable. With a any[] or loosely-typed array, you'd ship the restructured data and discover the breakage at runtime, possibly only on the pages you didn't manually check. TypeScript makes the coupling visible at compile time.
The one thing I had to be careful about: TypeScript infers types from how you use values, not from what you intend. If I had written the new data inline without an explicit type annotation on the const, the compiler would have accepted whatever shape I typed and I'd lose the safety net for consumers. Always annotate your data constants explicitly:
// Inferred — compiler accepts anything you type
const experience = [...];
// Explicit — compiler checks every entry against the type
const experience: ExperienceEntry[] = [...];
Small habit, big difference.
What the Graph Actually Needed
ExperienceGraph renders a horizontal timeline where bar widths are proportional to duration. With raw date strings, that's a few lines of math:
function durationInMonths(start: string, end: string | null): number {
const s = new Date(start);
const e = end ? new Date(end) : new Date();
return (e.getFullYear() - s.getFullYear()) * 12 + (e.getMonth() - s.getMonth());
}
Try doing that with "Jan 2023 – Present". You'd be writing a parser for a string format you invented, which means you're also maintaining it. Structured data eliminates that whole class of problem.
This is the same lesson I ran into building OrderX — when the menu data model was designed for a simple list view, adding filtering and sorting by category meant retrofitting structure that should have been there from the start. The data shape you choose upfront constrains what the UI can do later, often invisibly.
A Note on Coming Soon States
While restructuring the experience data, I also added a comingSoon flag to my project data for work that's still in development. Same principle applies: don't encode display logic in the label string (title: "My Project (Coming Soon)"). Put it in the data, handle it in the component.
interface Project {
title: string;
slug: string;
comingSoon?: boolean;
}
One boolean, consumed however the UI needs — grayed out card, a badge, a locked cursor, disabled link. The data doesn't know and shouldn't care.
Takeaway
When you replace a component, audit the data model it consumed before you write a line of the new component. Flat strings that were "good enough" for a simple list are almost never good enough for a visualization. Pull dates out of prose, lift groupings into structure, and annotate your data constants with explicit types so the compiler tells you what broke instead of your users.
The new shape is almost always more code in the data file and less code in the component — which is exactly the trade you want.