sorbet-runtime 0.5.10439 → 0.5.11120

Sign up to get free protection for your applications and to get access to all the features.
Files changed (46) hide show
  1. checksums.yaml +4 -4
  2. data/lib/sorbet-runtime.rb +7 -1
  3. data/lib/types/_types.rb +57 -3
  4. data/lib/types/compatibility_patches.rb +4 -2
  5. data/lib/types/enum.rb +6 -1
  6. data/lib/types/generic.rb +2 -0
  7. data/lib/types/private/abstract/declare.rb +10 -9
  8. data/lib/types/private/casts.rb +4 -1
  9. data/lib/types/private/class_utils.rb +13 -6
  10. data/lib/types/private/methods/_methods.rb +37 -12
  11. data/lib/types/private/methods/call_validation.rb +104 -5
  12. data/lib/types/private/methods/call_validation_2_6.rb +68 -60
  13. data/lib/types/private/methods/call_validation_2_7.rb +68 -60
  14. data/lib/types/private/methods/decl_builder.rb +21 -6
  15. data/lib/types/private/methods/signature.rb +63 -38
  16. data/lib/types/private/methods/signature_validation.rb +68 -6
  17. data/lib/types/private/runtime_levels.rb +19 -0
  18. data/lib/types/private/types/not_typed.rb +2 -0
  19. data/lib/types/private/types/simple_pair_union.rb +55 -0
  20. data/lib/types/private/types/void.rb +29 -23
  21. data/lib/types/props/_props.rb +2 -2
  22. data/lib/types/props/custom_type.rb +2 -2
  23. data/lib/types/props/decorator.rb +41 -36
  24. data/lib/types/props/has_lazily_specialized_methods.rb +2 -2
  25. data/lib/types/props/pretty_printable.rb +45 -83
  26. data/lib/types/props/private/setter_factory.rb +1 -1
  27. data/lib/types/props/serializable.rb +17 -9
  28. data/lib/types/props/type_validation.rb +5 -2
  29. data/lib/types/struct.rb +2 -2
  30. data/lib/types/types/anything.rb +31 -0
  31. data/lib/types/types/base.rb +15 -1
  32. data/lib/types/types/class_of.rb +11 -0
  33. data/lib/types/types/enum.rb +1 -1
  34. data/lib/types/types/fixed_array.rb +13 -0
  35. data/lib/types/types/fixed_hash.rb +22 -0
  36. data/lib/types/types/intersection.rb +1 -1
  37. data/lib/types/types/noreturn.rb +0 -1
  38. data/lib/types/types/simple.rb +27 -4
  39. data/lib/types/types/type_parameter.rb +19 -0
  40. data/lib/types/types/typed_array.rb +29 -0
  41. data/lib/types/types/typed_class.rb +85 -0
  42. data/lib/types/types/typed_enumerable.rb +7 -0
  43. data/lib/types/types/typed_enumerator_chain.rb +41 -0
  44. data/lib/types/types/union.rb +50 -14
  45. data/lib/types/utils.rb +41 -33
  46. metadata +14 -10
@@ -16,6 +16,7 @@ module T::Props::TypeValidation
16
16
  super || key == :DEPRECATED_underspecified_type
17
17
  end
18
18
 
19
+ # checked(:never) - Rules hash is expensive to check
19
20
  sig do
20
21
  params(
21
22
  name: T.any(Symbol, String),
@@ -24,12 +25,13 @@ module T::Props::TypeValidation
24
25
  type: T.any(T::Types::Base, Module)
25
26
  )
26
27
  .void
28
+ .checked(:never)
27
29
  end
28
30
  def prop_validate_definition!(name, _cls, rules, type)
29
31
  super
30
32
 
31
33
  if !rules[:DEPRECATED_underspecified_type]
32
- validate_type(type, field_name: name)
34
+ validate_type(type, name)
33
35
  elsif rules[:DEPRECATED_underspecified_type] && find_invalid_subtype(type).nil?
34
36
  raise ArgumentError.new("DEPRECATED_underspecified_type set unnecessarily for #{@class.name}.#{name} - #{type} is a valid type")
35
37
  end
@@ -41,8 +43,9 @@ module T::Props::TypeValidation
41
43
  field_name: T.any(Symbol, String),
42
44
  )
43
45
  .void
46
+ .checked(:never)
44
47
  end
