📰 IT NachrichtenToday’s NYT Mini Crossword Answers for Saturay, Sept. 12(12.09.2026 um 07:43 Uhr)
🔧 AI Nachrichten Etzioni on AI: What kids tell chatbots, but not you(04.09.2026 um 16:05 Uhr)
🔧 AI Nachrichten OpenAI Wants to Know if an AI Industry Slowdown Would Even Be Legal(11.09.2026 um 01:28 Uhr)
🔧 AI Nachrichten OpenAI puts Pro subscriptions on hold due to Astra demand(10.09.2026 um 22:59 Uhr)
🔧 AI Nachrichten OpenAI’s feud with mathematicians is only escalating(11.09.2026 um 22:57 Uhr)
📰 IT NachrichtenToday’s NYT Mini Crossword Answers for Saturay, Sept. 12(12.09.2026 um 07:43 Uhr)
🔧 AI Nachrichten Etzioni on AI: What kids tell chatbots, but not you(04.09.2026 um 16:05 Uhr)
🔧 AI Nachrichten OpenAI Wants to Know if an AI Industry Slowdown Would Even Be Legal(11.09.2026 um 01:28 Uhr)
🔧 AI Nachrichten OpenAI puts Pro subscriptions on hold due to Astra demand(10.09.2026 um 22:59 Uhr)
🔧 AI Nachrichten OpenAI’s feud with mathematicians is only escalating(11.09.2026 um 22:57 Uhr)

🔧 Programmierung 🕛 vor 2 Monaten 9 Min Lesezeit
0

We Were Always Building State Machines — From .NET ViewBag to TypeScript Discriminated Unions

↗ Quelle (dev.to)
🗣️ Stimme:

The Old Way: Dynamic Magic and Its Hidden Costs



Years ago, in ASP.NET MVC and Razor, we had a seductively quick tool for passing state to the UI: ViewBag.




CODE
// Controller
ViewBag.Role = "Admin";
ViewBag.CanEdit = true;
ViewBag.Theme = "Dark";
ViewBag.FeatureFlags = new[] { "beta_dashboard", "export_csv" };






And in the Razor view:




CODE
@if(ViewBag.CanEdit) {
<button>Edit</button>
}

@if(ViewBag.Role == "Admin") {
<admin-panel />
}






It was fast. It was flexible. It got the job done on Friday at 5 PM.



But the lurking problems went deeper than most developers realized:



The obvious problems:



· String-based keys with no contracts

· Typos silently failing at runtime (ViewBag.CanEdit vs ViewBag.Canedit)

· No autocomplete, no IntelliSense

· Refactoring meant "find and pray"



The deeper architectural wounds:



· Truth multiplicity: The string "Admin" existed in controllers, views, partials, and JavaScript files — with no single source of truth

· Impossible static analysis: No tool could trace the flow of ViewBag.IsAdmin from assignment to consumption

· Stringly-typed death: "admin" vs "Admin" vs "ADMIN" — which one is correct? All of them, until one isn't

· Ghost properties: ViewBag.Role could be null, undefined, or simply never set — and you'd only discover which at runtime

· No serialization contract: When your Razor view also needed to pass state to JavaScript, you were manually building JSON from dynamic properties



We were building distributed state machines across two runtimes (server and browser) — with zero type system support, zero serialization guarantees, and zero compile-time verification.





The First Evolution: Numeric Enums (The Baby Step)



TypeScript's first offering seemed like salvation:




CODE
enum UserRole {
Guest, // 0
User, // 1
Admin // 2
}






And in the component:




CODE
if (user.role === UserRole.Admin) {
// render admin controls
}






This was better. We had autocomplete. Refactoring worked. But a silent danger remained:




CODE
// This compiles fine. No error.
if (user.role === 2) {
// Is this Admin? Guest + 2? Who knows?
}

// API returns { role: 2 }. Is that valid? What about 999?
const role: UserRole = 999; // TypeScript allows this!






Numeric enums are fundamentally porous. Any number can inhabit them. They're not type-safe in the meaningful sense — they're just named constants with a hole in the bottom.






The True Upgrade: String Enumerations as Nominal Types



This is where the real safety begins:




CODE
enum UserRole {
Guest = "GUEST",
User = "USER",
Admin = "ADMIN"
}

enum Permission {
Read = "READ",
Write = "WRITE",
Delete = "DELETE"
}






Now something profound happens:




CODE
// ❌ TypeScript ERROR: Type '"ADMIN"' is not assignable to type 'UserRole'
const role: UserRole = "ADMIN";

// ❌ ERROR: Type '"READ"' cannot be assigned to 'UserRole'
const perm: UserRole = Permission.Read;

// ✅ Only this works
const role: UserRole = UserRole.Admin;






This is nominal typing in a structural type system. Even though UserRole.Admin resolves to the string "ADMIN", TypeScript treats the enum member as a distinct, opaque type. You cannot accidentally pass a raw string. You cannot mix enums that share the same string value.



