---
title: Type systems and checking in Elixir and Ruby
teaser: Adding type checking to dynamically typed languages is possible, and awesome!
tags: elixir,ruby,types,good code
author: Hawley Brett
published_on: 2021-01-18
---

I must admit -- when it comes to programming languages, I have a type: robust
type systems!

Ever since learning Elm, I’ve fallen in love with programming with a expressive
type system. Since I work in other languages as well which are dynamically
typed, I find myself yearning for a more robust type system and the guarantees
of a static type checker. So I started exploring options for adding types to
some of the languages in which I work most frequently, Ruby and Elixir.

Robust type systems are also becoming increasingly popular -- it’s not just me
who loves them! Type systems and checking eliminates a whole genre of errors --
runtime errors -- and it does more than that too: it offers a means of modeling
your domain, enforcing contracts and consistency within code, and documenting
your code to enhance readability and intent. There are lots of ways that types
can benefit a project, and much of the tooling out there lets you gradually add
types to your codebase, so you can test out how it's benefitting you before
making a commitment.

As more people have been seeing the value of working with an expressive type
system, more tooling has been created for gradually implementing them in
dynamically typed languages, and I want to share about what's emerging in this
area.

While I'll be focusing on Ruby and Elixir, TypeScript very much deserves mention
here as well, since it's an option for typing with JavaScript. Because it’s
already more widely used, I want to concentrate on what’s emerging in other
languages, but its growing popularity is testament to the helpfulness of strong
typing. Elm is another favorite language of mine that we’ve [written about a
bunch at thoughtbot](https://thoughtbot.com/blog/tags/elm), which is considered
to have a very robust type system.

## Note on vocabulary

Before we begin, let's review some vocabulary.

“Statically typed” means that types are checked at compile time. “Dynamically
typed” means that types are checked at runtime. We'll use these terms to
formally categorize languages.

A "type system" is a language's set of rules that govern its constructs, such as
varibles, functions, and components likes strings and integers, or other data
structures like maps and lists.

“Strongly typed” or “strictly typed” means that a language's type system is
robust, expressive, and strictly enforced. A “weak” type system by contrast is
one which has more permissive rules and which and does not support features for
expressing more complex types. “Strong”and “weak” are therefore not formal
technical terms, and rather are used to compare different type systems based on
how robust they are relative to each other. So we can think of type systems on a
range from more primitive, or weak, to more expressive, or strong.

## Elixir

Elixir offers some means of working with types out of the box, which is really
cool. Using typespecs and tooling for static analysis, one can mimick many
aspects of a static type system. There's also an exciting new language in
development, Gleam, which, not to be reductive, is like Elixir with type
checking, and it is pretty awesome.

### Typespecs

Elixir offers typespecs as an opt-in feature. Typespecs are used to annotate
function signatures (also called specifications), and for defining custom types.

On any Elixir function, you can add a typespec as annotation to state the types
of arguments it expects and the type which will be returned. Typespecs are like
documentation and they are not evaluated at runtime, so if you make a mistake
with them, that won’t affect how the code runs.

Typespecs can also be used to define custom types in Elixir, a feature of
stronger type systems. 

Typespecs alone are valuable, even if you don't combine them with other tooling
to enforce type checking. I like to think of a type annotation as documentation,
and as a contract. When you add a specification, it’s like making a promise that
your code does what you’re saying it does. It clarifies intent for other readers
of the code. If you use typespecs to define custom types, you can use those
types to expressively model your domain.

If you're interested in the benefits of a type checker, you have some options to
layer onto typespecs.

You can enable an IDE to highlight any mismatches between your typespecs and
code so you can identify issues as you work (I recommend Visual Studio Code +
[VSCode Elixir Plugin]
(https://marketplace.visualstudio.com/items?itemName=mjmcloug.vscode-elixir) for
this). This plugin will even show you auto-generated annotations to make adding
them less work, and you'll see highlights when you’ve created a mismatch, saving
time as you develop.

Typespecs can also be combined with a tool such as
[Dialyzer](https://erlang.org/doc/man/dialyzer.html) to do type checking on your
behalf outside of when you run the program, thus mimicking a static type
checker.

See [the documentation
here](https://hexdocs.pm/elixir/typespecs.html#user-defined-types) to go deeper
on how to use typespecs, such as mixing them in with guards or using opaque
types.

Finally, it's worth noting that Elixir offers some means of making "contracts"
within code beyond type checking, like [sum
types](https://thoughtbot.com/blog/better-domain-modeling-in-elixir-with-sum-types)
or
[Behaviors](https://elixir-lang.org/getting-started/typespecs-and-behaviours.html#behaviours).
These are outside the scope of this article but I encourage you to check out
these links if you're interested in going deeper.

### Gleam

The other option to consider for Elixir is [Gleam](https://gleam.run/), an
exciting new language in development. Again, not to be reductive, but Gleam is
like Elixir + robust static typing. I tried it out and it felt exactly like
Elixir, except I could build custom types with ease and rely on the type
checker. I did struggle a bit to get my Gleam environment set up because some of
the requirements involved different versions of Erlang OTP and rebar than what I
had installed already, but I was still able to get going. I have not tried
adding Gleam to a Phoenix application but it seems it can be done! I hope to try
that out soon.

I think that currently it's more practical in Elixir to rely on the built in
support for typing than it is to switch to Gleam, but I'm excited about where
Gleam is going and think it could prove to be a really excellent language. 

## Ruby

In Ruby land, there’s a growing suite of tools for type checking around existing
Ruby code - Sorbet, RBS, and, excitingly, the upcoming release of Ruby 3 which
will ship with support for type checking.

### Sorbet

[Sorbet](https://sorbet.org/) is a type checker designed for Ruby and built by
Shopify as they needed a way to maintain and scale their very large code base.
Sorbet lets you add type checking to your Ruby code one file at a time. I
recently tried adding it to a project, and it was easy to set up initially.

One weird thing you run into with Sorbet is that metaprogramming is popular in
Ruby and especially so in Rails, where many functions are defined at runtime,
and therefore don't yet exist when type checked ahead of runtime. For cases like
this, you can declare types of these methods ahead of time and Sorbet will know
to use them. It's a little counterintuitive to add types for methods that don't
yet exist, but it works.

If you're looking to add Sorbet to a Rails project, I'd recommend
[sorbet-rails](https://github.com/chanzuckerberg/sorbet-rails) which offers
tooling to help auto-generate types based on patterns in Rails, like column
getter methods. It would be a lot of redudant work to add type checking to
everything in a Rails project, so this tool thankfully automates that away.

### RBS and Ruby 3

[RBS](https://github.com/ruby/rbs) is a language for defining types that will be
used in Ruby 3, which will ship with support for type annotations. You can also
use it independently of Ruby 3. Sorbet will be incorporating RBS as a means of
adding type specifications, and you can [read about the ongoing development
here](https://sorbet.org/blog/2020/07/30/ruby-3-rbs-sorbet). Because of this,
Sorbet is concentrating on other features while the Ruby team focuses on the RBS
side of things. For now, if you're looking to get going with types in Ruby, I'd
recommend Sorbet (and Sorbet Rails) because it has more tooling and
documentation available, which will still be relevant when Ruby 3 ships.

## Give it a try!

If you’re working in a dynamically typed language, consider if adding a more
robust type system and static type checking would benefit your code. For many
popular language ecosystems, there is tooling that is emerging for this. It’s
been my experience that adding a type system can help get rid of errors, aid in
modeling the domain, and clarify the intent of your code for others to read, all
which helps to scale and maintain big projects. Importantly, you don’t have to
switch over to types all at once, instead opting to gradually add them in.
Consider trying it out!