45
- private def validate_type(type, field_name:)
48
+ private def validate_type(type, field_name)
46
49
  if (invalid_subtype = find_invalid_subtype(type))
47
50
  raise UnderspecifiedType.new(type_error_message(invalid_subtype, field_name, type))
48
51
  end
data/lib/types/struct.rb CHANGED
@@ -10,7 +10,7 @@ end
10
10
  class T::Struct < T::InexactStruct
11
11
  def self.inherited(subclass)
12
12
  super(subclass)
13
- T::Private::ClassUtils.replace_method(subclass.singleton_class, :inherited) do |s|
13
+ T::Private::ClassUtils.replace_method(subclass.singleton_class, :inherited, true) do |s|
14
14
  super(s)
15
15
  raise "#{self.name} is a subclass of T::Struct and cannot be subclassed"
16
16
  end
@@ -23,7 +23,7 @@ class T::ImmutableStruct < T::InexactStruct
23
23
  def self.inherited(subclass)
24
24
  super(subclass)
25
25
 
26
- T::Private::ClassUtils.replace_method(subclass.singleton_class, :inherited) do |s|
26
+ T::Private::ClassUtils.replace_method(subclass.singleton_class, :inherited, true) do |s|
27
27
  super(s)
28
28
  raise "#{self.name} is a subclass of T::ImmutableStruct and cannot be subclassed"
29
29
  end
@@ -0,0 +1,31 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module T::Types
5
+ # The top type
6
+ class Anything < Base
7
+ def initialize; end
8
+
9
+ # overrides Base
10
+ def name
11
+ "T.anything"
12
+ end
13
+
14
+ # overrides Base
15
+ def valid?(obj)
16
+ true
17
+ end
18
+
19
+ # overrides Base
20
+ private def subtype_of_single?(other)
21
+ case other
22
+ when T::Types::Anything then true
23
+ else false
24
+ end
25
+ end
26
+
27
+ module Private
28
+ INSTANCE = Anything.new.freeze
29
+ end
30
+ end
31
+ end
@@ -50,10 +50,20 @@ module T::Types
50
50
  t2 = t2.aliased_type
51
51
  end
52
52
 
53
+ if t2.is_a?(T::Types::Anything)
54
+ return true
55
+ end
56
+
53
57
  if t1.is_a?(T::Private::Types::TypeAlias)
54
58
  return t1.aliased_type.subtype_of?(t2)
55
59
  end
56
60
 
61
+ if t1.is_a?(T::Types::TypeVariable) || t2.is_a?(T::Types::TypeVariable)
62
+ # Generics are erased at runtime. Let's treat them like `T.untyped` for
63
+ # the purpose of things like override checking.
64
+ return true
65
+ end
66
+
57
67
  # pairs to cover: 1 (_, _)
58
68
  # 2 (_, And)
59
69
  # 3 (_, Or)
@@ -163,8 +173,12 @@ module T::Types
163
173
  # Type equivalence, defined by serializing the type to a string (with
164
174
  # `#name`) and comparing the resulting strings for equality.
165
175
  def ==(other)
166
- (T::Utils.resolve_alias(other).class == T::Utils.resolve_alias(self).class) &&
176
+ case other
177
+ when T::Types::Base
167
178
  other.name == self.name
179
+ else
180
+ false
181
+ end
168
182
  end
169
183
 
170
184
  alias_method :eql?, :==
@@ -27,6 +27,8 @@ module T::Types
27
27
  @type <= other.type
28
28
  when Simple
29
29
  @type.is_a?(other.raw_type)
30
+ when TypedClass
31
+ true
30
32
  else
31
33
  false
32
34
  end
@@ -36,5 +38,14 @@ module T::Types
36
38
  def describe_obj(obj)
37
39
  obj.inspect
38
40
  end
41
+
42
+ # So that `T.class_of(...)[...]` syntax is valid.
43
+ # Mirrors the definition of T::Generic#[] (generics are erased).
44
+ #
45
+ # We avoid simply writing `include T::Generic` because we don't want any of
46
+ # the other methods to appear (`T.class_of(A).type_member` doesn't make sense)
47
+ def [](*types)
48
+ self
49
+ end
39
50
  end
40
51
  end
@@ -29,7 +29,7 @@ module T::Types
29
29
 
30
30
  # overrides Base
31
31
  def name
