Josh Pitzalis

Compile-Time Clarity

Read this first

Multi-Turn conversations

Before we build anything of consequence, you must internalise an inconvenient truth: the Claude API remembers nothing.

No hidden transcript accumulates on Anthropic’s servers. If you want a conversation with memory, memory is your job.

Conversing with a Goldfish

In the previous lesson, we asked Claude: What is quantum computing?

Now follow up and ask it to “Write another sentence.”

Claude dutifully writes another sentence about migratory birds, perhaps, or the history of cheese. It has no idea what “another” refers to. From its perspective, this is the first thing you have ever said to it.

No quantum computing.

No context.

Just three words.

How Memory Works

  1. First, you must maintain the full list of messages yourself, in an ordinary array.
  2. Second, send that entire history along with every single request.

You send your initial user message. Then you take Claude’s reply and...

Continue reading →


Making a request

Behold. An HTTP endpoint that will answer any question you put to it.

You know how to talk to an HTTP endpoint. You have been doing it since you were a stripling. So let us do it the honest way, with fetch, and see how far honesty gets us.

const res = await fetch("https://api.anthropic.com/v1/messages", {
  method: "POST",
  headers: {
    "x-api-key": process.env.ANTHROPIC_API_KEY!,
    "anthropic-version": "2023-06-01",
    "content-type": "application/json",
  },
  body: JSON.stringify({
    model: "claude-sonnet-5",
    max_tokens: 1000,
    messages: [{ role: "user", content: "What is quantum computing?" }],
  }),
});

const data = await res.json();

This works.

Instantly immanentised synthetic wisdom.

You may now feel a small flush of pride.

There’s a link to a repo with runnable code at the end of this post, if you want to get this running before you proceed.

Please hover...

Continue reading →


Confessions of Impurity

A function is mathematically “pure” when the same input results in the same output every time.

x => x * 2 lives a quiet, honest life.

Purity is predictable, easy to reason about and sterile.

When a program touches nothing outside itself, if it never ventures into the real world, it is often useless.

And if adventure is what we seek, then there are a few flavours for our humble function to choose from.

First there is the fog-bound port of Nondeterminism: Things like Math.random(), Date.now(), reading an env var that might change.

CallingDate.now() is the same as asking me what time it is. Ask me three times, and you’ll get a different answer every time. The question was the same, the people talking were the same, but something changed.

“What time is it?” is not a pure question. There is an invisible, confounding factor afoot. This hidden contamination is called a side-effect...

Continue reading →


Why understanding TypeScript matters when using AI

You ask AI for an Order type from your API response. It gives you:

type Order = {
  id: string
  amount: number
  status: string
  discount?: number
}

It compiles. Everything looks good.

The real system only sends four values: 'pending' | 'paid' | 'cancelled' | 'refunded'.

Six weeks in, someone writes if (order.status === 'canceled') with one L.

The code compiles, but the comparison never matches. As a result, the system no longer refunds cancelled orders. You only find out because a customer tells you about the problem, not because the compiler caught it.

Also, is the amount in cents or in rupees?

The type signature does not specify. Nothing stops you from adding a value in cents to a value in rupees. A branded type would make this mistake a compile error: type Cents = number & { readonly brand: unique symbol }.

Then there’s discount?: number.

The question mark means that...

Continue reading →


Where the machine ends

Where does one state machine stop and the next begin? What goes in, what stays out, how do machines cooperate, and how do their diagrams stay honest?

Machines model behaviours, not apps

The thing being modelled is not “the application”; it’s not even “the component”; it’s a behaviour. One behaviour with a single lifecycle. A story with a beginning, middle, and a discrete set of possible moves: the draft save flow, the checkout wizard, the connection to the sync server.

Your state machine is not trying to model the whole app; that was never the goal. An application is a collection of running machines.

Imagine explaining the feature to product at a whiteboard. What you would draw is the experience a person goes through when they use a feature (the possible states, how they connect, the “only if” conditions); that is the machine. You would never draw a button’s hover colour, the exact...

