#2692: Type Safety: Static vs Dynamic, Soundness & More

Static vs dynamic, strong vs weak, and the truth about TypeScript's unsoundness. A deep dive into type theory.

Featuring
Listen
0:00
0:00
Episode Details
Episode ID
MWP-2853
Published
Duration
27:37
Audio
Direct link
Pipeline
V5
TTS Engine
chatterbox-regular
Script Writing Agent
deepseek-v4-pro

AI-Generated Content: This podcast is created using AI personas. Please verify any important information independently.

This episode dissects the concept of "type safety," moving beyond the buzzword to explore the precise distinctions that define a language's type system. The conversation begins with the most fundamental axis: static versus dynamic typing. The key differentiator is not whether a language has types, but when type checking occurs—at compile time (static) or at runtime (dynamic). Python, for example, has types for every value; it simply checks them during execution, throwing a TypeError when an operation is invalid. This trade-off means static typing catches a class of bugs before code runs, while dynamic typing offers more flexibility and faster iteration.

The discussion then addresses the sloppy terminology of "strong" versus "weak" typing. While not a formal term in type theory, "strong typing" generally describes a language that avoids implicit, surprising type coercions, like Python's refusal to add a string to an integer. In contrast, JavaScript’s weak typing allows such operations, silently converting types. The episode also clarifies "soundness," a formal guarantee that a successfully type-checked program will never produce a runtime type error. TypeScript is deliberately unsound, a pragmatic design choice to handle the dynamic nature of JavaScript without requiring massive rewrites, proving that catching 90% of errors can be more valuable than a perfect, unusable system.

Further concepts covered include gradual typing, which allows for incremental addition of type annotations to dynamically typed code, and the distinction between structural typing (TypeScript, where types are identified by their shape) and nominal typing (Java, where types are identified by their explicit name). Finally, the episode touches on type inference, where the compiler deduces types automatically, and hints at how more advanced systems like Rust's borrow checker and dependent types extend the concept of safety even further.

Downloads

Episode Audio

Download the full episode as an MP3 file

Download MP3
Transcript (TXT)

Plain text transcript file

Transcript (PDF)

Formatted PDF with styling

#2692: Type Safety: Static vs Dynamic, Soundness & More