32
- "T.deprecated_enum([#{@values.map(&:inspect).join(', ')}])"
32
+ @name ||= "T.deprecated_enum([#{@values.map(&:inspect).join(', ')}])"
33
33
  end
34
34
 
35
35
  # overrides Base
@@ -59,6 +59,19 @@ module T::Types
59
59
  @types.size == other.types.size && @types.zip(other.types).all? do |t1, t2|
60
60
  t1.subtype_of?(t2)
61
61
  end
62
+ when TypedArray
63
+ # warning: covariant arrays
64
+
65
+ value1, value2, *values_rest = types
66
+ value_type = if !value2.nil?
67
+ T::Types::Union::Private::Pool.union_of_types(value1, value2, values_rest)
68
+ elsif value1.nil?
69
+ T.untyped
70
+ else
71
+ value1
72
+ end
73
+
74
+ T::Types::TypedArray.new(value_type).subtype_of?(other)
62
75
  else
63
76
  false
64
77
  end
@@ -38,6 +38,28 @@ module T::Types
38
38
  when FixedHash
39
39
  # Using `subtype_of?` here instead of == would be unsound
40
40
  @types == other.types
41
+ when TypedHash
42
+ # warning: covariant hashes
43
+
44
+ key1, key2, *keys_rest = types.keys.map {|key| T::Utils.coerce(key.class)}
45
+ key_type = if !key2.nil?
46
+ T::Types::Union::Private::Pool.union_of_types(key1, key2, keys_rest)
47
+ elsif key1.nil?
48
+ T.untyped
49
+ else
50
+ key1
51
+ end
52
+
53
+ value1, value2, *values_rest = types.values
54
+ value_type = if !value2.nil?
55
+ T::Types::Union::Private::Pool.union_of_types(value1, value2, values_rest)
56
+ elsif value1.nil?
57
+ T.untyped
58
+ else
59
+ value1
60
+ end
61
+
62
+ T::Types::TypedHash.new(keys: key_type, values: value_type).subtype_of?(other)
41
63
  else
42
64
  false
43
65
  end
@@ -20,7 +20,7 @@ module T::Types
20
20
 
21
21
  # overrides Base
22
22
  def name
23
- "T.all(#{@types.map(&:name).sort.join(', ')})"
23
+ "T.all(#{@types.map(&:name).compact.sort.join(', ')})"
24
24
  end
25
25
 
26
26
  # overrides Base
@@ -4,7 +4,6 @@
4
4
  module T::Types
5
5
  # The bottom type
6
6
  class NoReturn < Base
7
-
8
7
  def initialize; end
9
8
 
10
9
  # overrides Base
@@ -4,6 +4,9 @@
4
4
  module T::Types
5
5
  # Validates that an object belongs to the specified class.
6
6
  class Simple < Base
7
+ NAME_METHOD = Module.instance_method(:name)
8
+ private_constant(:NAME_METHOD)
9
+
7
10
  attr_reader :raw_type
8
11
 
9
12
  def initialize(raw_type)
@@ -16,7 +19,7 @@ module T::Types
16
19
  #
17
20
  # `name` isn't normally a hot path for types, but it is used in initializing a T::Types::Union,
18
21
  # and so in `T.nilable`, and so in runtime constructions like `x = T.let(nil, T.nilable(Integer))`.
19
- @name ||= @raw_type.name.freeze
22
+ @name ||= (NAME_METHOD.bind(@raw_type).call || @raw_type.name).freeze
20
23
  end
21
24
 
22
25
  # overrides Base
@@ -29,6 +32,11 @@ module T::Types
29
32
  case other
30
33
  when Simple
31
34
  @raw_type <= other.raw_type
35
+ when TypedClass
36
+ # This case is a bit odd--we would have liked to solve this like we do
37
+ # for `T::Array` et al., but don't for backwards compatibility.
38
+ # See `type_for_module` below.
39
+ @raw_type <= other.underlying_class
32
40
  else
33
41
  false
34
42
  end
@@ -52,11 +60,21 @@ module T::Types
52
60
  end
53
61
 
54
62
  def to_nilable
55
- @nilable ||= T::Types::Union.new([self, T::Utils::Nilable::NIL_TYPE])
63
+ @nilable ||= T::Private::Types::SimplePairUnion.new(
64
+ self,
65
+ T::Utils::Nilable::NIL_TYPE,
66
+ )
56
67
  end
57
68
 
58
69
  module Private
59
70
  module Pool
