DEV Community

Cover image for Haskell as an alternative to TypeScript
digitallyinduced
digitallyinduced

Posted on • Updated on • Originally published at ihp.digitallyinduced.com

Haskell as an alternative to TypeScript

If you've been using TypeScript for your web development, you have understood the value that static typing brings to your productivity. However, TypeScript still has some issues in regards to type safety. This article is supposed to show these issues and help you see how another statically typed language - Haskell - is solving them. In the end, I hope that you will have learned something, and might be interested in continuing your journey into type-safe programming by trying out Haskell for your next project. If you choose to do so, I recommend using IHP, a productivity-focused web framework with which you can build full-stack web-apps (but APIs work just fine as well).

The anti-pattern that is any

The any type included in TypeScript can cause many issues, as it straight up disables typechecking for whatever it's used for. Once you use it, you might as well write plain JavaScript. But it might actually be worse than that: people might be highly confident in their refactoring or new code if they don't get any type errors. What they might not notice is that an any somewhere else in the code where they didn't change anything is not throwing a type error, although it would if the correct type were used.

So why would anyone use any? I think there's a few different reasons:

  1. the correct type is not known to the developer at the time of writing,
  2. the correct type would be too complex, or
  3. it's implicitly used by TypeScript because it can't figure out the correct type by itself (this one can be disabled using the noImplicitAny setting)

Now, if the correct type is not known by the developer, that could either mean it'd be useful for them to do some research into what it actually is, or, more likely, the type cannot be known at that point in time. In any case using TypeScript's unknown type is much better, as that is a type-safe way of accepting any value, that also disallows any interaction with the value that might be problematic. This is the type to use in case of migrating an existing JS codebase to TypeScript, as it keeps the guards of TypeScript intact.

If the correct type is too complex for the developer to figure out or to bother typing, and TypeScript isn't able to do so either, there's little you can do except use unknown again, which definitely isn't ideal. The developer probably already understands things about the type, but using unknown means that TypeScript will not, which will just lead to unnecessary checks.

In case Haskell was used instead, the likelihood of Haskell figuring out the type by itself (this is called type inference btw) is much higher. This is because in Haskell any isn't even an option, so it can't confuse the compiler. But also, ghc - the most widespread Haskell compiler - has years and years of effort behind it. And lastly, as long as you stick to basic Haskell, it is specifically designed to enable type inference.

Limitations of being a super-set of JavaScript

TypeScript has the core design-goal of being a super-set of JavaScript. What that means is that any JavaScript code is valid TypeScript code as well (if the transpiler is configured to accept it), and you only need to add code to add more definitive types to the code. This is great if you need to convert an existing codebase from JavaScript to TypeScript, as it allows you to work in very small increments.

The problem is that new projects built using TypeScript still suffer from many of the problems of JavaScript:

  • difficult-to-understand behavior that doesn't behave like in other languages (for example the this keyword)
  • many things on https://wtfjs.com/ ...and probably more.

So while I applaud converting an existing JavaScript codebase to TypeScript, I would never start a new project in either if there's good alternatives, which there always are for backend development. Also, using the right tools the need for a lot of frontend code can be eliminated, while maintaining a highly-interactive webapp. Check out IHP for my favorite solution right now.

Being a super-set of JavaScript, many type definitions are a lot more complex to write than they would otherwise need to be as well. Check out the following examples, which are type definitions for equivalent Haskell and TypeScript code:

// A function that takes a tuple of two values and returns the first:

// TypeScript
type first = <A, B> ([a, b]: [A, B]) => A

// Haskell
first :: (a, b) -> a
Enter fullscreen mode Exit fullscreen mode
// a map function for lists/arrays

// TypeScript
type map = <A, B> ((a: A) => B, list: A[]) => B[]

// Haskell
map :: (a -> b) -> [a] -> [b]
Enter fullscreen mode Exit fullscreen mode

TypeScript can't type everything

If you're using TypeScript you see the advantages of having static typing available, so we don't need to go over them. As such, it should bother you whenever you cannot actually use static types to encode useful information. Here are a few cases in which TypeScript cannot actually fulfill its promise of type-safety, and the way that Haskell solves it.

Side-Effects

