I'm trying to manage my projects mostly like the steps the OP takes. The only thing that I would add, which has helped me is, is having build automation in place as well (CI/CD or just scripts like a Makefile).
This could fall under tests, but I've found that having concrete steps written down on how to build the thing helps. As there's many times something that's easy to forget that has to happen for things build correctly if I've stepped away from the project for a while.
On those languages, if you define the types X = A + B and Y = A + B, and if you try to match X = Y, they don't match at all. Besides, they don't compose (X = A + B, Y = X + C, Y = A + B + C is false).
The result is that if you go and declare `read :: Handler -> Either FileAccessError String`, you can't just declare your function as `f :: String -> (FileAccessError | NetworkError | MyCustomError)` expect it to work well with the rest of your code. Because every time you need a different set here, you'll have to declare a different type, and transform the data from one type to the other.
Also