mcpField
V
valid-state-types

valid-state-types

github.com/marius-townhouseDevelopment649 installsUpdated 39w ago
Repository

About valid-state-types

valid-state-types is a programming skill for application developers that guides type designs toward valid states and away from invalid field combinations. It emphasizes discriminated unions and grouping fields that must occur together.

Key features

  1. 01Detect invalid-state design smells
  2. 02Represent states with discriminated unions
  3. 03Group fields that must occur together
  4. 04Model a single source of truth

Use cases

  1. 01Design request loading and result states
  2. 02Refactor types with dependent boolean flags
  3. 03Model multi-stage states such as shopping carts

Install

Copy the command and run it in a terminal.

$npx skills add https://github.com/marius-townhouse/effective-typescript-skills --skill valid-state-types

After installing, type/valid-state-typesin Claude Code to confirm it loaded.

Details

From the project README

Prefer Types That Always Represent Valid States

Overview

Design types so that only valid states are representable.

If your types allow invalid combinations of values, you'll end up with code that's harder to write, harder to read, and more prone to bugs.

When to Use This Skill

  • Designing state types for applications
  • Creating types with related fields
  • Modeling states that have dependencies
  • Debugging impossible state errors
  • Refactoring confusing type definitions

The Iron Rule

NEVER design types that can represent invalid states.

No exceptions:

  • Not for "it's simpler this way"
  • Not for "we'll validate at runtime"
  • Not for "the extra types are too much work"

Detection: The "Invalid State" Smell

If your type allows combinations that should never happen, redesign it.

// ❌ VIOLATION: Allows invalid states
interface RequestState {
  status: 'loading' | 'success' | 'error';
  data?: string;
  error?: string;
}

// These are all valid according to the type, but nonsensical:
const bad1: RequestState = { status: 'success' };           // Where's the data?
const bad2: RequestState = { status: 'error' };             // Where's the error?
const bad3: RequestState = { status: 'loading', data: 'x' }; // Loading but has data?
const bad4: RequestState = { status: 'success', error: 'x' }; // Success with error?

The Solution: Discriminated Unions

// ✅ CORRECT: Only valid states are representable
interface RequestPending {
  status: 'pending';
}
interface RequestLoading {
  status: 'loading';
}
interface RequestSuccess {
  status: 'success';
  data: string;
}
interface RequestError {
  status: 'error';
  error: string;
}

type RequestState = RequestPending | RequestLoading | RequestSuccess | RequestError;

// Now invalid states are impossible:
const bad: RequestState = { status: 'success' };
//    ~~~ Property 'data' is missing in type '{ status: "success"; }'

Real-World Example: Page State

// ❌ BAD: Implicit relationships, invalid states possible
interface PageState {
  isLoading: boolean;
  error?: string;
  currentPage: string;
  data?: PageData;
}

// What does this mean?
const confusing: PageState = {
  isLoading: true,
  error: 'Network error',
  currentPage: '/home',
  data: someData,  // Loading but has data? Has error but also data?
};

// ✅ GOOD: Explicit states, no invalid combinations
interface PagePending { state: 'pending' }
interface PageLoading { state: 'loading'; currentPage: string }
interface PageLoaded { state: 'loaded'; currentPage: string; data: PageData }
interface PageError { state: 'error'; currentPage: string; error: string }

type PageState = PagePending | PageLoading | PageLoaded | PageError;

// Now the render function is clear:
function renderPage(state: PageState) {
  switch (state.state) {
    case 'pending':
      return renderPending();
    case 'loading':
      return renderSpinner(state.currentPage);
    case 'loaded':
      return renderData(state.data);
    case 'error':
      return renderError(state.error);
  }
}

Related Fields Should Travel Together

// ❌ BAD: Related fields can be independently undefined
interface Person {
  name: string;
  placeOfBirth?: string;  // These should either both
  dateOfBirth?: Date;     // be present or both absent
}

// This is valid but probably wrong:
const person: Person = { name: 'Alice', placeOfBirth: 'NYC' };  // No date?

// ✅ GOOD: Group related fields
interface Person {
  name: string;
  birth?: {
    place: string;
    date: Date;
  };
}

// Now they travel together:
function printBirth(person: Person) {
  if (person.birth) {
    // Both place AND date are guaranteed to exist
    console.log(`Born in ${person.birth.place} on ${person.birth.date}`);
  }
}

The Air France 447 Anti-Pattern

A tragic example of bad state design:

// ❌ DANGEROUS: Independent controls with conflicting states
interface CockpitControls {
  leftSideStick: number;   // Pilot's stick position
  rightSideStick: number;  // Copilot's stick position
}

// What if they conflict? The code has to decide somehow.
function getStickSetting(controls: CockpitControls): number {
  // Average them? Return left? Return right? No good answer!
  return (controls.leftSideStick + controls.rightSideStick) / 2;
}

// ✅ SAFE: Single source of truth
interface CockpitControls {
  stickAngle: number;  // One stick, one truth
}

Pressure Resistance Protocol

1. "More Types Is More Work"

Pressure: "Creating all these interfaces is tedious"

Response: Invalid states cause bugs. The types are the easy part.

Action: Invest the time upfront. You'll save debugging time later.

2. "We Validate At Runtime"

Pressure: "We check for invalid combinations in the code"

Response: Every consumer has to remember to validate. They won't.

Action: Make invalid states unrepresentable. Eliminate the need to validate.

3. "It's Just Internal State"

Pressure: "No external code uses this type"

Response: Internal code is still code. You'll still have bugs.

Action: Design good types everywhere.

Red Flags - STOP and Reconsider

  • Multiple boolean flags that have dependencies
  • Optional fields that should appear together
  • Status enum with optional data fields
  • Comments explaining "if X then Y must be set"
  • Validation code checking for impossible combinations
  • Switch statements with "should never happen" default cases

Common Rationalizations (All Invalid)

ExcuseReality
"It's simpler"It's simpler until you have bugs.
"We're careful"Carelessness happens. Types don't forget.
"We document it"Documentation gets stale. Types don't.
"Too many interfaces"Better than too many bugs.

Quick Reference

Bad PatternGood Pattern
Multiple related optional fieldsNested object that's optional
Status string + optional data/errorDiscriminated union
Boolean flags with dependenciesDiscriminated union
Multiple independent sources of truthSingle source of truth

Designing Valid State Types

  1. List all possible states your system can be in
  2. For each state, determine what data is required
  3. Create an interface for each state
  4. Use a discriminated union to combine them
  5. Verify: Can you construct any invalid states?
// Example: Shopping Cart
interface EmptyCart { state: 'empty' }
interface ActiveCart { state: 'active'; items: CartItem[] }
interface CheckoutCart { state: 'checkout'; items: CartItem[]; payment: PaymentInfo }
interface CompletedCart { state: 'completed'; orderId: string }

type ShoppingCart = EmptyCart | ActiveCart | CheckoutCart | CompletedCart;

The Bottom Line

If invalid states are representable, invalid states will occur.

Design types that can only represent valid states. Use discriminated unions. Group related fields. Your code will be easier to write, easier to understand, and harder to break.

Reference

Based on "Effective TypeScript" by Dan Vanderkam, Item 29: Prefer Types That Always Represent Valid States.

Comments

Markdown supported