If you've been following recent trends in software development, especially in the JavaScript world, you know that functional programming principles are becoming more and more popular. React is built on them! One of the things people are advocating for are "pure functions": functions that, given the same arguments, always return the same result and that don't have side-effects. What makes these functions so great is that they are very easy to reason about, and there are various techniques for performance optimization that are much easier using them, if not impossible without them.

When coding in TypeScript, figuring out whether or not a function is pure requires you to read its source code and figure out how it works, which might lead to other functions that you have to do the same thing for. This gets tedious and could easily be avoided if the information of function purity was included in the function's type.

In Haskell, this is done by making every function pure by-default. Whenever you need to have a side-effect in a function, you need to declare this on the type-level. Not doing so is a compiler error.

Failures

As long as everything works as expected, TypeScript works just fine. However, when something goes wrong, you will likely have to interact with an error that gives you more information. Before you can even do so though, you need to be aware that an error can occur - and this is one of the shortcomings of TypeScript. It is impossible to declare on the type-level that a function might throw an error. This means you still need rigorous testing to figure this out, and even then you might run into unhandled errors in production due to not having tested thoroughly enough.

In Haskell it is most common to force handling errors by making the return type include the information of whether or not the function succeeded or not. Depending on the actual function, you might get no, some, or a lot of information in case of a failure, but you can be sure that you are handling these cases. The two most common ways of representing the option for failure are the Maybe and Either types.

The Maybe type can represent success and failure by containing the success value in the success case, or Nothing in the case of a failure. This is also basically Haskell's way of handling null and undefined, which prevents from using these values in a way that they don't support, which might cause issues.

The Either type works in a very similar way: in the case of a success it simply contains the value that the function would return anyways. But in case of a failure it contains some other value. The type of this is clearly defined, which means that there's no ambiguity in how to handle it.

What's best about these techniques is that it does not only allow you to handle the error case in a type-safe fashion, it forces you to handle error cases where they might occur. If the function doesn't force you to handle the error case, no error can occur.

One place where typing the failure state should clearly be possible are promises, however TypeScript has no default way of doing this, which means that in case you run into a failure in your asynchronous code, TypeScript is not going to help you in figuring out the value you got in place of the success value.

Conclusion

TypeScript is a great way to improve an existing JavaScript codebase, and to bring a bit more productivity and safety to your frontend code. However, the language has issues, some of which are so ingrained in the language itself that they are not going to be solved with time. So whenever you can, try opting for a language that has less design issues at its core, such as Haskell, and avoid writing JavaScript/TypeScript wherever possible.

To allow you to do this, we at digitally induced have written IHP - an opinionated full-stack web framework based on Haskell, with a focus on developer experience and productivity, without sacrificing user experience. To try it out for your next full-stack project or backend, get started using the Guide and join our community forum or active slack workspace to get any support you might need. We try to help wherever we can.

Do you have any thoughts on TypeScript that you want to share? Did I miss anything? I'd love to hear your opinions and thoughts!

Discussion (20)

Collapse
leob profile image
leob • Edited

But can it also be used for the front end (client side, e.g. through Web Assembly? same solution as used by Rust), or are you only talking about replacing JS/TS for the backend (node.js)? That didn't really become clear to me from your article.

I think Haskell has a lot of similarities with Rust, and both can be used in the same scenarios, to replace JS/TS.

Collapse
digitallyinduced profile image
digitallyinduced Author

I personally don't have experience with compiling Haskell to Web Assembly. There seems to be a compiler for this based on ghc here: github.com/tweag/asterius

The easiest for an existing frontend infrastructure is to use Haskell as a replacement for a nodejs backend.

If you want to take a look at IHP, the solution we're doing there is to put as much logic and code on the server, and to use tools such as the included autoRefresh to still achieve highly interactive webapps. You can read more about autoRefresh here: dev.to/digitallyinduced/displaying...

Rust certainly has similarities to Haskell, so if you like Rust, I highly recommend checking out Haskell too, as the differences will at the very least be a good learning opportunity.

Collapse
leob profile image
leob • Edited

Right, AutoRefresh, this reminds me of old TurboLinks of Rails fame, and more recently of some other "auto Ajax" frameworks which provide for easy and automatic dynamic updates without having to go all out on SPA + API. Can't remember the name of those tools right now, but this definitely seems a trend and a recurring theme recently.

