TypeScript in the era of AI… or why your types matter more than your skills

Skill files and comments are suggestions an AI agent can skip. A TypeScript conditional type turns a misuse into a compiler error that also explains the business reason.

So everyone is talking about skills now. Skills for the agent, skills files, prompt this, prompt that. And I get it, it’s nice. But I keep coming back to the same thought… the AI doesn’t really care what you told it in some markdown file if the code itself lets it do the wrong thing. What actually stops it is the compiler yelling at it.

So my take is pretty simple. Writing the code properly beats writing more instructions. Especially in TypeScript.

first, what the hell is an LSP

I know a lot of people don’t know this so quick detour. LSP is the Language Server Protocol. It’s the thing running behind your editor that tells you “hey this variable is unused” or “this type doesn’t match” while you type. The red squiggly lines, that’s the LSP sending you diagnostics.

For us humans it’s super helpful. For AI it’s extra super helpful. Because the AI makes mistakes all the time… it forgets an import, it leaves an unused variable, it passes the wrong thing to the wrong function. And the harnesses out there (Claude Code, Open Code and editors and such) are listening to the LSP. So the moment it writes something wrong it gets the diagnostic back and it has to deal with it. It doesn’t need to remember anything. The error just shows up.

Which means… the stronger the type system, the more errors you can surface, the better the outcome. That’s basically the whole article, the rest is how.

the example, just a union

A job can be active, paused or archived (a plain union, not how I’d ship it to production, it’s just here to explain the idea):

type JobStatus = 'active' | 'paused' | 'archived'

That’s a union. A JobStatus can be this, or that, or that. Anything else and the compiler says no. Write ‘done’ and you get an error, ‘done’ is not one of them. That’s already something… the AI can’t just invent a status.

But it only tells you that you’re wrong. Not why.

Now say archived turned deprecated. It still exists in the system, old jobs have it, you can still read it. You just shouldn’t move a job into it anymore. The normal thing is to slap a deprecated comment on top of the type. Fine. But… the AI might never look at the type. It goes straight to the function, calls it, moves on. The comment is just sitting there and nobody read it.

What you can do instead:

type Usable<S extends JobStatus> = S extends 'archived'
  ? { readonly __error: 'archived is deprecated. Billing no longer reconciles archived jobs, use paused and let the cleanup worker close it.' }
  : S

function updateJobStatus<S extends JobStatus>(id: string, status: Usable<S>) {
  // ...
}

updateJobStatus('42', 'paused')   // fine
updateJobStatus('42', 'archived') // error, and the error says why

If you don’t live in the type system, Usable is a conditional type. Basically an if, but for types. If the status is archived, swap its type for an object that holds the reason. Otherwise give the status back as it is. ‘archived’ is a string and the parameter now wants an object, so it can never match. It fails… and the thing it was supposed to be IS the explanation.

This is what you get, straight out of my VS Code (or tsc, same thing). And that’s the point… you don’t wait for a build to find out. The red line shows up the second you type it. Fail fast. Same for the agent, it gets it the moment it edits the file:

error TS2345: Argument of type 'string' is not assignable to parameter of type '{ readonly __error: "archived is deprecated. Billing no longer reconciles archived jobs, use paused and let the cleanup worker close it."; }'.

Read the end of it. That’s the business reason, sitting right there in the error. The agent reads that and goes ok, use paused. No skill file, no comment, nobody had to remember anything. And the LSP surfaces it, so the agent sees it whether it went looking or not.

And yes… these types are code now, so they need their own tests. If someone breaks Usable tomorrow nothing turns red, unless you have a test that expects the error:

// @ts-expect-error a deprecated status should fail
updateJobStatus('42', 'archived')

// @ts-expect-error a made up status should fail
updateJobStatus('42', 'done')

If any of those lines stops erroring, the ts-expect-error itself becomes the error. So the test fails exactly when the types stop protecting you.

This is what I mean by a system that explains from the domain perspective why the change is the change. The reasoning lives in the types. Not in a skill file, not in a comment someone may or may not read.

Why TypeScript specifically… it lets the compiler lie to itself

This is the part I think people miss. TypeScript is kind of special. It’s the only language I know where you can make the compiler lie to itself like this, on purpose, and it’s normal.

Try that update status thing somewhere else. In C, forget it. In Rust you can put a deprecated attribute with a note on an enum variant, but that’s deprecated everywhere, not “allowed here, forbidden in this one function, and here is the reason”. To get that you need to split the enum or go down some trait rabbit hole. In TypeScript it’s a conditional type on a parameter. That’s it.

And that cuts both ways.

Good thing, you have the freedom to do whatever you want. You can model the domain almost exactly how you think about it.

Bad thing… the types are now code. Code that can be wrong. Once the types get insanely complex you need another set of checks and tests just to confirm the types behave how you expect. Yes, tests for your types. But don’t tie them to some special type testing library. Use whatever you already have, node test, bun test, anything. The lines that should work are just normal code in the test and pass if they compile. The lines that should fail get a ts-expect-error. Run the type check as part of the test run (tsc --noEmit before the tests, for example) and if the types break, the test fails. That’s it. Otherwise you built a very confident liar.

so, skills vs code

I’m not saying skills are useless. I’m saying they are a suggestion and the compiler is a wall. The AI ignores suggestions all the time… it doesn’t get past the wall without seeing the error, and if the error carries the reason, then it actually learns something in the moment.

Write the types so that the mistake is loud, and so that the noise says why. That’s the whole thing really.

Have a question or comment about this article? Get in touch.