T::Enum Pros & Cons

One feature that Sorbet doesn’t haveYet. The biggest limitation is just that Sorbet’s approach to type inference is designed to run fast and be simple to understand, sometimes sacrificing power.

… but actually Sorbet already has these types internally 😅 It’s just that it doesn’t have syntax for people to write them in type annotations. And lo, it’s because they’re buggy, but for the things where Sorbet needs to use them internally we can intentionally work around the known bugs, so it hasn’t been worth the pain to fix.

but gets requested frequently is support for literal string and symbol types. Something like T.any(:left, :right), which is a type that allows either the symbol literal :left or :right, but no other Symbols much less other types of values. The closest that Sorbet has to this right now is typed enums:

class LeftOrRight < T::Enum
  enums do
    Left = new
    Right = new

TypeScript, Flow, and Mypy all have literal types. You probably have felt yourself wanting this. I don’t really have to explain why they’re nice. But I’ll do it anyways, just to prove that I hear you.

👎 T::Enum cannot be combined in ad hoc unions.

That’s a fancy way of saying we’d like to be able to write T.any(:left, :right) in any type annotation, without first having to pre-declare the new union type to the world. I spoke at length about how the existence of ad hoc union types make handling exceptional conditions more pleasant than checked exceptions, so I’m right there with you in appreciating that feature.

👎 T::Enum is verbose.

Even if you wanted to pre-declare the enum type. Consider:

LeftOrRight = T.type_alias {T.any(, )}

Boom. One line, no boilerplate. Wouldn’t that be nice?

👎 It’s hard to have one T::Enum be a subset of another.

This comes up so frequently that there’s an FAQ entry about it. The answer is yet more verbosity and boilerplate.

So I hear you. But I wanted to say a few things in defense of T::Enum, because I think that despite how nice it might be to have literal types (and again, we may yet build them one day), there are still a lot of points in favor of T::Enum as it exists today.

🚀 Every IDE feature Sorbet supports works for T::Enum.

T::Enums are just normal constants. Sorbet supports finding all constant references, renaming constants, autocompleting constant names, jumping to a constant’s definition, hovering over a constant to see its documentation comment. Also all of those features work on both the enum class itself and each individual enum value.

We could maybe support completion for symbol literals in limited circumstances, but it would be the first of its kind in Sorbet. Same goes for rename, and maybe find all references. Jump to Definition I guess would want to jump not to the actual definition, but rather to the signature that specified the literal type? It’s weird.

🙊 T::Enum guards against basically all typos.

Even in # typed: false files! Even when calling methods that take don’t have signatures, or that have loose signatures like Object! Incidentally, this is basically the same reason why find all references can work so well.

🤝 It requires being intentional.

Code gets out of hand really quickly when people try to cutely interpolate strings into other strings that hold meaning. I’d much rather deal with this:

direction = [left_or_right, up_or_down]

than this:

direction = "#{left_or_right}__#{up_or_down}"

If you try to do this with T::Enum you get strings that look like:


which confuses people, so they ask how to do the thing they’re trying to do, which is a perfect opportunity to talk them down from that cliff. If people decide that yes, this really is the API we need, we can be intentional about it with .serialize:

direction = "#{left_or_right.serialize}__#{up_or_down.serialize}"

🕵️ It’s easy to search for.

This is a small one, but I’ll mention it anyways. It’s quick to search the Sorbet docs for T::Enum and get to the right page. It’s similarly easy to find examples of it being used in a given codebase, to learn from real code. There’s no unique piece of syntax in T.any(:left, :right) that is a surefire thing to search for.