by tauoverpi | 2025-05-08 21:50:45+00:00
The type system doesn't replace unit/snapshot/property/simulation tests as it's only job is specification. The type system is meant to be used in addition to testing to reduce the set of possible inputs to a smaller domain such that it's easier to reason about what is possible and what isn't. The same would be true even if you go as far as formal verification of programs, you always need to test even when you have powerful static types!

For example `foo :: Semigroup a, Traversable t => t a -> a` I already know that whatever is passed to this function can be traversed and have it's sum computed. It's impossible to pass something which doesn't satisfy both of those constraints as this is a specification given at the type level which is checked at compile-time. The things that cannot be captured as part of a type (bounded by the effort of specification) are then left to be captured by tests which only need to handle the subset of things which the above doesn't capture (e.g `Semigroup` specifies that you can compute the sum but it doesn't prevent `n + n = n` from being the implementation of `+`, that must be captured by property tests).

Another example, suppose you're working with time:

tick :: Monad m => m Clock.Present zero :: Clock.Duration seconds :: Uint -> Clock.Duration minutes :: Uint -> Clock.Duration hours :: Uint -> Clock.Duration add :: Clock.Duration -> Clock.Present -> Clock.Future sub :: Clock.Duration -> Clock.Present -> Clock.Past is :: Clock.Duration -> Clock.Duration -> Bool until :: Clock.Future -> Clock.Present -> Clock.Duration since :: Clock.Past -> Clock.Present -> Clock.Duration timestamp :: Clock.Present -> Clock.Past compare :: Clock.Present -> Clock.Foreign.Present -> Order data Order = Ahead Clock.Duration | Equal | Behind Clock.Duration

From the above you can tell what each function should do without looking at the implementation and you can probably write tests for each. Here the interface guides you to handle time in a safer way and tells a story `event = add (hours 5) present` where you cannot mix the wrong type of data ``until event `is` zero``. This is actual code that I've used in a production environment as it saves the team from shooting themselves in the foot with passing a `Clock.Duration` where a `Clock.Present` or `Clock.Future` should have been. Without a static type system you'd likely end up with a mistake mixing those integers up and not having enough test coverage to capture it as the space you must test is much larger than when you've constrained it to a smaller set within the bounds of the backing integer of the above.

In short, types are specifications, programs are proofs that the specification has a possible implementation, and tests ensure it behaves correctly for that the specification cannot constrain (or it would be too much effort to constrain it with types).

As for SQL, I'd rather say the issue is that the SQL schema is not encoded within your type system and thus when you perform a query the compiler cannot help you with inferring the type from the query. It's possible (in zig [1] at least) to derive the type of a prepared SQL query at compile-time so you write SQL as normal and zig checks that all types line up. It's not that types cannot do this, your tool just isn't expressive enough. F# [2] is capable of this through type providers where the database schema is imported making the type system aware of your SQL table layouts solving the "redundant specification" problem completely./

So with all of that, I assume (and do correct me if I'm wrong) that your view on what types can do is heavily influenced by typescript itself and you've yet to explore more expressive type systems (if so I do recommend trying Elm to see how you can work in an environment where `any` doesn't even exist). What you describe of types is not the way I experience them and it feels as if you're trying to fight against a tool that's there to help you.

[1]: https://rischmann.fr/blog/how-i-built-zig-sqlite [2]: https://github.com/fsprojects/SQLProvider