Skip to content

Latest commit

 

History

History
171 lines (130 loc) · 5.72 KB

type-aliases.md

File metadata and controls

171 lines (130 loc) · 5.72 KB
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

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:

  1. Define type aliases for all argument and return types of the methods in question.
  2. 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:

→ Full example on sorbet.run

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.