TypeScript Ergonomics

One of the things I really enjoy about working with React Native is that I get to use TypeScript, and I’d like to share a little bit about why I find the language so fun to work with. In this article, I want to introduce you to TypeScript’s union types. We will compare the ergonomics of TypeScript unions with the ergonomics of Swift enums with associated vales. Ergonomics are how the languages “feel” to use – to a large degree, ergonomics are subjective. But the comparison should still be interesting, even if you disagree. So let’s dive in!

An important thing to understand about TypeScript is that its goal has been to add a strong type system on top of an existing language: JavaScript. Because JavaScript is loosely typed, TypeScript needed to meet JavaScript where it was at, with all its weird APIs and patterns. As a consequence, TypeScript has a really fun and flexible type system. TypeScript structural types, whereas Swift uses nominal types. Both are fine approaches, but structural typing has become my go-to way to think about types, generally.

Let’s consider a function that looks for a number in an array and returns its index if found. I’ll implement the function in both languages. We could get fancy with these functions, but I’m going to keep them simple so we can focus on comparing unions and enums with associated values.

Here’s what we might write in Swift:

// Swift

let primes = [1, 2, 3, 5, 7, 11, 13]

func findElementIndex(_ element: Int, inArray array: [Int]) -> Optional<Int> {
  for (index, value) in array.enumerated() {
    if (element == value) {
      return index
    }
  }
  return nil
}

findElementIndex(5, inArray: primes) // Returns 3

And now in TypeScript:

// TypeScript

const primes = [1, 2, 3, 5, 7, 11, 13]

const findElementInArray = (element: number, array: number[]): number | undefined => {
  for (const [index, value] of array.entries()) {
    if (value === element) {
      return index
    }
  }
  return undefined
}

findElementInArray(5, primes) // Returns 3

I want to draw your attention to the return type of each function. In Swift, we represent an index that might be missing as the Optional<Int> generic (you could also use Int? as a shorthand). In TypeScript, we use number | undefined. This is a union type, and it literally means “number or undefined.”

Union types are one of my favourite parts of TypeScript 😊 Let’s look at a more complex example.

Consider another function that needs to return either a string or a number. The implementation itself doesn’t really matter, it’s the return type we care about. When Swift needs to represent a potentially missing value, we use the Optional enum. But there’s no built-in Swift enum for “string or number”, so we’ll need to create our own with associated values.

// Swift

enum NumberOrString {
  case number(Int)
  case string(String)
}

func sillyFunction(_ input: Int) -> NumberOrString {
  if (input % 2 == 0) {
    return NumberOrString.string("Hello, world!")
  } else {
    return NumberOrString.number(1337)
  }
}

sillyFunction(10) // Returns NumberOrString.string("Hello, world!")
sillyFunction(11) // Returns NumberOrString.number(1337)

There’s a lot of boilerplate here. Not only do we need to define our own enum, but we need to unbox the values to use them afterwards. That is to say, the return value of sillyFunction(10) is not "Hello, world!" but rather an enum NumberOrString.string("Hello, world"); to access the associated value, we need more syntax. You would need to do something like:

// Swift

switch sillyFunction(10) {
case let .number(number):
  print("The number was \(number)")
case let .string(string):
  print("The string was \(string)")
}

That’s… kind of a lot. And I can never remember the syntax on the first try 😅

Let’s compare with a TypeScript implementation.

// TypeScript

const sillyFunction = (input: number): number | string {
  if (input % 2 === 0) {
    return 'Hello, world!'
  } else {
    return 1337
  }
}

sillyFunction(10) // Returns 'Hello, world!'
sillyFunction(11) // Returns 1337

In the TypeScript implementation, we don’t need to declare an enum. We declare an inline union type, number | string, but that’s it. Our return values are also not boxed – we can access them directly as number or string without any extra syntax. But that syntax is important, isn’t it? I mean, the syntax exists in Swift to distinguish between the two possible types, and I haven’t shown how that would be done in TypeScript.

Here’s where things get really interesting…

So far, our union types are quite straightforward. “This thing, or that thing”, basically. Next, we’re going to take a look at a more real-world example. We’re going to implement a function that returns a Result type.

Result is a generic enum built into the Swift standard library, similar to Optional. It represents success and failure cases, with each case including an associated value. Success associated values can be anything, but error associated values must conform to the Error protocol.

