Software engineers in the 21st century debate many things:
- Which coding editor should I use?
- Monolith or microservices?
- How should services talk to each other: REST, GraphQL, or gRPC?
But there's one long-running debate that coding agents have quietly resolved: whether to use a statically or dynamically typed programming language.
My background
I spent seven years building production systems, mostly in Python, at places like the New York Times and Squarespace. For the most part these codebases were dynamically typed and made little use of type annotations. I am now building Terse, the AI workflow platform for coding agents in Typescript.
The case for dynamic typing, pre-agent era
The argument for dynamic typing made a lot of sense to me:
- Static typing is repetitive, slow to compile and tedious.
- Dynamic languages are more expressive, which means less code and more developer productivity.
- I could write tests and many of the bugs that static typing checks would get caught anyway.
One of the most famous proponents of dynamic typing is @dhh. In an interview with @lexfridman, he offered this snippet as an example of everything wrong with static typing:
User user = new User()Capital-U User, I'm declaring the type of the variable. Lowercase user, I'm now naming my variable. Equals uppercase User, or new uppercase User. I've repeated "user" three times. - DHH
His broader point was that this kind of repetitiveness just to instantiate an object is frustrating. He goes on to mention that he likes to "chisel [programs] out of the screen with my bare hands". But what happens when this isn't the case anymore?
Why coding agents have caused a paradigm shift
Fewer and fewer developers are writing code by hand these days. As of early 2026, GitHub's committed code was 51% AI-generated or AI-assisted, according to figures circulating across the industry.
The industry shift has happened. Coding agents are now good enough to build production systems with human supervision. I feel like this has been the case since roughly when Opus 4.5 came out last November. AI-assisted coding is real, and in my own personal work, I believe it has made me more efficient.
Because I'm no longer writing each line by hand, my job has shifted from author to supervisor/reviewer. And the deep context I used to absorb just by typing everything myself is gone. When I am reviewing what an agent produced (or what a half dozen agents produce in parallel), types give me the context I need for the code I didn't write.
This flips DHH's argument. The boilerplate he hated was friction for the human author. But the human is no longer the author, and for the human reviewer, that same "boilerplate" provides extremely useful context.
Just a few observations to reinforce the point:
- Types are how coding agent teams scale just like human teams. @dhh concedes elsewhere that big teams and large codebases are where static typing starts to pay off. Multiple agents working on a codebase is exactly that situation (a large team), just compressed into one developer with a team of coding agents working in parallel.
- The expressiveness cost has basically evaporated. The criticism of static typing was all that boilerplate. But the LLM writes the boilerplate now, and far from slowing it down, those annotations give the model the same context they give a human reviewer and act as a form of documentation + feedback mechanism.
Why typing is so helpful
We're building Terse, the automation platform for coding agents. We're building a CLI, SDK, backend, and frontend with just two people. This is only possible because we make sure everything is typed and this acts as incredible guardrails. It just would not be possible to build this in a dynamically typed language.
Example 1 - Typing package keeps everything in sync
Every shared type in Terse lives in one package, terse-types. Every component (backend, frontend, CLI, SDK) imports this package into it's workspace. Every type is a Zod schema and the types themselves are inferred from the Zod schema. The inferred types gives us compile time checks and the Zod schemas parse data at the wire boundary between components.
import * as z from "zod";
// Example Zod schema
export const userProfileSchema = z.object({
id: z.string(),
email: z.string(),
displayName: z.string(),
firstName: z.string().nullable(),
lastName: z.string().nullable(),
displayPhotoUrl: z.string(),
});
// Inferred type
export type UserProfile = z.infer<typeof userProfileSchema>;Because all components depend on the same package, we can feel confident that the code written is verified at compile time and then the data that the code is acting on is revalidated by the Zod schema at runtime. It means no mapping layer or adapters.
Coding agents find this useful too. There is less for the agent to reason about in each part of the system and it doesn't have to write glue code to keep different components in sync. Also when the contract changes, the coding agent just needs to compile and it gets a list of every call site that needs updated, again saving on time and token cost.
Example 2 - Exhaustiveness checks
Exhaustiveness checking is a feature I have grown to love in TypeScript. Here's a simple example from our codebase: how we make sure every integration we support has an icon.
// Enum we track built-in integrations with
export enum IntegrationType {
GITHUB = "github",
LINEAR = "linear",
SLACK = "slack",
}
// Simple Example of code in frontend for displaying icons
export function IconForIntegration({
integration,
}: {
integration: IntegrationType;
}) {
switch (integration) {
case IntegrationType.GITHUB:
return <GithubIcon />;
case IntegrationType.LINEAR:
return <LinearIcon />;
case IntegrationType.SLACK:
return <SlackIcon />;
default:
throw integration satisfies never;
}
}For example, say we add a LinkedIn integration one day. We add LINKEDIN = "linkedin" to the enum, and without touching anything else, IconForIntegration stops compiling. That compile-time error tells the coding agent exactly what's left to do: add an icon for the new integration. Without it, we'd just have to pray the agent notices an icon is missing, and in my experience, agents don't always catch it.
This is the payoff of maintaining our types so carefully. The compiler hands the coding agent feedback good enough that we can often one-shot a new integration.
What should we do from here
As you look to scale coding agent use and get productivity benefits, it is imperative teams invest in static type checking. An alternative is investing in type annotations if your language supports it.
Two of the most popular dynamically typed programming languages, Python and Ruby, support type annotations. Those annotations can also be made to not just be suggestions, consider adopting mypy in Python or Sorbet in Ruby. More than ever, developers should prioritize adding enforced type annotations in their languages as a productivity multiplier for agents and a high leverage investment that keeps their coding agents productive.
The debate over static vs. dynamic typing is settled because what we argued over, a human typing code by hand, is quietly becoming the exception.


