
From Rust to TypeScript - valand
https://valand.dev/blog/post/from-rust-to-typescript
======
aerovistae
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????

function bar(someNumber: number)

{

    
    
      return foo(someNumber) + 1;  

}

function baz(someNumber: number) {

    
    
      return bar(someNumber) + bar; 
    

}

baz();

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.

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...

~~~
ByteJockey
In JS it's the function would be converted to a string representation of its
code and concatenated to the result of `bar(someNumber)`.

Some quick playing with ts-node says that the same thing should happen,
depending on the return type of bar. Given the definitions above, it should
throw a TypeError, but if bar returned a string, I think it would still behave
as I said.

~~~
formerly_proven
I was unsure if this comment was sarcasm so I tried and concatenating a
function and a string in JS _actually_ results in the source of the function
being concatenated with the string.

~~~
zamadatix
<rant> This whole "wow I thought implicit type coercion in JS was a joke"
style comment got old 10 years ago. If you look at where JS came from (the 90s
browser) and what it tried to do there then you should be left with few
surprises looking it JS code snippets. If you don't understand what a language
was designed for and how it evolved how can you be surprised at the results of
code snippets? Because it's not the same as every other language? What would
the point of the new language be then?</rant>

~~~
adchari
It’s not that the language is different, it’s that the default is stupid. It’s
much more likely that if I’m trying to concatenate a string and a function,
I’ve mistyped something, not that I want the source code of the function.

You can always include some additional function which takes a function as an
argument and prints the source code as a string for the 4 people in the world
who rely on function to string coercion, why is it a default behavior with no
visibility?

I get it, JS is different from C or C++ and every other language but we call
out poor design decisions by comparing them to expectations fostered by other
languages

~~~
zamadatix
Again how can you say it's an outrageous default if all you reference is what
you think it should have done from a generic language PoV not looking to
understand why it does it that way?