71
+ CACHE_FROZEN_OBJECTS = begin
72
+ ObjectSpace::WeakMap.new[1] = 1
73
+ true # Ruby 2.7 and newer
74
+ rescue ArgumentError # Ruby 2.6 and older
75
+ false
76
+ end
77
+
60
78
  @cache = ObjectSpace::WeakMap.new
61
79
 
62
80
  def self.type_for_module(mod)
@@ -76,16 +94,21 @@ module T::Types
76
94
  elsif !Object.autoload?(:Set) && Object.const_defined?(:Set) && mod == ::Set
77
95
  T::Set[T.untyped]
78
96
  else
97
+ # ideally we would have a case mapping from ::Class -> T::Class here
98
+ # but for backwards compatibility we don't have that, and instead
99
+ # have a special case in subtype_of_single?
79
100
  Simple.new(mod)
80
101
  end
81
102
 
82
103
  # Unfortunately, we still need to check if the module is frozen,
83
- # since WeakMap adds a finalizer to the key that is added
104
+ # since on 2.6 and older WeakMap adds a finalizer to the key that is added
84
105
  # to the map, so that it can clear the map entry when the key is
85
106
  # garbage collected.
86
107
  # For a frozen object, though, adding a finalizer is not a valid
87
108
  # operation, so this still raises if `mod` is frozen.
88
- @cache[mod] = type unless mod.frozen?
109
+ if CACHE_FROZEN_OBJECTS || (!mod.frozen? && !type.frozen?)
110
+ @cache[mod] = type
111
+ end
89
112
  type
90
113
  end
91
114
  end
@@ -3,11 +3,30 @@
3
3
 
4
4
  module T::Types
5
5
  class TypeParameter < Base
6
+ module Private
7
+ @pool = {}
8
+
9
+ def self.cached_entry(name)
10
+ @pool[name]
11
+ end
12
+
13
+ def self.set_entry_for(name, type)
14
+ @pool[name] = type
15
+ end
16
+ end
17
+
6
18
  def initialize(name)
7
19
  raise ArgumentError.new("not a symbol: #{name}") unless name.is_a?(Symbol)
8
20
  @name = name
9
21
  end
10
22
 
23
+ def self.make(name)
24
+ cached = Private.cached_entry(name)
25
+ return cached if cached
26
+
27
+ Private.set_entry_for(name, new(name))
28
+ end
29
+
11
30
  def valid?(obj)
12
31
  true
13
32
  end
@@ -26,6 +26,31 @@ module T::Types
26
26
  Array.new(*T.unsafe(args))
27
27
  end
28
28
 
29
+ module Private
30
+ module Pool
31
+ CACHE_FROZEN_OBJECTS = begin
32
+ ObjectSpace::WeakMap.new[1] = 1
33
+ true # Ruby 2.7 and newer
34
+ rescue ArgumentError # Ruby 2.6 and older
35
+ false
36
+ end
37
+
38
+ @cache = ObjectSpace::WeakMap.new
39
+
40
+ def self.type_for_module(mod)
41
+ cached = @cache[mod]
42
+ return cached if cached
43
+
44
+ type = TypedArray.new(mod)
45
+
46
+ if CACHE_FROZEN_OBJECTS || (!mod.frozen? && !type.frozen?)
47
+ @cache[mod] = type
48
+ end
49
+ type
50
+ end
51
+ end
52
+ end
53
+
29
54
  class Untyped < TypedArray
30
55
  def initialize
31
56
  super(T.untyped)
@@ -34,6 +59,10 @@ module T::Types
34
59
  def valid?(obj)
35
60
  obj.is_a?(Array)
36
61
  end
62
+
63
+ module Private
64
+ INSTANCE = Untyped.new.freeze
65
+ end
37
66
  end
38
67
  end
39
68
  end