This is not just type safety — it's domain safety.



Why String Enums Beat Numeric Enums
















































Concern Numeric Enum String Enum
Debugging 0, 1, 2 in logs "ADMIN" in logs
Serialization Numbers — ambiguous Strings — self-describing
API contracts Fragile to reordering Stable across versions
Database storage Requires lookup table Human-readable directly
Cross-system communication Number mapping required String — universal
Type safety Porous (any number works) Opaque (only enum member)
Refactoring safety Renaming safe, reordering unsafe Fully safe


String enums aren't just "better enums." They're a different category of safety.






Beyond Enums: The Full Type System Arsenal



The journey doesn't end with string enums. Modern TypeScript offers increasingly powerful tools for modeling UI state — each building on the last.



Level 1: String Enums (Compile-time constants)




CODE
enum Theme { Light = "LIGHT", Dark = "DARK" }






Safety: You can't use the wrong string.

Weakness: You can forget to handle a variant.



Level 2: Union Types (Exhaustiveness via compiler flags)




CODE
type Theme = "light" | "dark" | "system";






Safety: Lighter syntax than enums.

Weakness: Still just strings at runtime. No associated data.



Level 3: Discriminated Unions (Full state machines with associated data)




CODE
type UserState = {
role: UserRole;
permissions: Permission[];
name: string;
};
type UIState =
| { status: "loading"; startedAt: number }
| { status: "loaded"; data: UserState; permissions: Permission[] }
| { status: "error"; message: string; code: number; retry: () => void }
| { status: "unauthorized"; requiredRole: UserRole };






Safety: The compiler proves you've handled every state.

Power: Each state carries its own specific data shape.



Level 4: Template Literal Types (Compile-time string computation)




CODE
type EventName = `user:${UserRole}:${Permission}`;
// "user:GUEST:READ" | "user:GUEST:WRITE" | "user:USER:READ" | ...






Safety: String patterns verified at compile time.

Use case: WebSocket events, Redux action types, analytics events.



Level 5: Const Assertions and as const (Immutable value-level definitions)




CODE
const ROLES = ["GUEST", "USER", "ADMIN"] as const;
type UserRole = typeof ROLES[number]; // "GUEST" | "USER" | "ADMIN"

// Single source of truth: runtime array AND compile-time type






Safety: No duplication between runtime values and types.

Power: Iterate at runtime, type-check at compile time.





The Pattern That Changes Everything: Exhaustiveness Checking



Here's where discriminated unions transcend simple enums:




CODE
type UIState = 
| { status: "loading" }
| { status: "loaded"; data: UserState }
| { status: "error"; message: string }
| { status: "unauthorized" };

function render(state: UIState): JSX.Element {
switch(state.status) {
case "loading":
return <Spinner />;
case "loaded":
return <Dashboard data={state.data} />;
case "error":
return <Error message={state.message} />;
case "unauthorized":
return <Login />;
default: {
// This line is the magic.
// If we add a new status and forget to handle it,
// TypeScript ERROR: 'state' is not 'never'
const _exhaustive: never = state;
return _exhaustive;
}
}
}






This is what ViewBag could never do. The compiler proves you've considered every possible state. Add a new variant to UIState, and your code won't compile until you handle it everywhere.



This isn't just type checking. This is behavioral proof.






The Architectural Comparison: Five Dimensions of Safety



Let's compare across the full spectrum:














































































Dimension ViewBag Numeric Enum String Enum Discriminated Union
Type checking None (runtime) Porous (any number) Opaque (member only) Provable (exhaustive)
Serialization Manual, fragile Numbers, ambiguous Self-describing strings Self-describing + data
Invalid states All invalid states representable Extra numbers allowed Only valid members Impossible to represent
Missing handlers Silent runtime bug Silent runtime bug Silent runtime bug Compile error
Associated data Any shape, no contract None None Per-state data shapes
Cross-boundary safety Zero Low Medium High
Refactoring safety None (string find) Partial (number drift) Full (exhaustive check) Full (exhaustive check)
Documentation Scattered, implicit Enum name only Enum name only Type IS documentation
Runtime overhead Dynamic dispatch Enum object Enum object None (compiled away)





The Deeper Insight: We Were Always Doing This



Here's what struck me when I traced this evolution:



The concept hasn't changed. The vocabulary and tooling have.



In the Razor days, we were accidentally building distributed state machines with string keys and dynamic objects. We had:



· No name for the pattern

· No compiler checking our logic

· No way to prove completeness

· No serialization guarantees



Today, TypeScript gives us the vocabulary:



· Discriminated unions = State machines

· Exhaustiveness checking = Proof of completeness

· String enums = Opaque domain values

· Template literal types = Computed domain strings

· as const = Single source of truth



We're doing the exact same job: communicating state, permissions, and behavior from backend to UI. But now:



· The compiler is on our team

· Invalid states are inexpressible

· Missing handlers are compile errors

· The type system documents our intent





