by Jannis Jorre, 11.08.2021
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 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:
- the correct type is not known to the developer at the time of writing,
- the correct type would be too complex, or
- it's implicitly used by TypeScript because it can't figure out the correct type by itself (this one can be disabled using the
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.
- difficult-to-understand behavior that doesn't behave like in other languages (for example the `this` keyword)
- many things on https://wtfjs.com/
// A function that takes a tuple of two values and returns the first: // TypeScript type first = ([a, b]: [A, B]) => A // Haskell first :: (a, b) -> a
// a map function for lists/arrays // TypeScript type map = ((a: A) => B, list: A) => B // Haskell map :: (a -> b) -> [a] -> [b]
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.
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.
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 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
undefined, which prevents from using these values in a way that they don't support, which might cause issues.
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.
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!