0%

A subjective comparison between TypeScript and Go

TypeScript and Go are my top two favorite programming languages at the moment. I use TypeScript day-to-day, and I have built several side projects with Go. In my opinion, each one of these two languages has certain strengths that the other one lacks. I’ll share my current thoughts about those certain strengths and weaknesses in this article.

As an overview, here’s what I’m going to particularly focus on when comparing TypeScript and Go:

  1. Error handling
  2. Encapsulation
  3. Arrays

These are the areas where I feel the biggest difference in satisfaction when working with TypeScript versus Go.

Of course, there are many other aspects that the two languages are different from each other. For instance, dealing with concurrency is substantially different in TypeScript compared to Go, yet I’m fairly happy with both concurrency paradigms. So I’m not really interested in talking about that no matter how big a difference there is.

Error Handling

Alright, let’s start with error handling.

I’ll just say it right away: compared to all other programming languages that I’m familiar with (Python, Java, C, C++, PHP), Go has the best error handling system, period.

To be fair, the conventional error handling systems in all of the above involve throwing errors/exceptions, with the exception of C (pun intended).

Now I won’t go on and on about how brilliant error handling is in Go. Instead, let me try to express my frustration when I have to do things like this in TypeScript:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
const foo = (bar: Bar): Baz => {
// I hate having to declare these upfront!!
let client: APIClient
let baz: Baz

try {
client = bar.getClient()
} catch (err) {
throw new Error(`could not get bar client: ${err}`)
}

try {
baz = client.fetchBaz()
} catch (err) {
throw new Error(`could not fetch baz: ${err}`)
}

// ...
// code that may throw other errors
// ...

return baz
}

First of all, I hate having to declare client and baz at the top and then eventually assign their values in the following try blocks.

Look how elegant the same code could have been written if TypeScript supported multiple return values like Go:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const foo = (bar: Bar): (Baz, Error) => {
const client, err = bar.getClient()
if (err) {
return null, new Error(`could not get bar client: ${err}`)
}

const baz, err = client.fetchBaz()
if (err) {
return null, new Error(`could not fetch baz: ${err}`)
}

// ...
// code that may return other errors
// ...

return baz, null
}

Second of all, TypeScript turns a blind eye when I don’t handle errors at all. In that case, the unhandled errors simply bubble-up and until they eventually get caught by some high-level try-catch block. And that’s how we end up with crappy messages in the error logs.

It’s also completely fine with TypeScript if try to handle all the possible errors in a single try-catch block like this:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
const foo = (bar: Bar): Baz => {
try {
const client = bar.getClient()
const baz = client.fetchBaz()

// ...
// code that may throw other errors
// ...

return baz
} catch (err) {
throw new Error(`something went wrong with foo, this is all I know: ${err}`)
}
}

This might be better than no error handling, but we still lose the fine-grained error messages that we had in the initial version.

To clarify, I’m not saying that TypeScript should have support for multiple return values like Go. After all, it’s built directly on top of JavaScript, which doesn’t support multiple return values. I’m just saying that error handling in TypeScript feels annoying and unsafe compared to Go.

Encapsulation

Before I begin talking about encapsulation, let me narrow down the topic a little bit:

  1. When I talk about encapsulation in TypeScript, I’m talking about server-side TypeScript in the Node.js environment in particular.
  2. I’m not a fan of classes and I rarely use them. Therefore, I’m interested in achieving encapsulation in a non-object-oriented codebase.

In TypeScript, there is no such thing as an internal package. Obviously, you can bundle your project as a package (with its own package.json and whatnot), but conventionally you don’t break up a single project into individual internal packages.

So let’s say your app has a directory called datetime which contains various date & time related utilities including format.ts, which is referenced by the other files in the datetime directory, but not from the outside. The directory structure in this case would look like this:

1
2
3
4
5
6
7
8
9
src/
├─ other_code/
│ ├─ ...
├─ datetime/
│ ├─ format.ts
│ ├─ week.ts
│ ├─ month.ts
│ ├─ ...
├─ ...

The problem is, you can’t limit the scope of format.ts such that it’s accessible only from within the datetime directory. In other words, datetime is not a “package”, it’s just a directory. Anything you export from datetime.ts not only will be visible from week.ts and month.ts, but also from everywhere else:
1
2
3
4
5
6
7
8
9
10
11
12
// format.ts

// not accessible from outside of this file
// not even from week.ts and month.ts
const foo = () => {
// ...
}

// accessible from everywhere
export const bar = () => {
// ...
}

The only workaround to this problem (that I can think of) is to put everything in one big datetime.ts file and only export the stuff that you would like to include in its public API. In other words, have exactly one .ts file for each package.

Go, on the other hand, does have a package-level encapsulation system. For the above example, we would have the same directory structure. But this time, we would be able to control the visibility of the datetime package’s contents from the outside:

1
2
3
4
5
6
7
8
9
10
11
12
13
// format.go
package datetime

// accessible from within the current package, e.g. week.go and month.go
// but not accessible from other packages
func foo () {
// ...
}

// accessible from everywhere
func Bar () {
// ...
}

It’s clear that Go has better encapsulation than TypeScript as it allows you to break up your package into as many files as you wish, while still allowing you to fully control what’s included in its public API.

Arrays

I think it’s safe to say that arrays (or slices in Go) are by far the most frequently used data structures in programming. Therefore, I think it’s very important that a programming language makes it easy to work with arrays. For instance, here are some of the most commonly used built-in array functions in TypeScript:

  • map
  • filter
  • reduce
  • find
  • includes

Whenever I have to use a for loop instead of one of the above in Go, I pause for a second and wonder why the fuck I decided to build that project in Go. I’m not kidding. For a moment, I forget about everything that’s great about Go and just wish I was able to use map.

So far I’ve been a bit harsh on TypeScript, but I love how it makes it so easy and pleasant to work with arrays, unlike Go.

Conclusion

TLDR; I think Go has far better error handling and encapsulation. On the other hand, working with arrays is such a breeze in TypeScript. That’s why I personally like TypeScript slightly better overall compared to Go, even though it wins 2 out of 3 rounds.