Several times I have been asked about the differences between TypeScript and Elm. This is an attempt at listing down these differences, mainly from the perspective of the two different type systems.
Let's start with a quick introduction.
TypeScript is, in case you have been living under a rock for the last 5 years, a superset of JavaScript that adds optional static typing to it. "Superset" means that all legal JavaScript programs are also legal TypeScript programs, so TypeScript doesn’t fix anything in JavaScript but adds type checking at compile time.
Elm is a purely functional language that compiles to JavaScript. Elm is not only a language but it is also a framework in the sense that includes a way for building web applications ("The Elm Architecture") so it is more like the sum of TypeScript, React, and Redux combined.
Here we go...
Soundness
One of the definitions of "soundness" is the ability of a type checker to catch every single error that might happen at runtime.
TypeScript's type system is not sound, by design. "A sound or 'provably correct' type system is not a goal of TypeScript."
Elm's type system is sound and it infers all types. It uses the Hindley-Milner type system that is complete and able to infer the most general type of a given program without programmer-supplied type annotations or other hints.
There is some discussion about the feasibility of making TypeScript, or any superset of JavaScript for that matter, a sound type system. See Hegel for an example attempt in such direction.
Other attempts have been done, but required to define a subset (the opposite of a superset) of JavaScript to be able to reach a sound type system. In the paper "Type Inference for JavaScript", the author provides a static type system that can cope with dynamic features such as member addition, while providing the usual safety guarantees. To achieve that, the author creates a language that is "a realistic subset of JavaScript, but manageable with respect to formalization and static typing. [...] It is better to have a sound system, even with its restrictions, than a half attempt that gives no real guarantees."
Type inference
Type inference is the mechanism used by the compiler to guess the type of a function without the need of the developer to describe it.
In TypeScript some design patterns make it difficult for types to be inferred automatically (for example, patterns that use dynamic programming). Dependencies or functions like
JSON.parse()
can returnany
, having the effect of turning off the type checker and the inference engine.Elm's type inference is always correct and covers the entirety of the code, including all dependencies (external Elm packages). Elm doesn't have the concept of
any
.
Enforced type checking (escape hatches)
TypeScript uses implicit and explicit
any
as an escape hatch from the type checking. Is possible to reduce these escape hatches by configuring TypeScript withno-explicit-any
. This can still be overwritten witheslint-disable-next-line @typescript-eslint/ban-ts-comment, @ts-ignore: Unreachable code error
.Elm does not have escape hatches, the code compiles only if all types are correct.
JSON safety
Applications often deal with data coming from sources out of their control, usually over a network. Several things can make this data different from what we expect and this can harm our applications.
TypeScript's
JSON.parse()
returnsany
. This means that part of the code has now escaped the control of the type checker. There are other libraries, such asio-ts
,zod
,ajv
,runtypes
that can support the checking of JSON data.JSON.stringify()
also can generate exceptions, when used with BigInts, for example.Elm uses decoders and encoders when dealing with JSON data, forcing the developer to take care of all possible edge cases (for example, an invalid JSON structure, a missing key, or a value with a wrong type).
Protection from runtime exceptions
Runtime exceptions are errors happening in the browser when the JavaScript code tries to do an illegal operation, such as calling a method that doesn't exist or referencing a property of an undefined value. Most of these errors can be avoided with the support of a strict type system.
TypeScript mitigates the problem but runtime exceptions can still happen. “Mutation by reference” is one of the cases that can generate runtime exceptions.
Elm's sound type system together with other design choices guarantees no runtime exceptions.
null
and undefined
null
references, also called "The Billion Dollar Mistake" by its creator, are the cause of all sorts of problems. Together with undefined
, they are the culprit of a large chunk of bugs and crashes in applications.
TypeScript mitigates the issue with the
strictNullChecks
flag. When it is set totrue
,null
andundefined
have their distinct types and you’ll get a type error if you try to use them where a concrete value is expected.Elm does not have either
null
orundefined
. Elm leverages the type system in case of missing values, with the typesMaybe
(calledOption
in other languages) andResult
.
Error handling
Many things can go wrong during the execution of an application. The handling of these errors has a direct impact on the quality of the UX. Is the application just going to crash or is it giving informative feedback to the user?
TypeScript's error handling is based on the concept of throwing errors and using
try/catch
statements to intercept them. Developers have the responsibility to understand where things can go wrong and cover all possible cases.Elm handles errors leveraging the type system with the types
Maybe
andResult
. There is no concept of throwing exceptions in Elm, so thetry/catch
statement doesn't exist. All places where things can go wrong are explicit, highlighted by the compiler.
Pattern matching
Pattern matching is an expressive way of checking if a value matches certain patterns. Proper pattern matching also provides compile-time exhaustiveness guarantees, meaning that we won’t accidentally forget to check for a possible case.
TypeScript does not support pattern matching. It can support "exhaustiveness" with switch statements under certain conditions (flag
switch-exhaustiveness-check
activation use ofassertNever
).Elm's support pattern matching (with the
case...of
syntax). Elm's pattern matching always applies exhaustiveness.
Error messages
TypeScript's errors are good, especially for basic errors. They also suggest correct possible fixes. They can become less clear when the types get more complicated.
Elm's errors tend to pinpoint the exact location of the problem, especially if the code contains type annotations, and usually provide a well-balanced context and good advice about fixing the issue. Elm's errors have been taken into special consideration. They are considered the gold standard in their category and have been an inspiration for error messages in other languages, like Rust and Scala.
Opaque types
Sometimes is convenient to hide the internal implementation details of a custom type so that the library is decoupled from the code that uses it.
TypeScript's support for this feature is still unclear to me. Maybe private/public class attributes or methods can support it? Or maybe "branded types"? More info here and here.
Elm's support private modules so creating an opaque type is done exposing the type but not the type constructor as explained here.
Type annotations
TypeScript, wherever possible, tries to automatically infer the types in your code. If the inference fails or is wrong, it is necessary to add type annotations manually. Type annotations are mixed with the code, at the beginning of the function definition.
Elm never needs type annotations, the compiler can infer all the types all the time. Type annotations are separated from the code, they stay on a separated line, above the function definition. Even if optional, it is considered good practice to add type signature as this improves the readability of the code and also makes the compiler errors more precise.
Complexity and Learnability
Complexity directly impacts the time to learn new technologies and also the productivity of developers.
TypeScript is a superset of JavaScript so if you are familiar with JavaScript, it is simple to start using it. But mastering it is something different. TypeScript has an overly complicated typing system. This isn’t strictly a disadvantage of TypeScript, though, but rather a downside that stems from it being fully interoperable with JavaScript, which itself leaves even more room for complications.
Elm is a different language from JavaScript so starting with it, if you are coming from JavaScript, present an initial steeper learning curve. The type system is relatively simple so it is simple to master it. The Elm type system is rooted in two main concepts: custom types and type aliases.
Let's expand a bit on this, as I think is an important concept. The Elm type system is based on a small set of primitives, mainly Custom Types and Type Aliases.
For example, there is one way to enumerate the possible values of a type in Elm, using Custom Types.
type ButtonStatus = HIDDEN | ENABLED | DISABLED
While in TypeScript it can be done in three (and possibly more) ways:
// With string enums
enum ButtonStatus {
HIDDEN = 'HIDDEN',
ENABLED = 'ENABLED',
DISABLED = 'DISABLED',
};
// With union types of string literals
type ButtonStatus = 'HIDDEN' | 'ENABLED' | 'DISABLED';
// Using the "const" assertions
const ButtonStatus = {
HIDDEN: 'HIDDEN',
ENABLED: 'ENABLED',
DISABLED: 'DISABLED',
} as const;
Each of these approaches has its pros and cons.
The difference here is that Elm is more on the side of, similarly to the Python Zen, that "there should be one - and preferably only one - obvious way to do it".
On the other side, TypeScript gives multiple options that may confuse beginners ("Which type should I use?") but can bring more flexibility to experienced developers.
Adoption
TypeScript is widely adopted. It took off thanks to Angular support in 2015 when Google decided Angular 2 would be built using TypeScript. Since then, most of the other mainstream frameworks based on the JavaScript language started supporting it. Being a superset of JavaScript makes it relatively simple to add it to an already existing JavaScript project.
Elm has a smaller adoption. Compared to JavaScript, it is a different language with a different syntax and a different paradigm (Functional instead of Object-Oriented). So it requires a larger effort to convert existing projects and a mindset shift in developers to adopt it.
Configurability
TypeScript has around 80 options that can be turned on or off. This can be helpful when upgrading a JavaScript project where strictness can be increased gradually. It may also create differences in code when compiled with different settings. In this case, code may refuse to compile and is it necessary to either change the TypeScript configuration or to adjust the code.
Elm doesn't have any option related to the strictness of the compiler. It supports two settings related to the type of outputted code: With or without the debugger, and optimized or not optimized, for a production-grade build.
Third-party libraries - Protection from changes
When using TypeScript, updating libraries from NPM does not guarantee the absence of breaking changes (the progression of versions is not checked by NPM), or the introduction of errors in the type annotations.
Elm support two layers of protection. First, it enforces semantic versioning to published Elm packages. This means that the version of a package is decided by the Elm Package Manager and not by the author of the package. This guarantees that updating the libraries cannot break our code. Second, all libraries are type-checked the same as our code, so if the code compiles, it means that all types are correct and a library cannot start having side effects, like harvesting bitcoins as it happened in the event-stream incident.
Third-party libraries - Type checking coverage
TypeScript does not require all the dependencies to be written using TypeScript. Also, the quality of type annotations in the dependencies may vary. As @derrickbeining put it: "nearly every open source library with type declarations (if they even have any) were written by someone who seems to have only a cursory understanding of what the type system can do."
Elm's dependencies are all written 100% in Elm, so there are no holes in the type system. Types are correct across boundaries, keeping all guarantees intact, regardless of which library we import in your codebase.
Immutability
Immutability is when a variable (or object) cannot change its state or value, once it has been created.
Immutability has several benefits, like the absence of side effects, thread-safe, resilient against null reference errors, ease of caching, support for referential transparency, etc.
Immutability may also have issues, like impacting negatively on the performances of the system. These issues can be alleviated or completely removed with proper strategies.
TypeScript doesn't support real immutable data structures. In JavaScript, mutability is the default, although it allows variable declarations with "const" to declare that the reference is immutable. But the referent is still mutable. TypeScript additionally has a
readonly
modifier for properties but it is still not a guarantee of real immutability.Elm's data is fully immutable, by design. Including also in all the dependencies.
Purity
Purity means that the type system can detect and enforce if a function is pure, meaning that the same input provides the same output and it doesn't have any side effects. Pure functions are easier to read and reason about because they only depend on what is in the function or other pure dependencies. Are easier to move around, simpler to test, and has other desirable characteristics.
TypeScript can enforce some attributes of pure functions but cannot detect or enforce purity. There is a proposal about adding a "pure" keyword that is under discussion.
Elm code is all pure, by design. Including all the dependencies.
The type system "in the way"
Sometimes developers feel that the type checking is an obstacle rather than a support.
I think several factors can be the causes of this feeling.
It may come, for example, from a negative experience with other languages that required a vast amount of type annotations (Java?, C++?).
In TypeScript sometimes there are situations where the application is working but at the same, time the type checker is reporting that the types are incorrect or some type annotation is missing.
Especially coming from JavaScript, this situation can be frustrating as JavaScript always tries its best to not complain also when types are not correct.
Also sometimes the errors reported by TypeScript may not be clear enough to lead toward a resolution in a short time.
Elm can also give the feeling of being in the way, especially to a novice that needs to deal with a new paradigm, a new syntax, and a new type system. While I was learning Elm, I was arrogantly blaming some bug in the Elm compiler when I was getting some type error, because I was confident that my types were correct. After being proved wrong over and over I now take a more humble approach when I get these types of errors.
Compared to TypeScript, Elm will never require to add type annotations, as these are fully optional and the errors of the Elm compiler are always indicative of a real type mismatch. There are no false positives and the error messages are usually clear enough to lead to a quick fix.
Compiler performance
The time needed for the compiler to finish its work is important for a good developer experience. A short time from saving a file to seeing a web application changing on the screen allows for fast and comfortable development.
I could not find a precise benchmark for the performance of TypeScript. From anecdotal experiences, like the one of the Deno development team that stopped using TypeScript because it was taking "several minutes" to compile and some other posts it seems that TypeScript has some room for improvement in this field. Let me know if you have any hard data to add to this section.
Elm compiler performance was measured after the release of version 0.19 that contained several performance improvements. The expected approximate times for 50,000 lines of Elm code are 3 seconds for a build from scratch and 0.4 seconds for an incremental build. The actual compile time for the incremental build is around 100 milliseconds. The other 300 milliseconds are used to write the output to a file.
JavaScript Interoperability
TypeScript code can call JavaScript functions directly.
Elm has three mechanisms to interact with JavaScript: Flags, Ports, and Custom Elements. The lack of a Foreign Function Interface (FFI) is a tradeoff that Elm makes in exchange for several benefits.
Feature completeness
As a matter of what type of features are on both sides, there is a lot of overlapping. Sometimes things are easier to be expressed on one side, sometimes are easier to express on the other side. For example
Creating types from data
TypeScript can create types from data, using the
typeof
operator (note that JavaScript also hastypeof
but it has a different meaning). For examplelet n: typeof s
means thatn
ands
will be of the same type.Elm doesn't have the analog of
typeof
. Elm requires you to declare the type first, and after associate it to bothn
ands
.
Custom type differentiation
When we create our types, is good to be confident that certain values belong to these newly created types
- TypeScript requires boilerplate that add checks at runtime (User-Defined Type Guards), For example
function isFish(pet: Fish | Bird): pet is Fish {
return (pet as Fish).swim !== undefined;
}
if (isFish(pet)) {
pet.swim();
} else {
pet.fly();
}
- Elm's custom types are differentiated by the compiler at all times.
case pet of
Fish fish -> fish.swim
Bird bird -> bird.fly
Enums iterability and conversion to string
Sometimes is useful to iterate across all members of an enumerated type, or convert members to string.
TypeScript has three types that can be used as Enums: "enums", "const enums", and "literal types". Some of these can convert automatically to string. In the other cases, the conversion needs to be done manually.
Elm's Custom Types (used to create Enums) cannot automatically be iterated or converted to a string. These two operations need to be either done manually, through a code-generating tool, or with the static code analyzer
elm-review
.
Some alternatives
Let's see what are other alternatives separated by the two categories.
An alternative to TypeScript can be Flow, a library maintained by Facebook. Flow, similarly to TypeScript, is not a sound type system. "Flow tries to be as sound and complete as possible. But because JavaScript was not designed around a type system, Flow sometimes has to make a tradeoff". Another alternative is Hegel, a type system that "attempts" to be sound. It is unclear to me if the attempt succeeded or not but it is worth checking.
Alternative to Elm can be PureScript, ClojureScript, ReasonML, ReScript, and other languages that compile to JavaScript. There are also newer and interesting languages that are still in an explorative state like Ren or Derw.
Conclusions
These are two remarkable pieces of technology.
TypeScript is a powerful tool that helps to deal with the idiosyncrasies of JavaScript, designed to allow you to work seamlessly with a highly dynamic language like JavaScript. Trying to put types on top of a dynamic language is not a pleasant task and some of its characteristics, like not being a complete type system, can be a consequence of this constrain.
Elm is a different language from JavaScript. This allows for a coherent and organic type system that is baked in the language and provides the foundations of the language itself, making it possible to support a complete type system
Both languages came to the rescue
Both languages came to the rescue when JavaScript’s rather peculiar runtime semantics, applied to large and complex programs, make development a difficult task to manage at scale.
TypeScript requires a complex type system to work seamlessly with a highly dynamic language like JavaScript. The effort of fully type-check JavaScript remaining a superset of it seems close to impossible because it requires also considering all JavaScript's quirks and checking all dependencies.
As expressed in this comment: "TypeScript feels worth it until you use something like Elm, then you realize just how lacking TypeScript's type system truly is. [...] That strict dedication to being a superset [of JavaScript] means the type system explodes into ten thousand built-in types that come, seemingly at times, from nowhere, simply to control for the wildness of Javascript. [...] I need to have an encyclopedic knowledge of all these highly-specialized types that are included in the language and are often being used in the background"
Different perspective
I noted that the opinions about TypeScript change greatly if developers are coming from JavaScript or if developers are coming from a functional language, like Elm or Haskell. Coming from JavaScript, TypeScript may feel like a major improvement but coming from Elm, it may feel like a cumbersome way to deal with types. Both perspectives have some truth in them and I invite you to try to understand both sides.
So kudos to the TypeScript engineers that are trying to catch as many issues as possible. If you are building a large application in JavaScript and you cannot afford to change language, I think that TypeScript is improving several aspects of the developer experience.
Stress-free developer experience
But if we can break free from JavaScript, learning Elm can be an enlightening experience, to see how a sound type system built from the ground up can make the difference.
This is what can make our developer experience to became stress-free, taking away from us most of the burden as a developer. These types of questions that usually we need to carefully answer to build reliable code can disappear.
- Should I wrap this in a try-catch block because it may crash?
- Can I move this piece of code?
- Can I remove this function?
- Is this function pure?
- Should I check if these parameters are null or undefined?
- Should I check if this method exists before calling it?
- Can I trust this third-party library?
This can give us peace of mind and a lot of extra time to think about what we care about.
❤️
Other Resources
- From Javascript to Typescript to Elm by Kevin Lanthier
- If you're using React, Redux, and TypeScript, you would be so happy with Elm! by Ossi Hanhinen
- TypeScript’s Blind Spots by Dillon Kearns
- Functional Programming for Pragmatists by Richard Feldman
- Convergent Evolution by Evan Czaplicki
- Comparing TypeScript and Elm's Type Systems by Elm Radio
- ReScript – the language after TypeScript? by Marco Emrich
- Why Elm Instead of TypeScript? by Chris Krycho
- TypeScript Types Can Do Some Cool Things That Elm Can't by Dillon Kearns
- Why we chose TypeScript for the Hasura Console by Aleksandra Sikora
- Elm in TypeScript, pattern matching and beyond by Maciej Sikora
- Type Inference for JavaScript by Christopher Lyon Anderson
- Proposal to add first-class TypeScript support to Ramda
- Typing is Hard by Ben Fiedler
- The TypeScript Documentation
- The Elm Documentation
❤️ ❤️
Top comments (2)
Fantastic article! I've been adopting so many functional programming patterns in Typescript that I might just as well try out Elm.
You miss ELM in Javascript ?
Look at this : Derw
Regards