What We Gained, What We Lost



It's worth being honest about the tradeoffs.



What we gained:



· Safety at scale — refactor without fear

· Compiler-verified completeness

· Self-documenting code

· Cross-team contracts (backend can export TypeScript types)

· CI/CD catching errors before deployment



What we lost:



· Prototyping speed — ViewBag took zero setup

· The ability to pass anything without defining it first

· A certain productive chaos during exploration phases



But here's the thing: the friction TypeScript introduces is productive friction. It asks you to define your types before you've fully understood them — and in doing so, it forces you to understand your domain faster.



The craft is knowing when to stay dynamic (prototyping, exploration) and when to formalize (production, team code, maintained systems).





Practical Architecture: How I Model UI State Now



Here's my current approach for a feature with loading, data, and error states:




CODE
type UserState = {
role: UserRole;
permissions: Permission[];
name: string;
};
// 1. Domain enums — string-based, opaque
enum UserRole {
Guest = "GUEST",
User = "USER",
Admin = "ADMIN"
}

enum Permission {
Read = "READ",
Write = "WRITE",
Delete = "DELETE"
}

// 2. Role-to-permission mapping — compile-time checked
const ROLE_PERMISSIONS: Record<UserRole, Permission[]> = {
[UserRole.Guest]: [Permission.Read],
[UserRole.User]: [Permission.Read, Permission.Write],
[UserRole.Admin]: [Permission.Read, Permission.Write, Permission.Delete],
};

// TypeScript ERROR if we forget a role — Record enforces completeness

// 3. UI state as discriminated union
type DashboardState =
| { status: "loading" }
| { status: "loaded"; user: UserState }
| { status: "error"; message: string; retry: () => void }
| { status: "unauthorized"; requiredRole: UserRole };

// 4. Permission check — type-safe, not string-checked
function canPerform(user: UserState, permission: Permission): boolean {
return user.permissions.includes(permission);
}

// 5. Component with exhaustive rendering
function Dashboard({ state }: { state: DashboardState }) {
switch(state.status) {
case "loading":
return <Skeleton />;
case "loaded":
if (canPerform(state.user, Permission.Write)) {
return <EditableDashboard user={state.user} />;
}
return <ReadOnlyDashboard user={state.user} />;
case "error":
return <ErrorBanner message={state.message} onRetry={state.retry} />;
case "unauthorized":
return <RoleUpgradePrompt required={state.requiredRole} />;
}
}






Every invalid state is impossible. Every missing handler is a compile error. Every permission check is type-safe. Every refactoring is automated.






Conclusion: The Story Continues



When I trace the line from ViewBag.IsAdmin to UserRole.Admin to discriminated unions with exhaustiveness checking, I don't see a change in what we're doing.



I see the same pattern evolving across eras:



· State management

· Permission handling

· Conditional rendering

· Feature flagging

· Error boundary modeling



The goal was always clear communication between backend and frontend. What changed is how much help we get from our tools and what we can prove about our code.



ViewBag was a post-it note on a dynamic object.

Numeric enums were named constants with a hole.

String enums are opaque domain values — the first real safety.

Discriminated unions are provable state machines.



We're still writing the same story. We just have a much better editor — and now, a compiler that can prove our story has no plot holes.






Thanks for reading. If you've made a similar journey from dynamic server-rendered UIs to type-safe frontend architectures, I'd love to hear about the moment you realized the compiler was now on your team.

Vollständiger Original-Bericht
Ausführliche Details, Code-Beispiele & Hersteller-Stellungnahme auf dev.to.
↗ Original-Artikel auf dev.to lesen
Wie bewertest du diesen Beitrag?
1 Klick Feedback
Teilen mit Netzwerk & Team:

Community-Analysen & Experten-Meinungen 0

Verfasse deine eigene Analyse, teile Workarounds oder diskutiere diesen Vorfall im Blog.
Noch keine Community-Analyse verfasst. Markiere einen Textabschnitt oder klicke oben auf Eigene Analyse verfassen“!
Community Pulse: Relevanz-Einschätzung
1 Klick Experten-Votum
🔴 Akute Relevanz 0%
🟡 In Evaluierung 0%
🟢 Keine Auswirkung 0%
Spannende Innovation 0%
Verwandte Story-Cluster & Quellen (Vektor-KI)
Port 8095 Engine
2 Quellen
Seattle Times sues Microsoft and OpenAI, alleging they trained their AI on its journalism
1 Quelle
Today’s NYT Mini Crossword Answers for Saturay, Sept. 12
1 Quelle
Etzioni on AI: What kids tell chatbots, but not you
Ähnliche Beiträge
🔍 Verwandte News

Auch interessante Nachrichten We Were Always Building State Machines — From .NET ViewBag to TypeScript Discriminated Unions

Thematisch verwandte Begriffe: Were, Always, Building, State · 6 Treffer

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...

Laden...

Beiträge werden geladen ...

Laden...

Videos werden geladen ...