Building up from JavaScript to TypeScript to and C# 10 and .NET 6
As I’ve worked with JavaScript, TypeScript, and Node backends over the last year, there was always a point in the project when I wonder “This would be way better in C# and .NET 6”. When proposed to various teams, the responses I’ve gotten have been interesting.
There’s Been a Misunderstanding
The biggest challenge is that many recently minted engineers — and frankly even engineers who may have looked at C# and .NET even just 5 or 6 years ago — have a complete misunderstanding of where C# and .NET are today.
Of course, there are several “myths” about C# and .NET that are simply no longer true as the .NET Framework has given way to .NET Core and now simply .NET 6. However, many of those platform misunderstandings persist and engineers and teams without exposure to C# simply do not realize how trivial the lift is between TypeScript and C#.
When discussing the possibility of considering C# instead of TypeScript on one project with another developer, he stated that he always thought C# was more like C and C++ and was surprised by how closely it resembled TypeScript upon looking at C# more closely.
Others have expressed a concern that the lift from JavaScript to C# is too high and not feasible with the existing developers on the team yet push heavily for TypeScript after suffering the challenges of working with JavaScript on the server at scale. I personally think that for most JavaScript developers, the lift from JS to TS is much more significant than the lift from TS to C# to the extent that a team choosing to start a greenfield backend project in TS should evaluate C# as well.
When All You Have is a Hammer…
As the old saying goes, when all you have is a hammer, every problem looks like a nail.
In the last decade, as demand for software developers has grown, it has become more economical and efficient to train developers in a single programing language and anoint them as “full-stack” engineers. This language has been JavaScript. But the reality is that working with modern JavaScript, especially on the server, has many shortcomings and challenges (if I’ve piqued your interest, I’ve written more extensively on this topic). Yet many younger developers are not equipped to really consider other options because of just how pervasive Node has become as the first and often the only runtime environment developers are familiar with.
Even Ryan Dahl, the creator of Node, had this to write about Node (and by extension, JavaScript):
I want programming computers to be like coloring with crayons and playing with duplo blocks. If my job was keeping Twitter up, of course I’d using a robust technology like the JVM.
Node’s problem is that some of its users want to use it for everything? So what? I have no interest in educating people to be well-rounded pragmatic server engineers, that’s Tim O’Reilly’s job (or maybe it’s your job?).
In fact, Dahl would later expound on the many regrets he had with respect to Node:
And go on to create Deno to address many of those gaps. Notably, Deno is built on TypeScript and not JavaScript. It also focuses on security and improved dependency management; I think we all know the pain of working with node_modules
and fretting over (or more likely ignoring) the latest batch of vulnerability warnings on build. My favorite take on this is from Erlang The Movie II: The Sequel:
I like Node.js because as my hero Ryan Dahl says it’s like coloring with crayons and playing with Duplo blocks, but as it turns out it’s less like playing with Duplo blocks and more like playing with Slinkies. Slinkies that get tangled together and impossible to separate.
🤣
Duplo, Lego, and Technic
I love Dahl’s analogy of Node and JavaScript to Duplo because it works on so many levels. For the unfamiliar, Duplo is a chunky building block toy made by Lego which is designed to be easy to handle for young builders who lack the dexterity and fine motor skills to work with Lego proper; they are easier to snap together and easier to take apart. Duplo are often a young builder’s first introduction to Lego:
Dahl’s analogy of Node to Duplo works on many levels because working with Node and NPM is often like snapping blocks into place and with JavaScript and Node, those blocks have been designed for ease of handling rather than the ability to build complex structures. This is not to say that one cannot build complete and complex applications with JavaScript and Node, but that doing so involves compromises because the tool has fundamental limitations and challenges.
Of course, there comes a time when every young builder is ready to move on to regular Legos.
The pieces are smaller, more varied, more nuanced, and require more dexterity to work with; the sets themselves become more complex with higher piece counts. But it is easier to build more sophisticated constructs with Lego than with Duplo. Likewise, there comes a time in every team when TypeScript becomes a necessity to support the complexity of the construct being built or the size of the team doing the building.
It is possible to build and engineer incredibly complex structures with Lego, yet Lego produces another tier of building blocks known as Lego Technic that further extends the creative possibilities and the complexity of the structures that can be built.
There are even more varied pieces, more specialized pieces, motorization units, and so on which allows for the construction of elaborate and complex structures. While it is possible to build this same freight scene with Duplo or Lego, there is an undeniable richness that exists in the Technic version; there is a higher ceiling for creative outlet. And it is for this reason that teams seeking to build high performance systems should evaluate .NET and C#.
While Technic provides a higher ceiling to what can be constructed, one can argue that this comes at the cost of complexity. Yet it clearly remains undeniably Lego-like and it is easy to see the progression from Duplo to Lego to Technic whereas Plus-Plus building blocks clearly adopt a different paradigm (interesting fact: Lego, Plus-Plus, TypeScript, and C# were all created by Danes!).
Likewise, there is a clear progression from JavaScript to TypeScript to C#. For developers ready to make the lift to TypeScript, the gap to C# and .NET is really not that far as the languages — JavaScript, TypeScript, and C# — share a common lineage and observant developers will note that they have been converging since .NET 2.0.
The More Things Change…
I have worked with JavaScript for close to 24 years now and C# for 19 and what has been interesting to me is how they have converged over time. I first really noticed it with C# 3.0 which introduced arrow functions and LINQ expressions in 2007 before JavaScript introduced arrow expressions in 2015 with ES6. That release also introduced object and collection initializers.
With C# 3.0’s introduction of var
, the language has overall been trending towards more type inference and less explicit typing. While obviously not the same as JavaScript’s dynamic type system, there is a syntactic congruence.
C# even supports dynamic types (equivalent of var x = {}
) using — what else — the dynamic
type (aka ExpandoObject
) so it’s possible to use some dynamic techniques. I recently used it with the Jint library to build a simple JavaScript powered rules engine in .NET. It can even be used to implement double-dispatch style Visitor pattern.
Like JavaScript, functions are first class objects in C# via the Func
and Action
types. So you can pass, return, and invoke functions just as you would in JavaScript or TypeScript.
You can also notice that C#’s async/await
is largely identical with the exception of Task
versus Promise
.
C#’s try-catch-finally
exception handling is almost identical to JavaScript, but it is a bit more sophisticated since you can catch and handle specific types of exceptions using multiple catch()
blocks whereas JavaScript can only use a single catch()
and then check the type of the error.
C#’s deconstructing and discards are analogues to JavaScript’s very own.
C# even has local functions (in largely congruent styles):
It should be noted that C# lambda closures behave differently from JavaScript closures.
This:
Does not behave like this:
Which behaves like this:
(Google’s TypeScript style guide actually discourages the use of Array.prototype.forEach
and encourages the use of for-of
to iterate)
The three languages are so similar that when teams consider TypeScript on the backend (especially Nest.js), I recommend at least taking a look at .NET 6 Web APIs because the lift for most JavaScript developers to something like Nest.js with advanced concepts is pretty much 80% of the way to C# and .NET without all of the performance, runtime, and language benefits. Teams switching to .NET can save themselves a lot of headaches down the line with package churn, constant patching for security, and painful node_modules
management.
Ultimately, what I see happening with JavaScript, TypeScript, and C# is that the three languages are converging. JavaScript is in the late stages of implementing decorators like C# attributes (though there is some nuanced difference with C# attributes which are consumed via reflection). C# recently received pattern matching which I think we’ll see in JavaScript at some point in the future. TypeScript of course adds strong compile time checks, generics, and advanced structural code patterns (interfaces, abstract classes, statics, private members) like C#. C# will likely be getting discriminated union types similar to TypeScript’s unions in the near future, especially since C#’s sister language F# already has it.
My friend Arash Rohani said:
And to be fair the reason that .NET and C# is good now is because they have borrowed good concepts from other languages on top of the good things that they had already
.NET’s CLI tooling is now also very similar to the Node ecosystem and largely congruent:
1 2 3 4 5 6 |
dotnet new webapi dotnet add package serilog # Equiv of npm install winston dotnet build # Equiv of npm run build <-- build script dotnet run # Equiv of npm run start <-- run script dotnet watch # Equiv of running with watch dotnet test # Equiv of npm run test <-- test script |
In the old guard .NET community, there has been a lot of old-man-yelling-at-clouds with respect to the changes in C# and .NET. I myself am guilty of it. But each iteration of C# has only deepened my fondness of the language and platform, especially when I find myself fighting against Node, NPM, and the limitations of JS/TS.
Making the Lift
As Dahl implied, it is important for engineers and technical leaders to pick the right tool for the project. JavaScript and TypeScript on Node are fantastic tools to build with for rapidly building applications. Like Duplo, the blocks are easy to handle, especially for inexperienced builders and certainly there is an advantage for newly minted developers to be be able to program full stack with one language.
For performance at scale, platform security and stability, operational manageability, as well as overall scalability, C# and .NET’s close lineage with JavaScript and TypeScript provide a clear path for building more complex systems. C#’s congruency with TypeScript means that the lift from TypeScript to C# isn’t nearly as onerous as some would think; it’s easy enough to start with C# — especially using the minimal APIs — to provide your team a higher ceiling. You don’t have to build using all of the advanced features of C# and .NET, but you retain the flexibility to incrementally add more performance (e.g. Task Parallel Library) and complexity as needed.
It has been a long journey from the .NET Framework to to C# 10 and .NET 6 and the platform’s transformation that started with .NET Core has now been fully realized. For teams that have been feeling the growing pains with JavaScript and TypeScript on Node, there’s never been a better time to consider C# and .NET!