DEV Community

Jesse Warden
Jesse Warden

Posted on

10 Months of Elm to Angular

It's been 10 months since I went from Elm to TypeScript Angular, and I still struggle with the following:

Void Return Values

I see a void return value, and immediately freak out about side-effects. I instantly want to ensure we remove all code from it, save the actual dependencies being injected, ensure all the business logic for the function is unit tested in pure functions separately.

My co-workers, many of which grew up in Side Effects Are Ok Actually Ville®, have a range of reactions from mild intellectually curious, to genuine pity for me. We all galvanize around the thorough testing as a rally point which is nice.

Spies / Mocks

I have intentionally avoided all spies and mocks, favoring stubs only in unit tests within reason. The actual side-effects, I ensure are covered by Acceptance Tests in Cypress. I have loosened my methodology here a bit because some of the side-effects aren't easily testable (yet, on my list, sucka!) in Cypress. Examples include logging analytics and metrics to New Relic, or sending extremely important payloads in RxJS BehaviorSubjects.

That said, I do my best to ensure all those side effect tests are alone, and there are few, on purpose, because 90% of the code is pure. This is quite challenging in a class based language where the types from TypeScript have improved so much, it makes it quite easy to create functions/class methods that return void, but everything else is typed quite well. Additionally, "immutability by default" seems to be a culture thing here, and I don't think it's just because a few came from React. The readonly keyword is hardly found. However, in unit tests, I've seen copious mutation either indirectly through the use of Jest spies, or implicitly through manual mocks.

Failing at Test First Lead by Example

I continually see code that was tested after/tested last. While I'm on a Platform Team, and am only a Tech Lead for my team, I still am responsible for doing a round robin with colleagues to review PR's on our platform from numerous other teams. When I encounter such code, I usually write a book, customized for context of the PR, to help the developer make their testing life easier, and hopefully code maintenance too.

Some of the problems are copy-paste syndrome which has pro's and con's. The pro's are we have various standard ways of doing things within our Angular realm, and so it gives a lot of example code various teams can pull from which is great. The con, however, is some of those "ways of doing things" aren't great. Specifically, many will mix business logic and side-effects, making the code quite a challenge to unit test. There are no guilty parties here, just many who either never knew this, or haven't had to maintain a test with 20 mocks just to test 1 line of code, then have to go make a change to the internals, and the entire test suite breaks.

... but I keep trying anyway. A few, a small few, have expressed appreciation for the help, so I keep going.

Functional Thinking vs Angular Way

We've had a few cases where we needed to build new capabilities and you can see some of my co-workers, or some of the React devs who transferred to our platform, have a functional way of doing things (e.g. just functions and types, no classes, very few side-effects included). This makes code reviews extremely hard because on the one hand, Angular has a public API that has worked for almost a decade, upgrades tend to go very well, and we all follow the prescriptive way to build UI components. But beyond UI components, you have flexibility in implementation details. Yes, your injections have to be classes, but the implementation details do not. When you start testing first, and intentionally trying to avoid mixing your business logic with side-effects/UI rendering, you find that classes start having zero value; many are just static methods with no state. The next step is to just use functions.

For the components, sure, classes work for them "because Angular", but also, they do have state, so it's fine. It sounds great in theory that "choose Signals for this kind of stuff, and RxJS for this kind of stuff", but that having another way to do something has tradeoffs that do reduce consistency. This means reading the code slows down because how someone approaches something differs from section to section. Some of those implementations may be valid, and better, true, but that does have a cost. I constantly struggle to juggle "There's an Angular Way, and then there is this simple functional thing you did" and we review these a lot ensure other devs on the platform will grok it, dig it, and use it consistently. It's tough.

It's too bad the experiment that Angular did making their code look like Vue/Svelte, where no classes were used, and it was just functions and hook style re-useable framework bits never went anywhere. If you look at Angular's evolution, they seem to be continuing the exact same style, and just changing the underpinings (removing NgZone) + adding a few primitives like Signals. So the "Well typed OOP with immutability" seems like it'll be here for years to come. That's... sad.

TypeScript Has no Easy Type Conversion Features

This is probably the worst, and most painful for me in my career to explain to others. Coming from Elm and ReScript, we have super powerful type systems that are quite sound, or near sound. ReScript in particular is gradually typed like TypeScript, but much more thorough and strict. There really isn't a concept of TypeScript's "any" in either language, but ReScript give you enough lower level primitives that they "feel" like an TypeScript unknown which in turn leads to copious compiler errors, which is good. Once it compiles, you feel pretty safe.

Contrast this with TypeScript, and any and "as MyType" is used all over the place. Any is used for a variety of valid reasons: devs haven't been taught type based programming, what you can do with types, how to read the types, how to create their own types, they should be using unknown if they truly don't know, etc.

And the use of "as MyThing" is because TypeScript's type narrowing is super painful. You have to know about low-level JavaScript type casting functions and operators, and do them in a specific order to make the compiler give you insight on to where you should go next. The ROI doesn't appear to be there for anyone save me; I think that's because I spent years in both dynamic and typed languages getting bit by the lack of strong types, then lived in a world where the types ensured it was always a back-end person problem (or... the... occasional race condition), so I've seen how good things can be if you put in the effort. To combat this, I've tried to lead heavily with Zod which fixes all of TypeScript's painful type conversion with 1 line of code.

What I haven't been able to figure out is all the unsafe type casting and heavy use of Partial. The type casting is because creating some of the larger types/interfaces needed for unit tests in particular is quite painful. The idea of creating functions to make safe types like you did in Elm/ReScript/Scala, etc. isn't normalized here. The "make impossible situations impossible" is continually a new thing, which is great, but also telling. The deadline pressure usually pushes out anything that isn't immediately helpful, which I get. So given TypeScript is also a structurally typed language, they'll make an Object that's "close enough" and just use "as MyType" and voila, TypeScript is happy (turned off, but... hey, it's happy and the test passes). I've tried various tactics here, and not much helps.

The Partial is also annoying. The valid reason is because some of the typed JSON payloads we get back are gigantor for "reasons", and devs typically only need small parts so they'll use things like Partial or Pick to make testing and code easier. Given TypeScript has great support for optional values, much like Maybe in Elm, or Option in ReScript, and a lot of the values do NOT need default values because Angular et all will just convert to an empty string when shoved in a UI component... they don't have the same pain Maybe's/Optional's do in FP languages where you suddenly are forced to handle 2 things vs 1 thing. Readability and maintenance pain? Yes, that's there too.

While I'm not a Hexagonal / Onion Architecture fan, I think some Adapters here to convert the back-end JSON we get to useful UI types would help immensely. You'd have easier to use types, custom made for your UI views, and they decode in 1 line of code back to JSON when we need to send something to the back-end. Right now we're married to our back-end which isn't tailored for the UI so the JSON is hard to work with. That'd take like a year. Maybe I should try, idk.

Anyway, I feel like I'm helping, but it's... been rough, true, but not as "omg I'm going to be miserable every day". I enjoy my co-workers, I enjoy helping all the coders, and they teach me things too. That said, I still do side-projects in Elm and ReScript to remind myself of what we're shooting for.

Top comments (1)

Collapse
 
laurentpayot profile image
Laurent Payot

I don’t know Angular, but I had to leave Elm too since it is basically abandonware. I moved from Elm to F# + Fable + Feliz + Preact.  The syntax is quite similar, and interacting with JS is so easy. I simply regret I didn’t do it earlier.