Let’s rewrite our findElementIndex(_ inArray:) function from earlier to return a Result instead of an Optional.

// Swift

enum SearchError: Error {
  case notFound
}

func findElementIndex(_ element: Int, inArray array: [Int]) -> Result<Int, SearchError> {
  for (index, value) in array.enumerated() {
    if (element == value) {
      return Result.success(index)
    }
  }
  return Result.failure(SearchError.notFound)
}

findElementIndex(5, inArray: primes) // Returns Result.success(3)

We see that we need to define our own custom type, SearchError, in order to use Result. Unlike Optional, which has built-in syntactic sugar for unboxing values, Result’s associated types need to be unboxed to use them:

// Swift

switch findElementIndex(5, inArray: primes) {
case let .success(index):
  print("The index was \(index)")
case .failure(SearchError.notFound):
  print("The index was not found")
}

Again, a lot of syntax here. Plus, I’m only printing these out. Practically speaking, I’d probably be assigning the index to a local variable – yet more syntax!

// Swift

guard case let .success(index) = findElementIndex(5, inArray: primes) else { return }

Let’s rewrite the findElementInArray function in TypeScript to use a Result type. I need to define my own type here, since Result isn’t included in the TypeScript language.

// TypeScript

type Result = {
    type: 'success'
    value: number
  } | {
    type: 'not_found'
  }

This is a special kind of union. It’s called a discriminated union because all the members have an overlapping field name (type) which can be used to discriminate between the potential values. This might feel familiar, like a Swift enum with associated values.

Let’s see how our Result union type gets used:

// TypeScript

const findElementInArray = (element: number, array: number[]): Result => {
  for (const [index, value] of array.entries()) {
    if (value === element) {
      return {
        type: 'success',
        value: index
      }
    }
  }
  return {
    type: 'not_found'
  }
}

findElementInArray(5, primes) // Returns { "type": "success", "value": 3 }

Ah-ha! A boxed value! You can see the parallels between discriminated unions in TypeScript enums with associated values in Swift. How would we unbox this value? What kind of syntax would we use?

Well, remember that TypeScript was built on top of JavaScript, with all its existing APIs. So the syntax to unbox the value is just the syntax that JavaScript developers were already using:

// TypeScript

const returnValue = findElementInArray(5, primes)
switch (returnValue.type) {
  case 'success':
    console.log(`The index was ${returnValue.value}`)
    break
  default:
    console.log('The index was not found')
}

Pretty straightforward, eh? The TypeScript compiler is actually doing something really clever here, though. Within the switchcase (where returnValue.type is 'success'), TypeScript narrows returnValue to only be { type: 'success', value: ... }. If you tried to access returnValue.value outside of that case block, you would get a compiler error because not all the possible returnValue values have that property.

Similar to Swift, we could assign the index in a single line of code:

// TypeScript

const index = returnValue.type === 'success' ? returnValue.value : undefined

(This is not quite semantically identical to the Swift one-liner, but we’ve found an area where the languages happen to support different developer patterns. Swift’s example used an early return while TypeScript uses a number | undefined type. I love finding these little spots where programming languages differ in subtle, but meaningful ways!)

It’s important to remember that TypeScript was adding type safety to an existing language with existing patterns. JavaScript already had APIs that returned objects that looked like discriminated unions. This was a popular pattern among APIs. You would often get back an object, check its type, and then access properties that you expected to be there. TypeScript’s support for discriminated unions brought type safety to an existing developer pattern. TypeScript met JavaScript where it was at, and I think there’s something beautiful about that.


Please understand that I love Swift. It is so precise and specific and pedantic – I can’t help but love it! But during day-to-day use, it frustrates me. TypeScript gets out of my way and lets me focus on the structure of my code rather than on boilerplate and syntax.

You could say that both Swift and TypeScript frustrate me (because they are both programming languages and all programming languages are frustrating). But TypeScript frustrates me less severely, and less often. And that’s what I mean when I say that I like TypeScript’s developer ergonomics.

I hope you’ve enjoyed this blog post. I’ve been meaning to write code-level blog posts about React Native from a Swift developer’s perspective literally for years; I hope that this is a first step. I encourage you to look back at the examples and take a look at the syntax – I bet you can understand most of it, even if you’ve never written TypeScript before! Or, maybe there’s something that surprised you.

If you enjoyed this, let me know. I’d love to follow up with more.


Please submit typo corrections on GitHub