What you point out is exactly what I think is the main drawback as Clojure (any dynamic language, really). It gets very tedious to waste so much time refactoring code and in the increased number of unit tests you need to write in order to avoid this from happening.
Personally, I think the lack of types in Clojure (and other dynamic languages) has another major drawback in that it doesn't allow you to easily express (and take advantage of) useful abstractions. In typed languages I got used to first write a "skeleton" of the application / library / module as a set of interfaces (traits, protocols, whatever expresses contracts in your language) and then develop and tests the implementations mainly independently. I find this extremely productive, in actual terms of amount of functionality per unit of time.
In Clojure I find myself doing diagrams or writing down data structures, etc. because there is no practical way of doing that in the language itself. Yes, I know protocols, types, spec, malli, etc. but they are not nearly as clean and useful (IDE help, etc) as let's say Typescript types.
I find this lack of expressiveness very frustrating, because I love Clojure otherwise. Persistent data structures are a game changer and the power you can get from a very small, generic API is mind blowing. And the data oriented paradigm, where everything tends to be expressed as data structures which are trivial to operate on, is simple conceptually and very powerful. I even got to like the syntax a lot. Once you get used to it, it's extremely clean, elegant and even clear (!).
Being as it is, though, I'm not sure that I would chose Clojure for a large project, even if I was the only one working on it. My brain needs typed languages as soon as I have more than 200 likes of code, it seems.
I see the fact that people often end up creating large and tightly coupled monolithic codebases in static languages as a negative aspect of static typing. Such codebases are difficult to reason about even if you have guarantees that the types align. Ultimately, you need to understand the relationships in code, and how they relate to business logic. The more coupling an application has the harder it becomes to reason about it as a whole.
Ideally, I think applications should be structured as a bunch of Lego blocks that can be composed together. Each component should encapsulate some functionality, and then the flow of the business logic should bubble up to the top and expressed in how these components are chained together.
You have to write many more tests and add a lot of predicates as pre/post conditions (invariants) to functions to check at runtime what a more modern compiler could have told you at compile time.
It adds a lot of friction and slows down the development process.
Combined with Clojure having lazy evaluation and nil punning it becomes frustrating in larger, domain heavy code bases.
Simple common “CRUD” line of business applications with shallow code paths and a database schema to serve as a secondary type system would probably suffer less from this, explaining why some devs say they don’t experience this.
For comparison, Typescript became so popular precisely because it mitigates similar problems for JavaScript once code based grew to non-trivial sizes.
Open a REPL, and start working. You can just execute the code in realtime as you're refactoring. The size of the codebase is largely irrelevant.
Ironically, in Clojure the compiler _does_ tell you what is working and what isn't when you work this way :)
And anyway, you write tests don't you?
Besides that as others have mentioned: Clojure spec, various kind of automated testing, including, if that's your thing, generative-testing/property-based testing generated based on your specs (it's not always trivial but can really help at times).
you're not supposed to do that in a dynamic language. Clojure's mentality towards data is "take what you need and ignore the rest", it's always the receiver of some data who is responsible to handle what they receive.
A lot of problems people have with dynamic languages would go away if they embraced the dynamism. If you're constantly refactoring because you make assumptions about the state of the program globally you're not actually utilizing the dynamic aspect of the language.
In practical terms, treat data in clojure the way you treat data over the web. When you get a some json data over a network you don't necessarily make absolute assumptions over its structure, you take what you need and deal with it when you receive it.
Also, due to the extensive use of FP and not much side effects, you can catch most of these mistakes at test time - not the same as a good static type system, but it is an interesting tradeoff, better expressivity, but not happening at compile time.
As a runtime contract, spec can check things type systems can't, but IMO it's not pervasive enough in most code bases to really satisfy.
(Though perhaps using them internally as well may be okay, just disable it at prod)
> compiler telling you if the pieces still fit together?
You still write tests in scala do you?