Corn
Daniel sent us this one and it's a deceptively simple question. What does type safety actually mean? Because you hear it everywhere. TypeScript is type safe, Rust is memory and type safe, Python with mypy is gradually typed. But the phrase hides a whole taxonomy of distinctions that most people gloss over. He wants us to walk through what type safety really is at the language theory level. Static versus dynamic typing, strong versus weak, what soundness means and why TypeScript famously isn't sound, gradual typing, structural versus nominal, type inference, what a type system actually buys you versus what it doesn't, and then touch on Rust's borrow checker and dependent types before landing on a practical take. This is a Herman episode if I've ever seen one.
Herman
Oh, I've been waiting for this one. And by the way, quick note. DeepSeek V four Pro is writing our script today. So if anything comes out particularly elegant, that's why.
Corn
Alright, walking encyclopedia. Where do we start?
Herman
Let's start with the most fundamental distinction that people get wrong constantly. Static typing versus dynamic typing. In a statically typed language, type checking happens at compile time, before the program ever runs. In a dynamically typed language, type checking happens at runtime, while the program is executing. That's it. That's the whole distinction. It's not about whether you write type annotations. It's not about whether the language has types at all. It's strictly about when the checking occurs.
Corn
When someone says Python doesn't have types, that's wrong.
Herman
Python absolutely has types. Every value in Python has a type. The integer five has type int. The string hello has type str. Python just checks those types at runtime. If you try to call a string method on an integer, Python will happily start executing your program and then throw a TypeError when it hits that line. A statically typed language like Java or Rust would refuse to compile that program at all.
Corn
Which is why the phrase untyped language drives me up the wall. There's no such thing as an untyped language, really. Even assembly has types in the sense that you're operating on words of specific widths.
Herman
And the practical difference matters enormously. Static typing catches an entire class of bugs before you ever run the code. Dynamic typing gives you more flexibility and faster iteration because you don't have to satisfy the compiler before you can see results. That trade-off is real. It's not that one is better. It's that they optimize for different things.
Corn
Then what about strong versus weak typing? Because that seems like where things get fuzzy.
Herman
This is where the terminology gets genuinely sloppy. Strong typing is not a formal term in type theory. It's a colloquial description that roughly means the language doesn't implicitly convert between unrelated types in ways that lose information or produce surprising results. Python is strongly typed. You cannot add the string two to the integer three in Python. It throws a TypeError. JavaScript is weakly typed. You absolutely can add the string two to the integer three and JavaScript will silently convert the integer to a string and give you the string twenty-three.
Corn
Which has launched a thousand memes.
Herman
A thousand bugs. But here's the nuance. Even strong typing is a spectrum. C is often called weakly typed because you can cast pointers to arbitrary types and treat memory as whatever you want. But C also has a type system that the compiler enforces. The difference is C lets you opt out of it explicitly. So when people say weak typing, they might mean implicit coercions like JavaScript, or they might mean an escape hatch like C's pointer casts, or they might mean no runtime type enforcement at all. The term is fuzzy because it's not a single axis.
Corn
Which is why I think the static dynamic axis is more useful for actual conversations. At least that one has a clear definition. When the type check happens.
Herman
And that brings us to the next concept Daniel flagged. This is where things get really interesting, especially with TypeScript.
Corn
Alright, define soundness for me.
Herman
In type theory, a type system is sound if every program that passes type checking will never produce a type error at runtime. Formally, if the type checker says a value has type T, then at runtime that value is of type T. Soundness is a mathematical guarantee. Languages like Standard ML and the core of Haskell are designed to be sound. If it compiles, the types are correct at runtime.
Corn
TypeScript famously is not sound.
Herman
TypeScript is explicitly, deliberately unsound. And this is not a bug. It's a design decision that the TypeScript team has documented clearly. There's a section in the TypeScript handbook about it. The reason is pragmatic. TypeScript's goal is to type existing JavaScript code without requiring massive rewrites. JavaScript is wildly dynamic. Objects get mutated in strange ways. Properties appear and disappear. Functions return different types depending on their inputs in ways that are impossible to fully encode. A sound type system that could handle all valid JavaScript patterns would be so restrictive that nobody would use it.
Corn
They trade soundness for usability.
Herman
The classic example is array indexing. In TypeScript, if you access an array element by index, the type checker assumes it returns the element type. But at runtime, if the index is out of bounds, you get undefined. A sound system would force you to handle that possibility. TypeScript just trusts you. Similarly, type assertions with the as keyword are an explicit escape hatch. You can tell TypeScript this value is a string even when the compiler can't verify it, and TypeScript says okay, I believe you. That's fundamentally unsound. A value might not be a string at runtime, but the type checker has been told to assume it is.
Corn
Yet TypeScript is enormously successful. Which suggests soundness might be overrated for practical software engineering.
Herman
It suggests that catching ninety percent of type errors is enormously valuable even if you don't catch a hundred percent. TypeScript's unsoundness is a pragmatic trade-off. The type system catches real bugs. The holes are known, documented, and usually occur in places where developers are explicitly opting out of safety. The TypeScript team's philosophy is basically better to have an unsound type system that people actually use than a sound one that nobody adopts.
Corn
Which is a very different philosophy from Rust, where soundness is treated as non-negotiable.
Herman
Right, and we'll get to Rust. But first, gradual typing. Daniel mentioned Python with mypy as an example.
Corn
Gradual typing is the idea that you can add types to a dynamically typed language incrementally. You don't have to type everything at once.
Herman
Gradual typing lets you start with an untyped codebase and add type annotations file by file, function by function, even variable by variable. The type checker treats unannotated code as having the dynamic type, meaning essentially trust whatever happens at runtime. Then as you add annotations, it checks those parts statically. The boundary between typed and untyped code is where things get tricky. The type checker has to insert runtime checks or just trust that the untyped code respects the types it claims to produce. Python's mypy and TypeScript both use gradual typing, though in different ways.
Corn
TypeScript's approach with any is essentially the dynamic type in a gradual system.
Herman
The any type in TypeScript is the escape hatch that makes gradual typing work. A value of type any can be assigned to anything, and anything can be assigned to it. It opts out of type checking entirely. And TypeScript also has unknown, which is the safe version. Unknown means I don't know what this is, and you have to narrow it before you can use it. That's a really nice design. Any says trust me, I know what I'm doing. Unknown says I don't know, be careful.
Corn
The tension with gradual typing is always where typed and untyped code meet. That boundary is where the guarantees get fuzzy.
Herman
If you have a typed function calling an untyped function, the typed code has to trust that the untyped function returns what it claims. If it doesn't, you get a runtime error despite the type annotations. The soundness of the typed portions depends on the correctness of the untyped portions. Gradual typing gives you incremental benefits but not incremental guarantees. You only get full guarantees when the entire codebase is typed and you've eliminated all the any types and dynamic boundaries.
Corn
Alright, structural versus nominal typing. This is one where I think most working developers have an intuition but maybe haven't heard the formal terms.
Herman
Nominal typing means types are identified by their names. Two types are the same if and only if they have the same name, regardless of their structure. Java, C sharp, Rust, and Swift are nominally typed. If you define a class Person with fields name and age, and separately define a class Employee with fields name and age, those are different types in a nominal system. You cannot pass an Employee where a Person is expected, even though they have identical structure, because the names are different.
Corn
Structural typing says if it walks like a duck and quacks like a duck, it's a duck.
Herman
That's the duck typing slogan, but structural typing is a bit more formal. In a structural type system, types are identified by their shape. Two types are compatible if they have the same structure, the same properties with the same types. TypeScript uses structural typing. If you define an interface Person with name and age, and separately define an interface Employee with name and age, TypeScript considers them compatible. You can use one where the other is expected because they have the same shape.
Corn
Which is very natural for JavaScript, where objects are just bags of properties and nobody thinks about nominal identity.
Herman
JavaScript doesn't have nominal types at runtime. An object is just an object. Structural typing maps naturally to that reality. In a nominal system like Java, the type hierarchy is part of the design. You explicitly declare that Employee extends Person. In a structural system, the relationship is inferred from the shape. The trade-off is that nominal typing can prevent accidental compatibility. Just because two types happen to have the same fields doesn't mean they represent the same concept. A type called Inches with a number field and a type called Centimeters with a number field are structurally identical but semantically different. A nominal system would catch you mixing them up. A structural system would let it slide.
Corn
Which is why some languages support newtype wrappers or opaque types to get nominal behavior when you need it.
Herman
The broader point is that type systems encode different philosophies about what identity means. Nominal typing says identity comes from explicit declaration. Structural typing says identity comes from behavior and shape. Neither is universally correct. They fit different paradigms.
Corn
Now type inference. This is one of those things that modern languages have made so good that people forget it's happening.
Herman
Type inference is the compiler figuring out the types of expressions without you having to write them explicitly. In languages with full type inference like Haskell or OCaml or the ML family, you can write entire programs with almost no type annotations and the compiler infers everything. In languages with partial inference like TypeScript or Rust or Swift, you write annotations in some places, typically function signatures, and the compiler infers the rest.
Corn
The Hindley Milner algorithm, right?
Herman
Hindley Milner is the classic type inference algorithm from the nineteen seventies, independently discovered by Roger Hindley and Robin Milner. It's the foundation for type inference in ML, Haskell, and many functional languages. The algorithm is elegant. It collects constraints from how values are used and then solves those constraints to determine the most general type for every expression. The key property is that it infers the principal type, the most general type possible. If the algorithm succeeds, the program is type safe.
Corn
TypeScript's type inference is impressive given what it's working with. JavaScript is not a language designed for type inference.
Herman
Not at all. And TypeScript's inference has gotten dramatically better over the years. It can infer types through complex callbacks, through generic function calls, through conditional types. The control flow analysis is particularly clever. If you have an if statement that checks whether a value is a string, TypeScript narrows the type inside that branch automatically. That's not just inference, that's type narrowing, and it's one of TypeScript's most powerful features.
Corn
I've said before that TypeScript's type narrowing through control flow checks is one of its best features, and I stand by that. It's the kind of thing that once you've used it, going back to a language without it feels primitive.
Herman
It's hard to go back. The compiler understands the runtime control flow of your program and adjusts types accordingly. That's sophisticated stuff.
Corn
Alright, so we've covered the taxonomy. Static versus dynamic, strong versus weak, soundness, gradual typing, structural versus nominal, inference. Let's get to the big question. What does a type system actually buy you?
Herman
Three main things. First, catching errors at compile time. This is the obvious one. The type checker prevents you from calling a function with the wrong arguments, accessing properties that don't exist, or performing operations on incompatible types. Every error caught at compile time is an error that doesn't happen in production. Second, enabling tooling. Type information is what powers autocomplete, go-to-definition, refactoring tools, and documentation generation. When your editor knows the type of every expression, it can offer precise completions and catch errors as you type. Third, encoding invariants. A well-designed type system lets you express constraints in the types themselves, so the compiler enforces them. If you have a function that should never receive an empty list, you can encode that in the type system with a non-empty list type. The compiler then rejects any call that might pass an empty list.
Corn
What doesn't it buy you?
Herman
It doesn't make your code correct. This is the big misconception. Type safety means the program doesn't have type errors. It says nothing about whether the program does the right thing. You can write a perfectly type safe program that computes the wrong answer. Type systems catch a specific class of errors. They don't catch logic bugs. If you write a sorting function that sorts in descending order instead of ascending, the type checker won't notice because the types are identical. A type system also doesn't replace testing. You still need to verify that your code behaves correctly on actual inputs. Types and tests are complementary, not substitutes.
Corn
There's a cost to types that people sometimes ignore. Type annotations are code you have to write and maintain. Complex generic types can be hard to read. Advanced type system features have a learning curve.
Herman
The overhead is real. Writing precise types for highly dynamic patterns can be difficult. There's a point of diminishing returns where the effort to satisfy the type checker exceeds the benefit. The art is knowing where that point is for your project.
Corn
Let's talk about Rust's borrow checker, because Daniel specifically flagged it as a type system that encodes ownership. That's a different beast entirely.
Herman
The Rust borrow checker is one of the most interesting developments in type systems in the past decade. It's not just checking that values have the right types. It's checking memory safety properties at compile time using the type system. Ownership, borrowing, and lifetimes are type-level concepts. When you write a Rust function that takes a reference, the type system tracks how long that reference is valid and ensures it doesn't outlive the data it points to. This eliminates entire categories of bugs. Use-after-free, double-free, data races. All caught at compile time.
Corn
The cost is the infamous Rust learning curve. Fighting the borrow checker is practically a rite of passage.
Herman
But what's remarkable is that Rust achieves memory safety without a garbage collector. That was the holy grail for systems programming for decades. C and C plus plus give you manual memory management with no safety guarantees. Java and Go give you safety with a garbage collector and the runtime overhead that entails. Rust gives you safety at compile time with no runtime overhead. The type system is doing work that traditionally required either runtime checks or programmer discipline.
Corn
The borrow checker is essentially a static analysis that's integrated into the type system rather than bolted on as a separate tool. That integration means it's mandatory and comprehensive.
Herman
And that's the key insight. By encoding ownership in the type system, Rust makes the analysis part of compilation. You can't opt out. You can't skip it. Every Rust program that compiles is guaranteed to be memory safe, modulo unsafe blocks. The unsafe keyword is Rust's escape hatch, and it's explicit and greppable. You know exactly where the guarantees are suspended.
Corn
Which brings us to dependent types. Daniel mentioned Idris. This is where type systems get properly mind-bending.
Herman
Dependent types are types that depend on values. In most type systems, types and values live in separate worlds. You have types like int and string, and values like five and hello. Dependent types blur that line. You can have a type like vector of length five, where the length is part of the type. The type checker can then verify at compile time that operations on vectors preserve length correctly. If you concatenate a vector of length three and a vector of length two, the result has type vector of length five. If you try to access the tenth element of a vector of length five, the compiler rejects it because it can prove the index is out of bounds.
Corn
You're encoding properties of the values themselves into the type system. Not just this is an integer, but this is an integer between zero and one hundred.
Herman
Dependent types let you encode arbitrary mathematical properties as types. You can prove theorems in the type system. Languages like Idris, Agda, and Coq are used for formal verification. You can write a specification as a type and then write a program that satisfies that type. If it compiles, the program is guaranteed to meet the specification. This is used in critical systems. CompCert is a verified C compiler written in Coq. It's mathematically proven to preserve the semantics of the source program. That's an extraordinary guarantee.
Corn
The cost is enormous. Writing dependently typed programs is essentially doing formal mathematics. The ergonomics are nowhere near what a working developer would tolerate.
Herman
That's the current state. Dependent types are primarily used in research and in safety critical systems where the cost of a bug is astronomical. But the ideas are filtering down. Rust's const generics let you do some limited dependent type things, like parameterizing types over constant values. TypeScript's template literal types let you do string manipulation at the type level. The line between types and values is getting blurrier across the board.
Corn
Alright, so we've covered what type safety is and what it buys you. Let's address the pushback. Why do dynamic language fans argue that more types don't equal better code?
Herman
There are a few arguments that I think are worth taking seriously. The first is expressiveness. Dynamically typed languages let you write patterns that are difficult or impossible to type statically. Metaprogramming, code generation, runtime reflection on types. When you're building something like a Ruby on Rails application, the framework is doing an enormous amount of dynamic work that would be very hard to express in a static type system. The flexibility is the point.
Corn
The second argument is iteration speed. When you're prototyping, you don't want to spend time satisfying the type checker. You want to see results.
Herman
And there's evidence for this. Studies have shown that in the early stages of a project, dynamic languages can be faster for exploration. The trade-off is that as the project grows, the lack of types becomes a liability. Refactoring without types is scary. You don't know what you're breaking. This is why large Python and Ruby codebases tend to accumulate test suites that essentially duplicate what a type checker does.
Corn
The third argument is that types give a false sense of security. If you think type safety means your program is correct, you might skip testing or be less careful about logic errors. That's a real human factors concern.
Herman
I've seen codebases where developers treat satisfying the type checker as the finish line and don't think about whether the code actually works. Types are necessary but not sufficient. They're one tool among many.
Corn
The fourth argument, and I think this is the one that actually has the most weight, is that the domain matters. If you're writing a simple script that glues together a few APIs and processes some data, types might be pure overhead. If you're writing a kernel or a database or a flight control system, types are essential. The question isn't whether types are good. It's when they pay off.
Herman
Which is exactly what Daniel asked us to land on. So let's give a practical take. When does type safety pay off? First, when the codebase is large and has multiple contributors. Types serve as documentation and enforcement. They prevent one person from breaking another person's assumptions. Second, when correctness matters. If a bug means lost money, lost data, or lost lives, you want every static guarantee you can get. Third, when the code has a long lifetime. Code that will be maintained for years benefits enormously from types because the original authors won't be around to explain the invariants.
Corn
When is it overhead? When the project is small and short-lived. A script that does one thing and will never be touched again doesn't need types. When you're exploring and don't know what the right abstraction is yet. Types can lock you into a shape prematurely. And when the domain is naturally dynamic. If you're building something that fundamentally depends on runtime reflection or metaprogramming, fighting the type system will cost more than it saves.
Herman
There's also a middle ground that I think is underappreciated. Gradual typing in Python with mypy or in JavaScript with TypeScript lets you add types where they help and skip them where they don't. You don't have to choose between fully typed and fully untyped. You can type the public API surfaces, the complex logic, the shared data structures, and leave the rest dynamic. That's a very pragmatic approach.
Corn
I think that's where we're headed. The debate between static and dynamic typing is settling into a practical synthesis. Use types for the parts that matter. Skip them for the parts that don't. The tools are good enough now that you can have it both ways.
Herman
The other trend I'm watching is type systems getting more expressive without getting more burdensome. Type inference keeps improving. Rust's borrow checker has gotten smarter over time. TypeScript's type narrowing is magical in places. The cost of types is going down while the benefits are going up. That shifts the calculus for a lot of projects.
Corn
One thing we didn't touch on is that type systems can also encode effects. Algebraic effects, checked exceptions, async versus sync. The type system can track not just what values are but what the code does. That's an emerging area.
Herman
Haskell's IO type is the classic example. A function that returns a string is pure. A function that returns IO string is impure and can do input output. The type system tracks side effects. That idea is spreading. Rust's Result type forces you to handle errors explicitly. The type system won't let you ignore a potential failure. These are type-level encodings of control flow and effects, and they catch real bugs.
Corn
To pull it all together for Daniel. Type safety isn't a binary. It's a spectrum of guarantees that different languages provide at different costs. Static versus dynamic tells you when checking happens. Soundness tells you whether the guarantees are absolute or pragmatic. Gradual typing lets you mix the two. Structural versus nominal tells you how types relate to each other. And what it buys you is error prevention, tooling, and encoded invariants. What it doesn't buy you is correctness or a free pass on testing.
Herman
The practical answer to when it pays off is it depends on scale, criticality, lifetime, and domain. There's no universal answer. But the trend line is clear. Types are getting better, easier, and more expressive. The cost benefit ratio is shifting. Even traditionally dynamic communities are adopting gradual typing. The future is typed, but probably not sound, not fully dependent, and not dogmatic about it either.
Corn
Now, Hilbert's daily fun fact.
Herman
Go for it, Corn.
Corn
Hilbert, what have you got for us today?

Hilbert: Horseshoe crab blood contains a copper-based molecule called hemocyanin that, when vibrated at exactly four hundred and forty hertz during the coagulation cascade, produces a faint blue fluorescence visible only in complete darkness. During the late Victorian period, a British naturalist named Reginald Thistlewaite attempted to build an acoustic telegraph system in Honduras using this principle, convinced he could transmit messages through vats of agitated horseshoe crab blood. The Honduran government politely declined further funding after the first demonstration produced nothing but a foul odor and a very confused crab.
Corn
I have so many questions and I'm not sure I want any of them answered.
Herman
A very confused crab. This has been My Weird Prompts. Thanks to our producer Hilbert Flumingtop. You can find every episode at myweirdprompts dot com. If you enjoyed this, leave us a review wherever you listen. We'll be back with another one soon.

This episode was generated with AI assistance. Hosts Herman and Corn are AI personalities.