Continue reading →


The Fewest Number of Concepts You Need to Use Effect

There are 5 concepts you need to understand to start using Effect.

1. Effect.tryPromise

Think of Effect like a souped-up Promise that’s honest about errors.

// The Promise Version of getUser
try {
  const user = await getUser("123")
  console.log(user.name)
} catch (error) {
  // Is this a network error? 404? 500? Who knows!
  console.error("Something went wrong:", error)
}

Versus…

// The Effect Version

import { Effect } from "effect"

Effect.tryPromise({
  try: () => getUser(id),
  catch: () => "DatabaseError" as const,
});

Since we saved DatabaseError as a TypeScript literal, hovering over this Effect will show us the error at the type level: Effect<User, "DatabaseError", never>.

Every Effect carries three types. The first is the success value (the user we wanted), the second covers any known errors associated with this “promise” (in this case, a database error), and the...

Continue reading →


Trusting your LLM-as-a-Judge

The problem with using LLM Judges is that it’s hard to trust them. If an LLM judge rates your output as “clear”, how do you know what it means by clear? How clear is clear for an LLM? What kinds of things does it let slide? or how reliable is it over time?

In this post, I’m going to show you how to align your LLM Judges so that you trust them to some measurable degree of confidence. I’m going to do this with as little setup and tooling as possible, and I’m writing it in Typescript, because there aren’t enough posts about this for non-Python developers.

Step 0 — Setting up your project

Let’s create a simple command-line customer support bot. You ask it a question, and it uses some context to respond with a helpful reply.

mkdir SupportBot
cd SupportBot
pnpm init

Install the necessary dependencies (we’re going to the ai-sdk and evalite for testing).

pnpm add ai @ai-sdk/openai dotenv
...

Continue reading →


Setting Up Your First Eval with Typescript

One big barrier to testing prompts systematically is that writing evaluations usually requires a ton of setup and maintenance. Also, as a TypeScript engineer, there aren’t that many practical guides on the topic, as most of the literature out there is for Python developers.

I want to show you how write your first AI evaluation framework with as little setup as possible.

What you will need

  • LLM API key with a little credit on it (I use Gemini for this walkthrough).

Step 0 — Set up your project

Let’s start with the most basic AI feature. A simple text completion feature that runs on the command line.

mkdir Summarizer
cd Summarizer
pnpm init

Install the AI SDK package, ai, along with other necessary dependencies.

pnpm i ai dotenv @types/node tsx typescript

Once you have the API key, create a .env file and save your API key:

GOOGLE_GENERATIVE_AI_API_KEY=your_api_key

Create an...

Continue reading →


Fuzzy Best Practices

Getting back into development after years, I started writing a little Express server for a new project. I realised I don’t have an implicit checklist of best practices in my head anymore.

I know I need to handle errors on my endpoints and functions, especially the async ones. I’ve forgotten what errors I need to defend against. The specifics are all fuzzy. It feels vague and overwhelming.

What I need is an explicit checklist. Like a list of 16 things I must check before publishing a commit.

Maybe it’s not 16 things; it could be 36. the point is once I have a checklist, it will be easier to add, adjust, or change things as needed. Now I’m just guessing and I can already see the mess I’m going to get myself into.

View →


typescript

Variables

let apples = 5;
let speed: string = 'fast';
let hasName: boolean = true;
let nothingMuch: null = null;
let nothing: undefined = undefined;

Built in objects

let now: Date = new Date();

Arrays

let colors: string[] = ['red', 'green', 'blue'];
let myNumbers: number[] = [1, 2, 3];
let truths: boolean[] = [true, true, false];

Classes

class Car {}
let car: Car = new Car();

Object literals

let point: { x: number; y: number } = {
  x: 10,
  y: 20,
};

Functions

const logNumber: (i: number) => void = (i: number) => {
  console.log(i);
};

//or

const logNumber =  (i: number): void  => {
  console.log(i);
};

View →