The first code example he uses immediately threw me for a loop. Why is he adding the return value of a function call to a function itself? bar() + bar????<p>function bar(someNumber: number)<p>{<p><pre><code> return foo(someNumber) + 1; </code></pre>
}<p>function baz(someNumber: number)
{<p><pre><code> return bar(someNumber) + bar;
</code></pre>
}<p>baz();<p>Moreover, baz is called with its only argument being undefined-- so the exception throwing code won't even throw, because it isn't 0. And _moreover_, this is typescript, so this would have thrown a type error since a required numeric argument is omitted.<p>This has so many problems. This is not a promising start to this article's correctness or thought-outtedness. Yet here it is at the top of hacker news...
I love this. It reminds me a lot of the work I did in Prism to port what I learned using Haskell in TypeScript: <a href="https://dev.to/vncz/forewords-and-domain-model-1p13" rel="nofollow">https://dev.to/vncz/forewords-and-domain-model-1p13</a><p>Reading the article, I do have some suggestion:<p>1. Never use isLeft or isRight; they break the composability of the monad instance — use `fold` instead, as late as possible<p>2. Do not use Enum in TypeScript — they have a runtime representation in JavaScript that's likely messing up a lot of things. Use string unions to accomplish the same result
This article is unconvincing.<p>From the article: <i>Compared to the first snippet, this one is harder to debug. It seems so because 1.) You don't know that callMe will throw an error or not. 2.) If it throws an exception you will not know what is the type of the exception. The more modular your code is, the harder it is to debug exceptions.</i><p>This is only a problem with unchecked exceptions as seen in TypeScript and C#. Checked exceptions are the solution.<p>The problem with error code return is tediousness:<p><pre><code> int foo() {
int result = bar();
if (result == ERROR_CODE)
return result;
int result2 = baz();
if (result2 == ERROR_CODE)
return result2;
int result3 = bazz();
if (result3 == ERROR_CODE)
return result3;
}
</code></pre>
Compare to the same code written in a language that supports checked exceptions:<p><pre><code> int foo() throws SomeException {
int result = bar();
int result2 = baz();
int result3 = bazz();
}
</code></pre>
In the first version the logic is interspersed with error handling, which makes the logic hard to see.<p>How do people using Go and other languages that don't have exceptions solve this reduced readability problem?
I resonated with the post as I'm in a similar position but I'm really sad that typescript chose just to be a thin layer on top of javascript.<p>The sentiment mostly comes from having the JS ecosystem mostly being untyped and having to interact with it.<p>That being said I tried io-ts but found it undocumented, missing examples and hard to write. For future libraries/projects I'm looking to try again ReasonML, tried in the past but had to write too many bindings.
IMO the `Either` construct should be avoided in JS because inevitably you'll either need to wrap everything in `try ... catch` anyway or you'll be pulling your hair out trying to figure out how to get them to work in an async/event context, in which case you'll end up re-inventing the Promise api (perhaps wrapped in an extra thunk to make it more fp-friendly or whatever).<p>A more practical approach is to work along the grain of JS while getting inspiration from fp. A common snippet would be:<p><pre><code> function maybeUrl(str) {
try {
return new URL(str);
} catch (err) {
return null;
}
}
</code></pre>
This is much more straight-forward and interoperable. Dealing with something like Left<InvalidUrlError> where you've created an entire error subclass and wrapping function for an unrecoverable failure that will be discarded is way overkill.<p>The unreliablility of thrown values in JS is a valid concern, but instead of trying to wrap all my code in a container construct, I simply wrap/convert the untrusted errors themselves if I need to use properties on an error reliably.
Don't write code like this... trying to fake sum types in an object oriented language ends up being a horrible, hard to maintain mess, similar to those examples on the original post.<p>Typescript's "discriminated unions" [1] make for incredibly inelegant looking code for anything but the most basic sum types, the ergonomics are nothing like the experience of using Haskell or OCaml.<p>I love sum types but in an OOP the equivalent implementation is the visitor pattern (if the problem calls for it). I was once starry eyed about "making invalid states irrepresentable". I even managed to introduce a code generator to bolt sum types into a production Dart app. My colleagues loved it (not) :-)<p>Thing is, programs have this annoying tendency of being incorrect no matter what technique you use :-), there's no silver bullet! If a pattern is natural for a given language, it is better to use that pattern than to force it to be something it isn't.<p>1: <a href="https://www.typescriptlang.org/docs/handbook/unions-and-intersections.html#union-exhaustiveness-checking" rel="nofollow">https://www.typescriptlang.org/docs/handbook/unions-and-inte...</a>
The first example code is rather off-putting, because it has multiple errors. The code would <i>not</i> throw an exception, because it produces compiler errors so you’ll never run it. (OK, so it still generates JavaScript that you could run, but I’m assuming you’re using TypeScript properly and won’t do so.)<p>• `return number + 1;` should be `return someNumber + 1;`. If you ignore errors and run the JavaScript, this means that it’ll throw a ReferenceError on this line.<p>• `bar(someNumber) + bar;` is adding a number (well, it should be a number, but you’ve omitted the return types, so the previous error will mean it infers `any` instead of `number`) to a function. This is a type error.<p>• `baz()` is called with zero arguments instead of the one it requires.<p>> <i>When baz is called it will throw an uncaught exception. By reading the code you know that foo throws an Error. Debugging this snippet is a piece of cake, right?</i><p>As mentioned, this code doesn’t compile, but if you ignore that and run it anyway, then the uncaught exception is a ReferenceError, <i>not</i> the error you see on a throw line. I… don’t <i>think</i> this is what was intended. This also demonstrates what I consider a bit of a weakness of the exception system in such languages: various programming errors that a compiler should catch come through as runtime errors instead.<p>(I’d generally prefer to use exceptions to indicate <i>exclusively</i> programmer errors, and basically never use a try/catch block, but too much is designed around treating exceptions as normal parts of control flow to be able to do this in most cases. I like the Result/panic split in Rust, where panic <i>always</i> means programmer error, and Result is for probably-exceptional cases, but where they’re still generally expected and part of the design rather than programmer error.)<p>If you fixed only the first error, you’d get a value like "NaNfunction bar(someNumber) {\n return foo(someNumber) + 1;\n}" out of the baz() call. Fun times.
Reminds me of a pattern I've wished would take off in TS, but hasn't (yet):<p><pre><code> const result = await fetchData().catch(e => e)
if (_.isError(result)) {
return "some failure"
}
return (await result.json()).foo
</code></pre>
TS is smart enough to know whether result will be of type Error or the Response type.<p>This pattern should replace most Either cases when the left is an error state of some kind. Also should replace exceptions and promise rejections.<p>You can also analogously replace a Maybe type with nullability, simply `type MaybeFoo = Foo | null`.
Tangent: This article made me think deeper about programming languages in general and I found it fascinating. So I went to subscribe to his blog in my feed reader, but alas, no RSS feed. :-(
You don't actually need the tag on Either. This is how I defined my Result type. The shape is enough to discriminate them.<p><pre><code> type Result<S, E> = Success<S> | ResultError<E>;
type Success<S> = { value: S };
type ResultError<E> = { error: E };</code></pre>
I'm very unconvinced considering the section on avoiding exceptions doesn't discuss async code at all. I mean, a returned Promise from an async function is very similar to the author's "Result" object where you can even pass a success handler and a rejected handler right in the single "then" method. That said, I greatly prefer instead to await on an async function and handle any rejections in a catch block.
Please please please don't perpetuate the use of either or maybe types :(<p>They make software very hard to maintain and end up breaking anyone who depends on a library that uses them.<p>Rich Hickey (author of Clojure) has a great discussion of Either and Maybe here: <a href="https://www.youtube.com/watch?v=YR5WdGrpoug" rel="nofollow">https://www.youtube.com/watch?v=YR5WdGrpoug</a> that dives into their unsoundness.
With JS and Typescript, I find myself going back to logic error handling in the catch block, because there is only one Error type. You can't have multiple types of exceptions. So you have to set the error.type attribute and then do logic.
Why do you need Either when you have union? All the left types subclass Error so you can easily separate that from the successful value as well. Seems rather a dogmatic choice.
Does anyone else hate foo(), bar() and baz() as example names. They are meaningless - except communicating that its a programmer selected name. Even func1(), func2() would be better.