Sure, at boundaries between teams, you need to specify the data in some way. That could be a type, but for me, often the other team is using a different language than me, so it needs to be a language agnostic type, and it can't include unsigned numbers because Java can't cope, and it can't include large integers because Javascript can't cope, etc. Protobufs are popular, json is too.
I have a lot of unpopular opinions though, and that's fine. It's just tiresome that everyone wants to come in and add types to things that don't need them. Also, I agree with dllthomas, most developers and teams are capable of creating dumpster fires in all sorts of environments, with all sorts of tooling. :)
Putting the fire out in an untyped language is a Herculean effort.
With the way that Erlang and Elixir pattern matching can be used in function heads, I can have much the same feeling of certainty that people express from Haskell and Rust. (Erlang typespecs help here, but are not checked by the compiler itself, only by additional tools like dialyzer or gradualizer.)
This assumption is changed, IMHO, by Erlang. Hot loading makes the cost to make small changes very low. So the question becomes, do you pay the definite cost of build time type checking (usually includes coding time type annotation), or do you accept the possible future cost to making small fixes.
Of course, if you work in an organization where even a small fix requires months to release, then do all the things you can to prevent making small mistakes.
A small number do. Assembly languages are generally untyped. The Forth language is also untyped.
> At build time you catch bugs that would appear later at runtime. Which is more costly to fix later.
Generally agree. Programmers proficient in Haskell or Ada tend to consider types to be integral to their development process. The real question is whether this is a good tradeoff against development velocity, for your given project. Neither language markets itself for rapid application development, instead they tend to emphasise that the language aids with correctness and the ability to reason about code's behaviour.
That said, a dumpster fire usually has no or little tests, so maybe we're arguing non-existent hypotheticals :|
- Types and tests find different bugs. I’ve found new bugs by converting a project from javascript to typescript. The project in question had a 2:1 test:code ratio but as soon as the typescript compiler could read it, it spotted a couple obvious errors.
- Large test suites often make refactoring harder, not easier. If you have a clear, fixed API boundary and your tests test that boundary, then testing helps. But most refactoring also involves changing up those APIs as well - since bad APIs are often the reason you want to refactor in the first place. When you do that, you have to also rewrite all your tests. Good type systems help refactoring. Writing rust in Intellij, I can globally rename functions and types in my project, promote tuples to structs, reorder function arguments, and all sorts of other handy refactorings. My tests get updated too. And the compiler tells me immediately if I missed anything, without needing to rerun my tests.
- Reading the types is my favourite way to get up to speed on a project, or get back up to speed on something I wrote myself that I’ve forgotten. "Show me your flowchart (code) and conceal your tables (type definitions), and I shall continue to be mystified. Show me your tables, and I won't usually need your flowchart; it'll be obvious." -- Fred Brooks, The Mythical Man Month (1975)
- I find I need far fewer tests to write reliable software when I’m using a language with a good type system. Most rust code I write works correctly once it compiles. Javascript is easier to write than typescript, but it’s harder to test and debug.
So with all that, I’m personally in camp type these days for most software. I think it’s usually the right choice.
A clear, fixed API boundary is exactly what Phoenix tries to encourage with contexts. Unfortunately, a lot of developers find them hard to understand. They're simple if you read up on DDD but again, a whole host of developers won't, or don't, do that either. LiveView in particular has a really a really great testing library [0] where you can write what are essentially end-to-ends that never touch even a headless browser. Since I'm always writing LiveViews, I pretty much only write LiveView tests and contexts tests which gives me large coverage (also some unit tests for the odd utility function). Otherwise, it's really important when writing non-typed functions to make it really obvious what is coming in and out, which is arguably a nice forcing factor.
The number one thing people bring up when shilling types is large codebases (it's been brought up in these comments). My opinion there I have found is quite unpopular and that is that pair programming should be far more prevalent than it is. I think the whole notion of "just stick a junior on that" is broken and I don't understand how types make that situation _that_ much better.
All said and done, I'm not actually anti-type. I mostly just find them to be incredibly noisy compared to a well-written function. I really like Ocaml where it's statically typed without needing to actually specify them.
However, a language with an insufficient type-system indeed makes things harder than they are without it. I would count all the languages you listed into this category.
As another poster mentioned, typescript is fairly expressive. There are other (production) languages too, such as Scala or maybe D. And there are lots of academic/very-niche languages.
> It's just tiresome that everyone wants to come in and add types to things that don't need them
Well, types are there, if you like them or not. There's a reason that you have e.g. typeof in javascript, gettype in PHP. The question is rather if you explicitly annotate them or not. But yeah, sometimes it's not helpful to annotate types, especially if the language is incapable of expressing the correct type anyways, which is true for most programming languages.
Disclaimer: I work at Microsoft, but not in the Developer Division
I have started programming in untyped languages, but simply can’t remember back at all, and now I can’t really imagine dealing with objects in my mental medal as not having some type.
Note: this is not a rebuttal for/against dynamic typing, I do think that types are really important at boundaries, but they may not be the silver bullet - contracts may be better at some things, for example. This may be an open question.
In Elixir, I do think about shapes more than capabilities (because Elixir is not OO), but with pattern matching, I can either specify "this must be a MyApp.Account struct" (which is just a fancy map) or I can specify "we will handle any map that has the keys X and Y, and Y must be a map itself".
I replicate this more formally when writing TypeScript, usually by building up type definitions and specifying those.
Look at Erlang. It has bigints, floats, Booleans, but-strings (sequence of bits —added because it is so common in telecom), string (not technically a primitive data type), functions, atoms, list, tuple, and map.
None of these look the same or act the same. People dream about seeing `123 == myMap`, but it simply isn’t a common thing because it doesn’t make sense.
The common rebuttal becomes: but how do I know if property X is a string or number when I’m using it?
If that’s your question, you are already messing up. What you really want to know is what X actually represents. Otherwise, you’re just shooting in the dark which is at least as dangerous as getting the type wrong and probably more so because a wrong type will become obvious quickly while mangling that number or string may not be caught until a much later time after serious damage has propagated throughout the data.
Let’s say you have something called `login`. Is it a number or string? If it’s a string, is it an ISO date, UTC date, or something else? Is it when they logged in or when their login expires? If it’s a number, it could be a Unix string. It could also be a calculated value for how long the user has been logged in and could be days, hours, minutes, seconds, milliseconds, or something less common.
How do you know which thing is correct?
In a good codebase, you read the docstring comment on the data constructor that describes what it does. If it says “milliseconds since last login” vs “token expiration using ISO datetime format” do you have any question at all about whether it’s a number or string?
If there isn’t a docstring, you’ll be digging through that code or playing around with the responses and will see the data type anyway.
The result is that you’re forced to better understand what you’re doing which isn’t a bad thing in my opinion. There may still be mistakes, but that leads to the next point.
Dynamic languages generally make it easy to dynamically check the types of incoming data and tend to be more flexible with mistyping (especially JS). I can’t count the number of major errors from common typed languages because they make introspection hard, so programmers don’t do it and crash on malformed data or when an API suddenly changes.
It’s also worth talking about null exceptions. Many dynamic languages expect type weirdness and handle it well. This usually includes null. Most statically typed code out there has LOADS of null exceptions lurking about which only trigger in obscure cases during runtime. In this regard, you could argue that the worse type issues also happen in typed languages, but are more dangerous in those languages too.
Untyped languages also tend to use safe numbers everywhere. Infinity is mostly useless, but not usually dangerous. Bigints everywhere are slightly slower in some cases, but completely eliminate overflow errors. Most typed languages use risky numeric types, so they must also force users to think about those things.
Finally, no common typed language offers good runtime introspection like smalltalk, Common Lisp, Erlang, or even JS. I feel static types are just a crutch to make up for this deficiency.
That brings us to good static languages. Typescript offers all the benefits of normal, unsound static typing combined with all the robustness of JS’s dynamic environment. The same can be said for Coalton and Common Lisp.
StandardML offers a language that feels like a dynamic language, but still offers static checks that are actually sound and completely eliminates null exceptions. Rust (an ML in spirit if not in syntax) does the same things in environments where garbage collection and other such amenities aren’t possible.
I like both kinds of languages and good examples from each word around the problems of each approach to make them (in my experience) about equal in productivity for equally skilled and experienced developers.