id | title | sidebar_label |
---|---|---|
type-aliases |
Type Aliases |
T.type_alias |
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
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.
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.