DEV Community

Cover image for Differences between TypeScript and Elm
lucamug
lucamug

Posted on • Edited on

Differences between TypeScript and Elm

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.

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 return any, 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 with no-explicit-any. This can still be overwritten with eslint-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() returns any. This means that part of the code has now escaped the control of the type checker. There are other libraries, such as io-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 to true, null and undefined 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 or undefined. Elm leverages the type system in case of missing values, with the types Maybe (called Option in other languages) and Result.

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 and Result. There is no concept of throwing exceptions in Elm, so the try/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 of assertNever).

  • 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
Enter fullscreen mode Exit fullscreen mode

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;
Enter fullscreen mode Exit fullscreen mode

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

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 has typeof but it has a different meaning). For example let n: typeof s means that n and s 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 both n and s.

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();
}
Enter fullscreen mode Exit fullscreen mode
  • Elm's custom types are differentiated by the compiler at all times.
case pet of
    Fish fish -> fish.swim
    Bird bird -> bird.fly
Enter fullscreen mode Exit fullscreen mode

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

❤️ ❤️

Top comments (2)

Collapse
 
johanneslindgren profile image
Johannes Lindgren

Fantastic article! I've been adopting so many functional programming patterns in Typescript that I might just as well try out Elm.

Collapse
 
artydev profile image
artydev

You miss ELM in Javascript ?
Look at this : Derw

Regards