@@ -0,0 +1,85 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module T::Types
5
+ class TypedClass < T::Types::Base
6
+ attr_reader :type
7
+
8
+ def initialize(type)
9
+ @type = T::Utils.coerce(type)
10
+ end
11
+
12
+ # overrides Base
13
+ def name
14
+ "T::Class[#{@type.name}]"
15
+ end
16
+
17
+ def underlying_class
18
+ Class
19
+ end
20
+
21
+ # overrides Base
22
+ def valid?(obj)
23
+ Class.===(obj)
24
+ end
25
+
26
+ # overrides Base
27
+ private def subtype_of_single?(type)
28
+ case type
29
+ when TypedClass
30
+ # treat like generics are erased
31
+ true
32
+ when Simple
33
+ Class <= type.raw_type
34
+ else
35
+ false
36
+ end
37
+ end
38
+
39
+ module Private
40
+ module Pool
41
+ CACHE_FROZEN_OBJECTS =
42
+ begin
43
+ ObjectSpace::WeakMap.new[1] = 1
44
+ true # Ruby 2.7 and newer
45
+ rescue ArgumentError
46
+ false # Ruby 2.6 and older
47
+ end
48
+
49
+ @cache = ObjectSpace::WeakMap.new
50
+
51
+ def self.type_for_module(mod)
52
+ cached = @cache[mod]
53
+ return cached if cached
54
+
55
+ type = TypedClass.new(mod)
56
+
57
+ if CACHE_FROZEN_OBJECTS || (!mod.frozen? && !type.frozen?)
58
+ @cache[mod] = type
59
+ end
60
+ type
61
+ end
62
+ end
63
+ end
64
+
65
+ class Untyped < TypedClass
66
+ def initialize
67
+ super(T.untyped)
68
+ end
69
+
70
+ module Private
71
+ INSTANCE = Untyped.new.freeze
72
+ end
73
+ end
74
+
75
+ class Anything < TypedClass
76
+ def initialize
77
+ super(T.anything)
78
+ end
79
+
80
+ module Private
81
+ INSTANCE = Anything.new.freeze
82
+ end
83
+ end
84
+ end
85
+ end
@@ -54,6 +54,9 @@ module T::Types
54
54
  when Enumerator::Lazy
55
55
  # Enumerators can be unbounded: see `[:foo, :bar].cycle`
56
56
  true
57
+ when Enumerator::Chain
58
+ # Enumerators can be unbounded: see `[:foo, :bar].cycle`
59
+ true
57
60
  when Enumerator
58
61
  # Enumerators can be unbounded: see `[:foo, :bar].cycle`
59
62
  true
@@ -88,6 +91,8 @@ module T::Types
88
91
  # both reading and writing. However, Sorbet treats *all*
89
92
  # Enumerable subclasses as covariant for ease of adoption.
90
93
  @type.subtype_of?(other.type)
94
+ elsif other.class <= Simple
95
+ underlying_class <= other.raw_type
91
96
  else
92
97
  false
93
98
  end
@@ -145,6 +150,8 @@ module T::Types
145
150
  end
146
151
  when Enumerator::Lazy
147
152
  T::Enumerator::Lazy[type_from_instances(obj)]
153
+ when Enumerator::Chain
154
+ T::Enumerator::Chain[type_from_instances(obj)]
148
155
  when Enumerator
149
156
  T::Enumerator[type_from_instances(obj)]
150
157
  when Set
@@ -0,0 +1,41 @@
1
+ # frozen_string_literal: true
2
+ # typed: true
3
+
4
+ module T::Types
5
+ class TypedEnumeratorChain < TypedEnumerable
6
+ attr_reader :type
7
+
8
+ def underlying_class
9
+ Enumerator::Chain
10
+ end
11
+
12
+ # overrides Base
13
+ def name
14
+ "T::Enumerator::Chain[#{@type.name}]"
15
+ end
16
+
17
+ # overrides Base
18
+ def recursively_valid?(obj)
19
+ obj.is_a?(Enumerator::Chain) && super
20
+ end
21
+
22
+ # overrides Base
23
+ def valid?(obj)
24
+ obj.is_a?(Enumerator::Chain)
25
+ end
26
+
27
+ def new(*args, &blk)
28
+ T.unsafe(Enumerator::Chain).new(*args, &blk)
29
+ end
30
+
31
+ class Untyped < TypedEnumeratorChain
32
+ def initialize
33
+ super(T.untyped)
34
+ end
35
+
36
+ def valid?(obj)
37
+ obj.is_a?(Enumerator::Chain)
38
+ end
39
+ end
40
+ end
41
+ end
@@ -6,6 +6,8 @@ module T::Types
6
6
  class Union < Base
7
7
  attr_reader :types
8
8
 
9
+ # Don't use Union.new directly, use `Private::Pool.union_of_types`
10
+ # inside sorbet-runtime and `T.any` elsewhere.
9
11
  def initialize(types)
