C.W.K.
Stream
Lesson 02 of 05 · published

Type-Shaped Thought

~10 min · ts-epilogue, philosophy, thought-shape

Level 0Curious
0 XP0/93 lessons0/23 achievements
0/100 XP to next level100 XP to go0% complete
"After enough TypeScript, you start designing in types before you write the code. That's not a bug; it's the upgrade."

The shift

Most programmers come to TypeScript from JavaScript and treat types as a layer of bookkeeping on top of the 'real' code. Annotations are a tax; inference is a relief. The mindset is 'I'm writing JavaScript that happens to type-check.'

After enough TypeScript, the mindset flips. Types become the design surface. You sketch the shape of the data and the relationships between functions in types first — then the implementation falls into place because the contracts are already drawn.

What that looks like in practice

Designing a new feature, you ask:

  • What's the shape of the data this works on? (A type.)
  • What are the legal states this thing can be in? (A discriminated union.)
  • What's the public API surface? (A set of function signatures.)
  • What invariants must hold? (Where do narrowing checks need to live?)

The answers are types. Once they're typed, the implementation is mostly mechanical. The hard thinking is done at the type level.

OOP universe and type-shaped thought

Dad's 'OOP as universe operating principle' applies here. The principles you'd use to model the world with classes — inheritance, polymorphism, encapsulation, abstraction — appear in types: extension via intersection, polymorphism via generics, encapsulation via private/#private, abstraction via interfaces. TypeScript isn't competing with OOP; it's encoding the same principles into the type layer.

Type-shaped thought is the upgrade. JavaScript-with-types is the entry point; TypeScript-as-design-language is the destination. Most people who've used TypeScript for a year are somewhere in between. Both are valid; the destination is the deeper one.

Code

Type-first design·typescript
// Imperative-first thinking — implementation drives the type.
function loadConversation(id: string): unknown {
  // ... write code, figure out types as you go
  return result;
}

// Type-first thinking — design drives the implementation.
type ConversationLoadResult =
  | { status: 'success'; conversation: Conversation }
  | { status: 'not-found' }
  | { status: 'error'; error: Error };

async function loadConversation(id: string): Promise<ConversationLoadResult> {
  // The implementation has to produce one of three shapes — laid out before the body.
  try {
    const c = await fetchConversation(id);
    return c
      ? { status: 'success', conversation: c }
      : { status: 'not-found' };
  } catch (error) {
    if (error instanceof Error) return { status: 'error', error };
    return { status: 'error', error: new Error('unknown') };
  }
}

// The caller can switch on the discriminator and handle each case.
function render(r: ConversationLoadResult) {
  switch (r.status) {
    case 'success': return r.conversation.title;
    case 'not-found': return 'Not found';
    case 'error': return r.error.message;
  }
}

External links

Exercise

Pick a small new feature (a parser, a state machine, a small CRUD operation). Spend 20 minutes designing the types — no implementation, just types. Then implement. Compare with the time you'd have spent the other way around.
Hint
For most well-bounded features, type-first cuts total time roughly in half because the implementation has fewer dead ends. The types catch design issues before you've written code that depends on them.

Progress

Progress is local-only — sign in to sync across devices.
Spotted a bug or have feedback on this page?Report an Issue

Comments 0

🔔 Reply notifications (sign in)
Sign inPlease sign in to comment.

No comments yet — be the first.