Thread Thread
digitallyinduced profile image
digitallyinduced Author

You might have heard of Phoenix' LiveView. That's certainly very similar.

It's a great pattern - if you haven't used it before I highly recommend trying it out. When I first heard about it I was quite skeptical of whether it could compete with a SPA, but it has certainly convinced me after trying it out.

From the user's point of view there's no difference to be felt really, and as a developer it is SO MUCH nicer.

Thread Thread
leob profile image
leob

You're right ... LiveWire (of Laravel fame) is another one, this really seems to be a popular trend, and for good reasons.

Collapse
leob profile image
leob • Edited

I've looked at Haskell in the past, and then at Rust more recently, and I couldn't help but notice how many similarities there are between them (at a conceptual level), even though Rust isn't strictly an FP language.

Thread Thread
digitallyinduced profile image
digitallyinduced Author

There certainly are many similarities. Probably the most popular are the Optional/Maybe and Result/Either types that I also mentioned in the article.

It'd be cool if you could declare functions as using IO/not using IO in Rust too - I think that is a great feature, which has often meant that debugging behavior and making sense of someone else's code got much easier. Though Rust's focus on lower-level programming (compared to Haskell at least) might make it less useful. I don't have much experience with that though - I've always focused on higher-level.

Thread Thread
leob profile image
leob • Edited

Yes indeed, the way "optional" values are treated - same story for errors/exceptions, both Haskel and Rust have no try/catch, errors are propagated via return values. Rust also seems to utilize "algebraic data types" in the same fashion as Haskell does, whereas other languages would use more of an OO approach with classes/objects. Following along with the Rust tutorial I constantly thought "deja vue" and then "Haskell" !

And both are impressive in how much checking the compiler does for you - if used properly you can achieve a large degree of confidence that your code "does the right thing" even before running it.

The only thing Rust seems to be missing is tail recursion, this also stops it from being seen as a "pure" FP language, typically you'd still write imperative/procedural loops in Rust.

Collapse
luismed15971068 profile image
luis medina

I am new to this program, I am learning Haskell for several months, I understand that Rust was very inspired by Haskell. I like both (especially the Syntax of the Rust Lambdas 🤤). Excuse me My English please

Collapse
leob profile image
leob

Yes there are many similarities between Haskell and Rust, and not just superficial ones, it runs pretty deep!

Thread Thread
luismed15971068 profile image
luis medina

Do you know any project where I can practice rust on a personal level?, I mean something that I can show in the future

Thread Thread
leob profile image
leob • Edited

Sorry no nor really, I was involved in a project which used Rust long ago, but all I did was work through the Rust manual, the project fell by the wayside and I didn't actually build anything ... well I'm sure there's plenty of project ideas around if you google it a bit, using it on the client with Web Assembly seems popular, but Rust is of course also great as a server side language. I don't know, maybe try to build a chat server and client with it?

Thread Thread
luismed15971068 profile image
luis medina

Well, the language I chose to learn in depth is haskell, I heard that the best way to learn in any science is by working on it, that is why I am looking for projects with which to practice 1st Haskell 2nd rust.

Thread Thread
leob profile image
leob

Well, like I said you could try to build a chat app, backend and frontend, that sounds like a nice challenge and should keep you off the street for some time, haha ... and maybe have a look at this link:

users.rust-lang.org/t/recommended-...

Collapse
lucamug profile image
lucamug

Nice post, thank you for writing it!

Collapse
digitallyinduced profile image
digitallyinduced Author

Thank you for appreciating it!

Collapse
luismed15971068 profile image
luis medina

Has anyone used Yesod framework ?, I want to start a personal project to practice my haskell skills.

Collapse
digitallyinduced profile image
digitallyinduced Author

Hello Luis, seeing as you want to start learning Haskell (which is great!), I would suggest you start working through IHP's getting started guide (ihp.digitallyinduced.com/Guide/ind...), with which you will build a simple blog website using Haskell. You can then go and build any other website you want with the foundation that you learn there.

You can try building a similar project with Yesod, though we specifically built IHP for people to be more productive faster, and have an easier time getting started than if they were to use Yesod for example.

Good luck on you journey!

Collapse
luismed15971068 profile image
luis medina

Thank very much

Thread Thread
digitallyinduced profile image
digitallyinduced Author

No problem!