Type Aliases
Alias = T.type_alias {Type}
This creates a type alias of Type called Alias. In the context of Sorbet, the type alias has exactly the same behavior as the original type and can be used anywhere the original type can be used. The converse is also true.
Note that the type alias will not show up in error messages.
# typed: true
extend T::Sig
Int = T.type_alias {Integer}
Str = T.type_alias {String}
sig { params(x: Int).returns(Str) }
def foo(x)
T.reveal_type(x) # Revealed type: Integer
x.to_s
end
a = T.let(3, Integer)
foo(a)
b = T.let(3, Int)
foo(b)
c = foo(3)
T.reveal_type(c) # Revealed type: String
When creating a type alias from another type alias, you must use T.type_alias again:
A = T.type_alias {Integer}
B = T.type_alias {A}
For simple use cases, type aliases are nearly identical to just making a new constant:
# typed: true
extend T::Sig
A = T.type_alias {Integer}
sig { returns(A) }
def foo; 3; end
B = Integer
sig { returns(B) }
def bar; 3; end
However, when the type is more complex, you must use type aliases:
# typed: true
extend T::Sig
A = T.type_alias {T.any(Integer, String)}
sig { returns(A) }
def foo; 3; end
B = T.any(Integer, String)
sig { returns(B) } # error: Constant B is not a class or type alias
def bar; 3; end
Note that because type aliases are a Sorbet construct, they cannot be used in certain runtime contexts. For instance, it is not possible to match an expression against a type alias in a case expression.
# typed: true
extend T::Sig
class A; end
class B; end
class C; end
AB = T.type_alias {T.any(A, B)}
sig { params(x: T.any(AB, C)).returns(Integer) }
def invalid(x) # error: Returning value that does not conform to method result type
case x
when AB then 1 # <- this line is problematic
when C then 2
end
end
We could refactor this example to use A, B in the when and AB in the sig. However, this introduces coupling between the definition of AB and our method. If we ever updated the definition of AB, we would need to update the definition of our method as well.
sig { params(x: T.any(AB, C)).returns(Integer) }
def valid(x)
case x
when A, B then 1
when C then 2
end
end
T.type_alias and RBI files
Type aliases defined in RBI files do not exist at runtime because RBI files are not loaded by the Ruby VM. For this reason, it’s usually dangerous to put T.type_alias declarations in RBI files, because Sorbet would not catch code that actually uses it (e.g., inside a runtime-checked sig).
To define a type alias that doesn’t participate in runtime checking, a better alternative is to define the type alias in a Ruby source file with .checked(:never):
MyType = T.type_alias { T.any(Integer, String) }.checked(:never)
See below for more information on .checked in type aliases.
.checked: Controlling runtime checking
Like .checked on method signatures, type aliases allow configuring whether the type alias participates in runtime checking:
# (1) Always validate (default)
MyType = T.type_alias { T.any(Integer, String) }.checked(:always)
# (2) Only validate in tests
MyType = T.type_alias { T.any(Integer, String) }.checked(:tests)
# (3) Never validate at runtime
MyType = T.type_alias { T.any(Integer, String) }.checked(:never)
Note: The
.checkedgoes outside of the{ ... }block, unlike withsig, where it goes inside the block.
When a type alias is not checked (either .checked(:never), or .checked(:tests) outside of a test environment), the type alias accepts all values at runtime, similar to T.anything. Like with .checked on a sig, Sorbet always uses the real type when doing static checks, regardless of the runtime checked level.
When omitted, type aliases use the T::Configuration.default_checked_level, which defaults to :always. See .checked on method signatures for more. Note in particular that .checked(:tests) requires special setup, documented there.
The .checked annotation only affects runtime value checking: the actual type defined inside the block will still be used for things like runtime override checking.
What about type aliases for method signatures?
Sometimes a question arises like, “Is there a way to factor an entire method signature into a type alias, not just types for individual arguments?”
No, there is not. This is mostly for simplicity of implementation within Sorbet.
Two workarounds are:
- Define type aliases for all argument and return types of the methods in question.
- Factor shared arguments into a typed data structure (perhaps using T::Struct), and update the methods in question to take that structure.
Note that types for lambdas and procs can be written in type aliases using proc types.
What about recursive type aliases?
Some languages have recursive type aliases. For example, TypeScript allows writing type aliases like this one which vaguely describes the type of all JSON documents (example uses TypeScript syntax):
type JSON = null | number | string | JSON[] | {[arg: string]: JSON};
Sorbet does not support recursive type aliases. To have types that reference themselves, use class types.
class SelfReferential
extend T::Sig
sig { returns(T.nilable(SelfReferential)) }
attr_reader :val
sig { params(val: T.nilable(SelfReferential)).void }
def initialize(val); @val = val; end
end
Unfortunately for the case of typing JSON, this generally leads to more verbosity than in other languages, but can still accomplish something similar:
For the specific example of typing JSON, note that most Sorbet users tend to just use T::Hash[String, T.untyped] or T.untyped. Serializing and deserializing JSON is usually handled better by purpose-built serialization libraries. The type of “all JSON documents” is usually unnaturally wide—it’s better to have an explicit step which converts the loosely JSON data structure into a more structured internal representation.