Javascript came in as a scripting language for working with the untyped string
content of a browser so you get dynamic duck typing where it tries to make the
types work (generally towards string since that's what the browser had)
instead of having the scripter trying to make some simple web content dynamic
do more type checks than actual code. In JS functions are just objects (like
most everything) and objects. Combine the above so when you say object + "foo"
it coerces object to string and it shouldn't seem outrageous just not what
you're used to.

Now a fair question would be "Why doesn't TS consider this an error out of the
box".

Also for those annoyed by this in places JS wasn't originally designed to go
(heavy app logic, server backends, desktop applications) you might want to
look at the no-implicit-coercion rule in ESLint or similar.

------
XVincentX
I love this. It reminds me a lot of the work I did in Prism to port what I
learned using Haskell in TypeScript: [https://dev.to/vncz/forewords-and-
domain-model-1p13](https://dev.to/vncz/forewords-and-domain-model-1p13)

Reading the article, I do have some suggestion:

1\. Never use isLeft or isRight; they break the composability of the monad
instance — use `fold` instead, as late as possible

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

~~~
no_wizard
I really wish the TS team would have used frozen null prototype objects for
enums instead. Feels like it would have been a perfect fit.

With that said I am curious why you think one shouldn’t use enums. I know it’s
a “factory” IIFE like namespaces at runtime but otherwise I don’t understand
why it’s undesirable?

~~~
XVincentX
My main issue with Enums in Typescript:

1\. Because they have a runtime representation, I cannot do `import type` —
and I am importing the entire file (which might have a side effect). While
it's still not justified (ideally importing should not do anything) the
reality in JavaScript is different.

2\. I can swap the enum value with a number or a string and it would still
work, even if invalid. See
[https://www.typescriptlang.org/play?#code/KYOwrgtgBAggxgFwJY...](https://www.typescriptlang.org/play?#code/KYOwrgtgBAggxgFwJYHsRQN4CgpQKIByMAQgDJ5QC8UADADQ5QAiAkgMonlVQCMWAvliwATYHAA2AQwBOwKADMwIRKnTCUbBGHnyAFAHcAFpIQAuWCrQBKcwCMUKccEkgA3EPWbte+MjQA6Qk48K1coAHpwqAB5AGsRDS0dXV9Vf1YOMhCwyJiAaQSvZJ4aUIioghQEKFlJcXEATyg4B3EhUEgoTWkkEABzVLRMRiCs7gAiPBBJWydxhlwM4ImmJABnGbmBIA)
for an example

3\. On the other hand, I cannot use strings to create an Enum
([https://www.typescriptlang.org/play?#code/KYOwrgtgBAygLgJwJY...](https://www.typescriptlang.org/play?#code/KYOwrgtgBAygLgJwJYgOYEEDGckHsRQDeAUFFAKIBy6AQgDLlQC8UAROSAIYBGANsKwA0pKABEAkjFoNmbUUgDOPfq2IBfYsUz4FcKJwBcsRCgzY8BFuy58BUAPT2olXHs69euAO7AAJkA))
— this is the exact opposite of n. 2

4\. Duplicates are possible:
[https://www.typescriptlang.org/play?#code/KYOwrgtgBAIghgTwPI...](https://www.typescriptlang.org/play?#code/KYOwrgtgBAIghgTwPIDMDqxgGsoG8BQUUAymCACaIA0hUAsgPYXW0AqYwAzpQjURuRBcefKKwAWYAE7dEUALxQATKIBiUgJYjaxOABdpPfAF8gA)

~~~
nidu
1\. You can define enum like `const enum A {...}` and they will not have
runtime representation.

~~~
Jasper_
const enum doesn't work when in transpileOnly mode (e.g. webpack, Parcel), and
Babel treats "const enum" the same as "enum". I'd really love a non-broken
const enum in TypeScript, though.

~~~
XVincentX
I'd love to see them out of the language. I do not have evidence/document
supporting this, but I think it was ultimately a failed attempt of copying the
C# feature ignoring the fact that JavaScript works differently.

------
flowerlad
This article is unconvincing.

From the article: _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._

This is only a problem with unchecked exceptions as seen in TypeScript and C#.
Checked exceptions are the solution.

The problem with error code return is tediousness:

    
    
      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;
      }
    

Compare to the same code written in a language that supports checked
exceptions:

    
    
      int foo() throws SomeException {
         int result = bar();
         int result2 = baz();
         int result3 = bazz();
      }
    

In the first version the logic is interspersed with error handling, which
makes the logic hard to see.

How do people using Go and other languages that don't have exceptions solve
this reduced readability problem?

~~~
dilap
Go does indeed use the tedious approach. Rust and Zig have nicer syntax for
it.

But personally, I prefer even Go's "tedious" approach to (unchecked)
exceptions, simply because it's too easy to miss an error case otherwise.

I also find that knowing where errors occur is important information, so I
don't really mind having it add an extra line or two. (But the more minimal
Rust/Zig syntax is nicer.)

(This attitude comes from having once worked on a python server application,
which involved a lot, "oh shit, that throws" whackamole.)

~~~
flowerlad
> _" oh shit, that throws" whackamole_

You don't have that problem in languages that support checked exceptions.

~~~
ufmace
I've never seen a language that supported checked exceptions and actually used
them consistently for everything.

But then that would be kind of awful, because IMO the checked exceptions cases
for "I know this will never actually throw, leave me alone" and "This is just
a quick script, go ahead and blow up the whole program if this fails" are
poor, forcing you to write try catch blocks all over the place.

IMO, Rust has a nice approach for being stricter - unwrap. If it's a quick
script, throw unwrap everywhere with reckless abandon. For real programs, grep
for unwrap and make sure for every one, you really know for sure that it will
never fail or that blowing up the whole program if it does is the right move.
To convert from one to the other, grep unwrap and actually think about what
you want to happen if that line is Err.

------
gregopet
Sure, people say they want to have to check for _every_ possible error, but
then Java gets endless heat for having checked exceptions.

~~~
abraxas
Yes, this schizophrenic attitude drives me bonkers. Immature JS kids at my
company love to make noises about Java’s verbosity yet spend hours debugging
and troubleshooting most mundane errors because seemingly nothing is ever
enforced by the JS interpreter.

My theory is that nowadays Java is unpopular mostly because it is their
parents’ programming language. That’s reason enough for these twenty
somethings to despise it without even knowing the first thing about it.

~~~
ragnese
Hold on, though. There is plenty to dislike about Java. Everything can be
null, interfaces have to be implemented at definition, broken array type
variance, no const/mutable at the language level, no value types, etc...

~~~
abraxas
No question there is a lot to dislike about java but there are also things it
got mostly right or better than competing languages of the day. Checked
exceptions is in my opinion one of those.

~~~
ragnese
I agree with that. I was heavily involved on the other thread someone linked
to about Java's checked exceptions. Until that thread, I thought I was the
only one who didn't accept that they were a mistake.

------
taosx
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.

The sentiment mostly comes from having the JS ecosystem mostly being untyped
and having to interact with it.

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.

~~~
valand
Author here. I used io-ts when the documentation is still on its root
README.md. Apparently it has been moved to [https://github.com/gcanti/io-
ts/blob/master/index.md](https://github.com/gcanti/io-ts/blob/master/index.md)

------
rmrfrmrf
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).

A more practical approach is to work along the grain of JS while getting
inspiration from fp. A common snippet would be:

    
    
       function maybeUrl(str) {
         try {
           return new URL(str);
         } catch (err) {
           return null;
         }
       }
    

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.

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.

~~~
rattray
Totally agree.

In cases where the error type matters, I like to return rather than throw the
error.

    
    
        type Success = string
        type Result = Success | BadInputError | NotFoundError | IncalculableError
    
        const foo = (input: I): Result => {
          //...
       }

------
emmanueloga_
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.

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.

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) :-)

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.

1: [https://www.typescriptlang.org/docs/handbook/unions-and-
inte...](https://www.typescriptlang.org/docs/handbook/unions-and-
intersections.html#union-exhaustiveness-checking)

~~~
moomin
The joke is, OO language have sum types, albeit incredibly broken ones,
because any set of classes that inherits from the same base has the base as a
sum type.

You can surface this by implementing the visitor pattern.

------
chrismorgan
The first example code is rather off-putting, because it has multiple errors.
The code would _not_ 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.)

• `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.

• `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.

• `baz()` is called with zero arguments instead of the one it requires.

> _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?_

As mentioned, this code doesn’t compile, but if you ignore that and run it
anyway, then the uncaught exception is a ReferenceError, _not_ the error you
see on a throw line. I… don’t _think_ 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.

(I’d generally prefer to use exceptions to indicate _exclusively_ 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
_always_ 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.)

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.

~~~
valand
Author here. Caught red-handed not running it on playground. Thanks for the
correction.

> I’d generally prefer to use exceptions to indicate exclusively programmer
> errors

The idea that panic/exception means programmer errors worth to be noted. It
think they should be intended for errors that is intended to be unrecoverable.
Still, catching errors on runtime is not fun

------
rattray
Reminds me of a pattern I've wished would take off in TS, but hasn't (yet):

    
    
        const result = await fetchData().catch(e => e)
        if (_.isError(result)) {
          return "some failure"
        }
        return (await result.json()).foo
    

TS is smart enough to know whether result will be of type Error or the
Response type.

This pattern should replace most Either cases when the left is an error state
of some kind. Also should replace exceptions and promise rejections.

You can also analogously replace a Maybe type with nullability, simply `type
MaybeFoo = Foo | null`.

~~~
tym0
I agree with Maybe not being that useful in TS but the point of an
Either/Result type is that the caller is gonna handle it each branch
differently. In your example the caller has no way to tell the difference
between an error and the succesful return value.

~~~
rattray
Perhaps I was unclear, the implicit typing is so:

    
    
        const result: FetchResult | Error = ...
        if (_.isError(result)) {
          // Here TS knows that result is of type Error
          return
        }
        // Here TS knows that result is of type FetchResult
    
    

On my phone, apologies for shorthand

In any case, with this pattern TS can easily tell the difference between
success and error (or left and right or whatever) as long as the error / left
case is a class of a different type than the success / right case.

Tldr, where relevant, classes are cleaner than tagged disjoint unions in TS.
Errors are one situation where this is relevant.

------
judah
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. :-(

~~~
valand
Author here, thanks for the input. Forgot to put it in my backlog :pepesad:

~~~
valand
Rolled out!

~~~
judah
Thanks! Subscribed.

------
unnouinceput
You can write bad code in any language, Rust included. This article is cherry-
picking.

~~~
bitxbit
I think it’s best to stay language agnostic especially in 2020. I try to think
data-in data-out and use the best tools available for clean execution.

~~~
azangru
> and use the best tools available

How do you know which tools are the best?

~~~
matt_kantor
0\. Define what problem you are trying to solve, make sure you understand the
domain.

1\. Come up with a set of options by doing research, talking to people who
have solved similar problems before, etc.

2\. Rank them by some criteria (e.g. availability of libraries, documentation,
raw performance, hireability, support, quality of tooling, etc).

3\. Do some prototyping in your problem domain using the top 2-3 (more if you
aren't confident in your ranking) and pick whichever you like best.

4\. Always be willing to change your mind down the road. Avoid the sunk cost
fallacy and try not to lock yourself in too much until you're confident in
your choice.

------
tym0
You don't actually need the tag on Either. This is how I defined my Result
type. The shape is enough to discriminate them.

    
    
        type Result<S, E> = Success<S> | ResultError<E>;
        type Success<S> = { value: S };
        type ResultError<E> = { error: E };

~~~
valand
Author here. Did this too. I found some quirks (rather irrelevant) while
compiling using TypeScript 3.6-ish.

``` const { value, error } = result; if (error) return doSomething(); if
(value) return doSomethingElse(); ```

^ You can't do this because value or error might be not an attribute of result

Nowadays I will just `npm install fp-ts` and use them.

~~~
tym0
You're right that you can't destructure until you've established which side of
the union you got, does the tag help with that?

I like a Result type over an Either because it feels semantically more
meaningful. I work with people with a wide range of background and a Result
type is self explanatory, they can just read the code by themselves without
knowing and understanding the convention of which side of the branch the error
goes, etc...

~~~
valand
I agree with you on the Result type being semantically more meaningful and I
agree there are a lot of conventions to be remembered coming from functional
programming that gets meta.

Thanks for the insight

------
hn_throwaway_99
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.

~~~
choward
Exceptions aren't typed though so you either have to decode it in some way or
treat every exception the same.

------
tantaman
Please please please don't perpetuate the use of either or maybe types :(

They make software very hard to maintain and end up breaking anyone who
depends on a library that uses them.

Rich Hickey (author of Clojure) has a great discussion of Either and Maybe
here:
[https://www.youtube.com/watch?v=YR5WdGrpoug](https://www.youtube.com/watch?v=YR5WdGrpoug)
that dives into their unsoundness.

~~~
mattnewton
Rick’s argument seems to be that we should have better tools in the language
type system. If your language doesn’t give you those tools, these are still
very useful types. In addition, the library maintenance issues he raises seem
trivially solvable with languages that allow function polymorphism or have
refactoring tools to me.

------
aussieguy1234
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.

~~~
MobiusHorizons
It is worth noting that you can throw anything you want. It doesnt' have to be
new Error(). There are definitely some drawbacks to this, such as if the
errors thrown by your dependencies are of type Error, but the upside is that
as long as you are consistent, you get a much richer set of types available.

IMO this also works primarily because errors are very rarely thrown by native
APIs, which instead encode failure into the return type. For this reason, you
can typically have a reasonable level of control over what code can reasonably
be expected to throw.

------
nsonha
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.

------
ape4
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.

~~~
jpxw
f(), g() and h() are decent alternatives.

Of course, the real solution is to come up with an reasonable example scenario
with meaningful names.

~~~
hu3
a, b, c seems more intuitive

------
sdwvit
Well exceptions are generally a sign that code smells. Instead of throwing an
exception, just return the default value.

~~~
otabdeveloper4
> 1 / 0 just returns 0, #yolo

No thanks.

~~~
sdwvit
Returns infinity like it should

------
auggierose
Stopped reading after the exception / result stuff. You can have both, both
are useful, learn yourself some Swift maybe.

~~~
lioeters
I can see the author's point, that a throw behaves awfully like a GOTO. It can
add complexity and unpredictability to the code path, especially dealing with
external modules.

On the other hand, there are situations in which throw/catch provides a
simpler, even elegant way to handle errors.

So I'd agree with you that both are useful.

The author makes a good point though, and I will consider it next time whether
returning errors may be more suitable, to be clear and explicit/verbose,
without requiring the GOTO-like control flow.

~~~
libria
Another place throws are inconvenient is chaining/streams.

Pseudocode:

    
    
        var goodStuff = listOfStuff.map(x => transform(x))
            .filter(t => typeof(t.result) != Error)
            .map(y => handleGoodData(y))
    

If `transform()` threw exceptions, this whole thing aborts. But if it can
return an `Error` type, we can continue processing the good ones and log the
bad ones (here I ignore them for simplicity).