10
12
  @types = types.flat_map do |type|
11
13
  type = T::Utils.coerce(type)
@@ -20,11 +22,16 @@ module T::Types
20
22
 
21
23
  # overrides Base
22
24
  def name
23
- type_shortcuts(@types)
25
+ # Use the attr_reader here so we can override it in SimplePairUnion
26
+ type_shortcuts(types)
24
27
  end
25
28
 
26
29
  private def type_shortcuts(types)
27
30
  if types.size == 1
31
+ # We shouldn't generally get here but it's possible if initializing the type
32
+ # evades Sorbet's static check and we end up on the slow path, or if someone
33
+ # is using the T:Types::Union constructor directly (the latter possibility
34
+ # is why we don't just move the `uniq` into `Private::Pool.union_of_types`).
28
35
  return types[0].name
29
36
  end
30
37
  nilable = T::Utils.coerce(NilClass)
@@ -57,32 +64,61 @@ module T::Types
57
64
  raise "This should never be reached if you're going through `subtype_of?` (and you should be)"
58
65
  end
59
66
 
67
+ def unwrap_nilable
68
+ non_nil_types = types.reject {|t| t == T::Utils::Nilable::NIL_TYPE}
69
+ return nil if types.length == non_nil_types.length
70
+ case non_nil_types.length
71
+ when 0 then nil
72
+ when 1 then non_nil_types.first
73
+ else
74
+ T::Types::Union::Private::Pool.union_of_types(non_nil_types[0], non_nil_types[1], non_nil_types[2..-1])
75
+ end
76
+ end
77
+
60
78
  module Private
61
79
  module Pool
62
80
  EMPTY_ARRAY = [].freeze
63
81
  private_constant :EMPTY_ARRAY
64
82
 
83
+ # Try to use `to_nilable` on a type to get memoization, or failing that
84
+ # try to at least use SimplePairUnion to get faster init and typechecking.
85
+ #
86
+ # We aren't guaranteed to detect a simple `T.nilable(<Module>)` type here
87
+ # in cases where there are duplicate types, nested unions, etc.
88
+ #
89
+ # That's ok, because returning is SimplePairUnion an optimization which
90
+ # isn't necessary for correctness.
91
+ #
65
92
  # @param type_a [T::Types::Base]
66
93
  # @param type_b [T::Types::Base]
67
94
  # @param types [Array] optional array of additional T::Types::Base instances
68
95
  def self.union_of_types(type_a, type_b, types=EMPTY_ARRAY)
69
- if types.empty?
70
- # We aren't guaranteed to detect a simple `T.nilable(<Module>)` type here
71
- # in cases where there are duplicate types, nested unions, etc.
72
- #
73
- # That's ok, because this is an optimization which isn't necessary for
74
- # correctness.
75
- if type_b == T::Utils::Nilable::NIL_TYPE && type_a.is_a?(T::Types::Simple)
96
+ if !types.empty?
97
+ # Slow path
98
+ return Union.new([type_a, type_b] + types)
99
+ elsif !type_a.is_a?(T::Types::Simple) || !type_b.is_a?(T::Types::Simple)
100
+ # Slow path
101
+ return Union.new([type_a, type_b])
102
+ end
103
+
104
+ begin
105
+ if type_b == T::Utils::Nilable::NIL_TYPE
76
106
  type_a.to_nilable
77
- elsif type_a == T::Utils::Nilable::NIL_TYPE && type_b.is_a?(T::Types::Simple)
107
+ elsif type_a == T::Utils::Nilable::NIL_TYPE
78
108
  type_b.to_nilable
79
109
  else
80
- Union.new([type_a, type_b])
110
+ T::Private::Types::SimplePairUnion.new(type_a, type_b)
81
111
  end
82
- else
83
- # This can't be a `T.nilable(<Module>)` case unless there are duplicates,
84
- # which is possible but unexpected.
85
- Union.new([type_a, type_b] + types)
112
+ rescue T::Private::Types::SimplePairUnion::DuplicateType
113
+ # Slow path
114
+ #
115
+ # This shouldn't normally be possible due to static checks,
116
+ # but we can get here if we're constructing a type dynamically.
117
+ #
118
+ # Relying on the duplicate check in the constructor has the
119
+ # advantage that we avoid it when we hit the memoized case
120
+ # of `to_nilable`.
121
+ type_a
86
122
  end
87
123
  end
88
124
  end