by tauoverpi | 2025-05-13 03:12:23+00:00
> You need tests either way.

The argument isn't that with static types you don't need tests but rather with static types you can focus on testing what actually matters which is the fuction itself along with it's integration into the program. Tests won't cover 100% of the surface area of a function taking `any` as that is by definition infinite with the constraint on the shape specified in the body of the function dependent on the path taken at runtime (it might never be hit). I urge you to take a look at languages other than TS for this as TS is in the strange place of bolting on a static type system to rather messy language as what may be issues with TS may not be for the rest of the space of languages with static type checking (e.g haskell effectively requires no type annotations at all in theory but in practice it's often better to use them and with extensions it's required).

To add to that, try looking at Go, Elm, Zig, C#, mercury, and Idris to see different type systems at play rather than assuming TS and Rust are all that's available in this design space.

> Simply running the problematic code will most likely throw an exception.

The issue here is "most likely" which during a large refactor (and due to a wide spread use of duck typing) can hide many faults within the application that go without being noticed until a user hits that path. Static types turn this into a compile-error thus it won't ever reach production. If you have 100% path coverage and test every possible type that can be passed and ensure truthy values don't change behaviour (e.g `if ("")`, `if ([])`, ...) when passed (need to test the space where all of them evaluate to `false` in addition to `true`) you're still short of the number of variations that can be passed to your function and thus may end up in a situation in future where the wrong behaviour causes failure at runtime post deployment. This is not to say you should use types only and not tests (still test even with static types) but rather that the domain of possible things passed is reduced to a testable set of values instead of always being the largest path-dependent set possible.

> It's hard to write a test that checks behavior but misses a wrong-type.

It's incredibly easy to write such tests as most don't test the negative space of such functions nor is it easy to know what the minimal shape of an object may be for your function as it depends on which logic it hits inside. If a truthy value is used or a branch is made on a field or method call of the object then anything unused in the consequent branch is not checked by your test. You'd effectively have to resort to fuzzing to find what the minimal shape is for each possible path to have an idea of what your function doesn't catch in tests to get even close to static type checking (which is still far off as in some cases the set is so large it will never finish / enumerate all possible shapes).

> SQL type autogen is limited to full rows, so any query returning an aggregate or something isn't going to work with that.

Ask _why_ tooling doesn't work with that and you may notice it's due to a lack of specification or just insufficient tooling. Take F# [1] as the example here which can do far more [2] and doesn't suffer from the mentioned problem. F# is a language with static type checking.

[1]: https://learn.microsoft.com/en-us/dotnet/fsharp/tutorials/ty...

[2]: https://fsprojects.github.io/SQLProvider/