I almost never face type related issues, and when they occur they are the easiest to catch.
The second I feel is by design. A runtime exception is better than undefined behavior, which was the other option when many of these more abstract languages started taking hold. We're just reaching a point where languages with undefined behavior are being competed against by projects like Rust which offer similar performance without the same risks. Runtime exceptions become less attractive when you can avoid them and avoid undefined behavior with more comprehensive solutions.
However, any decent IDE will work with type hinting (putting types in the comments, basically), and as long as you're fastidious about using them, you can realize those same benefits.
This is done using what is called "sum types" - we can tell the compiler that say a user can be "Premium" or "Regular". This distinction will affect policies across the codebase - eg. Premium customers get a discount on their shipping costs. We'll have robust unit tests and integration test that cover all these policies.
But even with the tests, we run into trouble when the feature request to create a new type of User comes in - "Semi-Premium" who gets only their _domestic_ shipping free. We now have to hunt and peck across the codebase, and change all our policies so that it handles this possibility well. Our tests are of no help here - they are meant to verify existing facts about the system, and can't tell when this user fall through certain policies because we forgot to handle it there. The type system on the other hand knows exactly the places where we decide things based on the kind of user. It will then realize that a new type of user has come in, and our policies don't handle them. This turns what usually is a high-risk and difficult task into an almost mechanical one.
What's happening here is that we're telling the compiler more things about our domain. This means the compiler can then remind us as we go about our business writing code, to handle every case, to never pass a null, and to help model our data structures that prevents inconsistent states from ever happening. The more we can encode in types, the more powerful this becomes.
A stellar case about Object design, not strong typing. I don't find type solves this issue, in fact, it even might be the opposite. Its way looser and more flexible to be able to say how things actually behave than to define the exact type they are to see how they behave.
The static analysis is definitely a huge plus, but it's a bit tangential: if a static analysis tool were able to point to the same issues, the reason would evaporate. (It's like saying a language is better because it has a better community, which is practical but not intrinsic).
> if a static analysis tool were able to point to the same issues, the reason would evaporate.
This static analysis tool is called the type checker. The experience of using a type checker is best when the underlying language has sound types. Sorbet, TypeScript, Typed Racket etc. bolt a type system on top of an existing language, and so don't have a choice but to be unsound, and so their qualitative experience isn't as nice as it could be. Even so, they're highly recommended for large codebases.
> It's like saying a language is better because it has a better community, which is practical but not intrinsic
Not at all. This is an intrinsic feature of programming languages and has a huge effect on how we think of programs.
It is however not a silver bullet and when building web applications, practicality - tooling, ecosystem etc. trumps everything else. However if you run into difficult problems which require deep attention to data modelling, and wish for a better tool to help frame the problem, then Typed Functional Programming might be a good try.
There isn't really a universal agreement on how to handle nil in the ruby community, and as long as that remains true, type issues will continue to be a daily reality of using the language.
So many of the production bugs I've encountered in my rails career (probably around 2/3rds) would immediately go away with a type system. Now, that's not to say that I won't encounter other issues, but type systems provide other benefits as well. Typescript annotations make JS seem pretty sane, and editor integrations like VSCode's support for TS is amazing. You get go-to-source functionality that isn't based on heuristics, compile-time error reporting as opposed to runtime error reporting.
Sure, it might not make sense in your situation. But every single job I've had in my career would've been much easier with a type system, and depending on how solid sorbet turns out to be, working in ruby might look quite a bit different in the next few years.
Particular example: every time you've accidentally pulled the wrong key out of a map (because that's how everyone does structs in Ruby and Python and Javascript) and gotten a null pointer exception: those were probably type related issues, and they're easily buried and discovered only in production, or through stupidly intensive unit testing, much (not all, but much) of which exists mostly to cover for (wait for it) lack of typing.
Slides here: https://github.com/dlthomas/using-c-types-talk/tree/master/s...
The example is a toy but the technique was developed for (and very successfully used in) a production setting.
Fast forward to when the same project grows and matures. Now it is huge and no one knows every subsystem. The original developers skipped town years ago. There are -1x to 10x developers on the team, and contractors who must "hit the ground running." Static typing is now crucial to prevent the whole thing from collapsing like a house of cards.
I recommend the best of both worlds, such as prototyping in a productive language, then port when the design solidifies. Or, use a language that can be gradually typed later as your startup matures into an established company with customers that rely on it.