sorbet_typed-props 1.5.0 → 1.5.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 9f174dad33ad91ad3f978dca0a7048436f5174ddc4697d986352eea398efe3ce
4
- data.tar.gz: 0425e0dbfc6549cccdac000060c1f968d7877fb815c2f7e0374e494101887d40
3
+ metadata.gz: 38708cf1f39a8cef314d4843c4fed453f4faf8f2fc35db4e47e1c659dcd581d7
4
+ data.tar.gz: 94b1bf4f088610d3e3e3bb1eb1ea7f09da8428f9849176d214f4f0a9d2760ff7
5
5
  SHA512:
6
- metadata.gz: 1e63e26d0ad9e645e56efeda20b3df2aab26f0fac3da887a175c8d1b75dc8062a1360b6e615f370b8b6f065f93c93961e7947006a1840d76e89c2014c233c5c0
7
- data.tar.gz: 9e7310699fe6272d445e59a46b1c08c17fc3ccf0e27c0aecf3b41b8a595415ab5bcf52919db8a40d43e2efe4b971dc2516df2bf76c017ad87d07c7ed35fb7e27
6
+ metadata.gz: d37eeeaea430f1950b0a9bc80e50e5c98945458082ee037989c213151a593e67ecf9e52816b05c371636b8110b01d161ba53418f601ec514d654c5ea4abfa374
7
+ data.tar.gz: e54af5f419e3cea6b5aaacc4379db254d17c3cc6da84be35c8ab715149194466a1c5cec687896dc2cf93027541f1a5af88246ac4fd83d99fa92d3d54801e973e
data/.cz.yaml CHANGED
@@ -15,7 +15,7 @@ commitizen:
15
15
  - mise run format
16
16
  tag_format: v$version
17
17
  update_changelog_on_bump: true
18
- version: 1.5.0
18
+ version: 1.5.2
19
19
  version_files:
20
20
  - lib/sorbet_typed/props/version.rb
21
21
  version_scheme: semver2
data/.prettierignore ADDED
@@ -0,0 +1 @@
1
+ .superpowers/
data/CHANGELOG.md CHANGED
@@ -1,3 +1,15 @@
1
+ ## v1.5.2 (2026-07-03)
2
+
3
+ ### Fix
4
+
5
+ - **renovate**: stop creating MRs before minimumReleaseAge elapses
6
+
7
+ ## v1.5.1 (2026-06-23)
8
+
9
+ ### Fix
10
+
11
+ - **props**: adapt to sorbet-runtime bound_setter signature change
12
+
1
13
  ## v1.5.0 (2026-06-22)
2
14
 
3
15
  ### Feat
data/README.md CHANGED
@@ -14,10 +14,13 @@ the
14
14
  which doesn't really work with sorbet, if you want to have everything fully typed (and not use two runtime type-systems
15
15
  in parallel).
16
16
 
17
+ - [Tested with](#tested-with)
17
18
  - [Installation](#installation)
18
19
  - [Usage](#usage)
19
- - [Phlex Example](#phlex-example)
20
- - [Instance Variables](#instance-variables)
20
+ - [Defaults and optional props](#defaults-and-optional-props)
21
+ - [Internal props](#internal-props)
22
+ - [Inheritance](#inheritance)
23
+ - [Phlex integration](#phlex-integration)
21
24
  - [Extending initializer logic](#extending-initializer-logic)
22
25
  - [Method Visibility](#method-visibility)
23
26
  - [Development](#development)
@@ -27,12 +30,27 @@ in parallel).
27
30
 
28
31
  <!-- begin:tested-versions -->
29
32
 
30
- | | Tested versions |
31
- | -------------- | ---------------- |
32
- | Ruby | 3.3, 3.4, 4.0 |
33
- | sorbet-runtime | 0.6 |
34
- | tapioca | 0.17, 0.18, 0.19 |
35
- | sorbet | 0.6 |
33
+ This gem is tested against all combinations of the following dependency versions.
34
+
35
+ | | Tested versions |
36
+ | -------------- | -------------------- |
37
+ | Ruby | 3.3, 3.4, 4.0 |
38
+ | sorbet-runtime | 0.6.12997, 0.6.13321 |
39
+ | tapioca | 0.17, 0.18, 0.19 |
40
+ | sorbet | 0.6.12997, 0.6.13321 |
41
+
42
+ ### Excluded combinations
43
+
44
+ Some version combinations are incompatible with each other independently of this gem — users depending on those
45
+ combinations would face the same conflict in their own projects. These are excluded from the test matrix:
46
+
47
+ - `tapioca < 0.19.0` + `sorbet-runtime >= 0.6.13149` — tapioca < 0.19 calls has_rest on T::Private::Methods::Signature
48
+ which was removed in sorbet-runtime 0.6.13149
49
+ - `tapioca < 0.19.1` + `sorbet-runtime < 0.6.12698` — tapioca < 0.19.1 references T::Module which was added to
50
+ sorbet-runtime in 0.6.12698
51
+ - `tapioca >= 0.19.1` + `sorbet-runtime < 0.6.12698` — tapioca >= 0.19.1 enforces sorbet-runtime >= 0.6.12698 in its
52
+ gemspec; listed explicitly so the test generator skips sorbet-runtime < 0.6.12698 rather than treating the bundler
53
+ rejection as an unexpected error
36
54
 
37
55
  <!-- end:tested-versions -->
38
56
 
@@ -67,43 +85,99 @@ my_object.my_prop # => "foo"
67
85
  my_object.my_prop = 'bar'
68
86
  ```
69
87
 
70
- ### Phlex Example
88
+ [Full documentation](docs/props-and-const.md)
89
+
90
+ ### Defaults and optional props
91
+
92
+ Use `default:` for a static fallback value or `factory:` for a freshly constructed default per instance:
71
93
 
72
94
  ```ruby
73
- class Components::HelloWorld < Phlex::HTML
74
- extend T::Sig
95
+ class Config
75
96
  include SorbetTyped::Props
76
97
 
77
- prop :name, String
98
+ const :timeout_seconds, Integer, default: 30
99
+ const :logger, Logger, factory: -> { Logger.new($stdout) }
100
+ end
78
101
 
79
- sig { void }
80
- def view_template
81
- h1 { "Hello, #{name}!" }
102
+ Config.new # uses defaults
103
+ Config.new(timeout_seconds: 60) # override specific prop
104
+ ```
105
+
106
+ → [Full documentation](docs/defaults.md)
107
+
108
+ ### Internal props
109
+
110
+ Use `extra: { initializer: false }` for attributes that are set after construction, not at it — avoiding `T.must` noise
111
+ at read sites for order-dependent implementations:
112
+
113
+ ```ruby
114
+ class Pipeline
115
+ include SorbetTyped::Props
116
+
117
+ const :input, String
118
+ prop :result, String, extra: { initializer: false }
119
+
120
+ def run
121
+ self.result = process(input)
82
122
  end
123
+
124
+ private
125
+
126
+ def process(value) = value.upcase
83
127
  end
128
+
129
+ pipeline = Pipeline.new(input: 'hello')
130
+ pipeline.result # => TypeError("Property `result` of `Pipeline` was read before being set")
131
+ pipeline.run
132
+ T.reveal_type(pipeline.result) # => String
133
+ pipeline.result # => 'HELLO'
84
134
  ```
85
135
 
86
- ### Instance Variables
136
+ [Full documentation](docs/internal-props.md)
87
137
 
88
- If you need more instance variables beside defined props, define them within the class body, not the initializer.
138
+ ### Inheritance
139
+
140
+ Props are inherited by subclasses. The Tapioca compiler generates a combined `initialize` signature for each concrete
141
+ class:
89
142
 
90
143
  ```ruby
91
- class MyClass
92
- extend T::Sig
144
+ class Animal
93
145
  include SorbetTyped::Props
94
146
 
95
- prop :my_prop, Integer
147
+ prop :name, String
148
+ end
96
149
 
97
- @my_var = T.let(nil, T.nilable(String))
150
+ class Dog < Animal
151
+ prop :breed, String
152
+ end
153
+
154
+ Dog.new(name: 'Rex', breed: 'Labrador')
155
+ ```
156
+
157
+ → [Full documentation](docs/inheritance.md)
158
+
159
+ ### Phlex integration
160
+
161
+ ```ruby
162
+ class Components::HelloWorld < Phlex::HTML
163
+ extend T::Sig
164
+ include SorbetTyped::Props
165
+
166
+ prop :name, String
98
167
 
99
168
  sig { void }
100
- def foo
101
- T.reveal_type(my_prop) # => Integer
102
- T.reveal_type(@my_var) # => T.nilable(String)
169
+ def view_template
170
+ h1 { "Hello, #{name}!" }
103
171
  end
104
172
  end
105
173
  ```
106
174
 
175
+ > **Known limitation:** Phlex overrides `::new` rather than `#initialize` in `Phlex::SGML`, so Sorbet cannot statically
176
+ > validate props on Phlex components. Calling `HelloWorld.new` without required props will not raise a Sorbet error. See
177
+ > issue [#23](https://gitlab.com/sorbet_typed/props/-/work_items/23).
178
+
179
+ → [Full documentation](docs/phlex.md)
180
+
107
181
  ### Extending initializer logic
108
182
 
109
183
  If you want to implement some logic on instance initialization, implementing a custom `initializer` method would require
@@ -128,8 +202,9 @@ class MyClass
128
202
  end
129
203
  ```
130
204
 
131
- **Note:** you cannot do instance variable initialization here. Define them [within the class body](#instance-variables)
132
- instead.
205
+ [Full documentation](docs/post-init-hook.md)
206
+
207
+ **Note:** you cannot do instance variable initialization here.
133
208
 
134
209
  ### Method Visibility
135
210
 
@@ -186,6 +261,8 @@ my_object.baz
186
261
  my_object.bar # => "baz"
187
262
  ```
188
263
 
264
+ → [Full documentation](docs/method-visibility.md)
265
+
189
266
  Putting prop-definition after an access modifier without arguments does not work:
190
267
 
191
268
  ```ruby
data/docs/defaults.md ADDED
@@ -0,0 +1,198 @@
1
+ # Defaults and optional props
2
+
3
+ ## Table of Contents
4
+
5
+ - [Table of Contents](#table-of-contents)
6
+ - [What they do](#what-they-do)
7
+ - [When to use](#when-to-use)
8
+ - [Options](#options)
9
+ - [Examples](#examples)
10
+ - [Prop with a static default](#prop-with-a-static-default)
11
+ - [Const with a static default](#const-with-a-static-default)
12
+ - [Nilable prop without default — still required (statically)](#nilable-prop-without-default--still-required-statically)
13
+ - [Nilable prop with default nil — optional](#nilable-prop-with-default-nil--optional)
14
+ - [Const with factory — dependency injection](#const-with-factory--dependency-injection)
15
+ - [Behavior notes](#behavior-notes)
16
+
17
+ ## What they do
18
+
19
+ `default:` and `factory:` are native [Sorbet `T::Props`](https://sorbet.org/docs/tstruct) options. They make a prop or
20
+ const optional in `initialize`. When the caller does not supply the argument, the declared default or factory result is
21
+ used instead.
22
+
23
+ ## When to use
24
+
25
+ - A prop has a sensible fallback value that most callers will not need to override.
26
+ - You want to inject a collaborator (e.g. a repository or service object) with a real-world default, while keeping the
27
+ ability to swap it out in tests.
28
+
29
+ ## Options
30
+
31
+ | Option | Value type | Description |
32
+ | ------------------- | ------------------------------ | ----------------------------------------------------------------------------------------------------------------- |
33
+ | `default: value` | Any value matching prop type | Static default. The same object is used across all instances. Use only for immutable values (numbers, Symbols). |
34
+ | `factory: -> { … }` | `Proc` returning the prop type | Called once per instantiation. Use for mutable defaults or objects that must be freshly constructed per instance. |
35
+
36
+ ## Examples
37
+
38
+ ### Prop with a static default
39
+
40
+ ```ruby
41
+ class Counter
42
+ include SorbetTyped::Props
43
+
44
+ prop :count, Integer
45
+ prop :step, Integer, default: 1
46
+ end
47
+
48
+ Counter.new(count: 0) # step defaults to 1
49
+ Counter.new(count: 0, step: 5) # step overridden
50
+ ```
51
+
52
+ Generated `initialize` signature — `count` required, `step` optional:
53
+
54
+ ```ruby
55
+ # sorbet/rbi/dsl/counter.rbi
56
+ # typed: true
57
+
58
+ class Counter
59
+ # Generated from sorbet SorbetTyped::Props
60
+ sig { params(count: ::Integer, step: ::Integer).void }
61
+ def initialize(count:, step: T.unsafe(nil)); end
62
+ end
63
+ ```
64
+
65
+ ### Const with a static default
66
+
67
+ `const` with `default:` is optional in `initialize` but has no writer — the value is fixed after construction.
68
+
69
+ ```ruby
70
+ class Config
71
+ include SorbetTyped::Props
72
+
73
+ const :timeout_seconds, Integer, default: 30
74
+ end
75
+
76
+ Config.new # timeout_seconds == 30
77
+ Config.new(timeout_seconds: 60) # timeout_seconds == 60
78
+ # Config.new.timeout_seconds = 10 # NoMethodError — const has no writer
79
+ ```
80
+
81
+ Generated `initialize` signature:
82
+
83
+ ```ruby
84
+ # sorbet/rbi/dsl/config.rbi
85
+ # typed: true
86
+
87
+ class Config
88
+ # Generated from sorbet SorbetTyped::Props
89
+ sig { params(timeout_seconds: ::Integer).void }
90
+ def initialize(timeout_seconds: T.unsafe(nil)); end
91
+ end
92
+ ```
93
+
94
+ ### Nilable prop without default — still required (statically)
95
+
96
+ Sorbet's native `T::Props` runtime behavior is that `T.nilable` **always implies `default: nil`**, with
97
+ [no way to opt out](https://sorbet.org/docs/tstruct#tnilable-without-implying-default-nil). At actual Ruby runtime, a
98
+ `T.nilable` prop without an explicit `default:`/`factory:` is optional — omitting it simply sets it to `nil`.
99
+
100
+ This gem's Tapioca compiler deliberately diverges from that for the **generated static signature**: it never treats a
101
+ nilable prop as optional unless `default:`/`factory:` is set explicitly. This means `srb tc` will flag a missing nilable
102
+ prop as a static type error, even though it would succeed at runtime. This is intentional — it surfaces "forgot to pass
103
+ this prop" mistakes at type-check time instead of letting them silently become `nil`.
104
+
105
+ ```ruby
106
+ class Notification
107
+ include SorbetTyped::Props
108
+
109
+ prop :message, T.nilable(String)
110
+ end
111
+
112
+ Notification.new(message: nil) # ok
113
+ Notification.new # ok at runtime — message is nil
114
+ # But `srb tc` reports: Missing required keyword argument `message`
115
+ ```
116
+
117
+ ### Nilable prop with default nil — optional
118
+
119
+ Adding `default: nil` makes the prop optional. `nil` is both the type-safe value and the default.
120
+
121
+ ```ruby
122
+ class Notification
123
+ include SorbetTyped::Props
124
+
125
+ prop :message, T.nilable(String), default: nil
126
+ end
127
+
128
+ Notification.new # ok — message is nil
129
+ Notification.new(message: 'hello') # ok
130
+ ```
131
+
132
+ Generated `initialize` signature:
133
+
134
+ ```ruby
135
+ # sorbet/rbi/dsl/notification.rbi
136
+ # typed: true
137
+
138
+ class Notification
139
+ # Generated from sorbet SorbetTyped::Props
140
+ sig { params(message: T.nilable(::String)).void }
141
+ def initialize(message: T.unsafe(nil)); end
142
+ end
143
+ ```
144
+
145
+ ### Const with factory — dependency injection
146
+
147
+ `factory:` provides a proc that constructs a fresh default value for each instance. Combined with `private`, this
148
+ creates a clean dependency injection point: the real implementation is used by default; tests can supply a test double
149
+ by passing it to the constructor.
150
+
151
+ ```ruby
152
+ class OrderRepository
153
+ include SorbetTyped::Props
154
+
155
+ const :database, Database, factory: -> { Database.connect }
156
+ const :cache, Cache, factory: -> { Cache.new }
157
+ private :database, :cache
158
+
159
+ def find(id)
160
+ cache.fetch(id) { database.query(id) }
161
+ end
162
+ end
163
+
164
+ # Production — real database and cache are created by the factory:
165
+ repo = OrderRepository.new
166
+
167
+ # Tests — inject test doubles:
168
+ repo = OrderRepository.new(database: test_db, cache: test_cache)
169
+ ```
170
+
171
+ Generated `initialize` signature — both deps are optional (factory provides defaults):
172
+
173
+ ```ruby
174
+ # sorbet/rbi/dsl/order_repository.rbi
175
+ # typed: true
176
+
177
+ class OrderRepository
178
+ # Generated from sorbet SorbetTyped::Props
179
+ sig { params(database: Database, cache: Cache).void }
180
+ def initialize(database: T.unsafe(nil), cache: T.unsafe(nil)); end
181
+ end
182
+ ```
183
+
184
+ ## Behavior notes
185
+
186
+ - Props with `default:` or `factory:` become optional keyword arguments in the generated `initialize`. They appear after
187
+ all required props in the signature, regardless of declaration order.
188
+ - `factory:` is called once per object construction. It is safe for mutable objects (arrays, hashes, service instances).
189
+ - `default:` reuses the same object across all instances. Do **not** use `default: []` or `default: {}` — use
190
+ `factory: -> { [] }` instead.
191
+ - The Tapioca compiler treats `default:` and `factory:` identically — both produce optional keyword arguments in the
192
+ generated signature.
193
+ - For the difference between a `T.nilable` prop and an internal prop that is never in `initialize`, see
194
+ [Internal props](internal-props.md).
195
+ - Native sorbet `T::Props` runtime behavior: `T.nilable` always implies `default: nil`, with no opt-out
196
+ ([sorbet docs](https://sorbet.org/docs/tstruct#tnilable-without-implying-default-nil)). This gem's static signature
197
+ generation intentionally does not mirror that — see
198
+ [Nilable prop without default — still required (statically)](#nilable-prop-without-default--still-required-statically).
@@ -0,0 +1,139 @@
1
+ # Inheritance
2
+
3
+ ## Table of Contents
4
+
5
+ - [Table of Contents](#table-of-contents)
6
+ - [What it does](#what-it-does)
7
+ - [When to use](#when-to-use)
8
+ - [Examples](#examples)
9
+ - [Subclass inherits parent props](#subclass-inherits-parent-props)
10
+ - [Child adds a required prop](#child-adds-a-required-prop)
11
+ - [Child adds a prop with a default](#child-adds-a-prop-with-a-default)
12
+ - [Behavior notes](#behavior-notes)
13
+
14
+ ## What it does
15
+
16
+ Props declared in a parent class are inherited by subclasses. The Tapioca compiler generates a separate `initialize` RBI
17
+ for each concrete class that has props, including all inherited ones.
18
+
19
+ ## When to use
20
+
21
+ - You have a base class that defines common constructor parameters shared by all subclasses.
22
+ - You want subclasses to extend the constructor with additional props without repeating the parent's declarations.
23
+
24
+ ## Examples
25
+
26
+ ### Subclass inherits parent props
27
+
28
+ ```ruby
29
+ class Animal
30
+ include SorbetTyped::Props
31
+
32
+ prop :name, String
33
+ prop :weight, Float
34
+ end
35
+
36
+ class Dog < Animal
37
+ prop :breed, String
38
+ end
39
+
40
+ Dog.new(name: 'Rex', weight: 30.5, breed: 'Labrador')
41
+ ```
42
+
43
+ Parent RBI — generated for `Animal`:
44
+
45
+ ```ruby
46
+ # sorbet/rbi/dsl/animal.rbi
47
+ # typed: true
48
+
49
+ class Animal
50
+ # Generated from sorbet SorbetTyped::Props
51
+ sig { params(name: ::String, weight: ::Float).void }
52
+ def initialize(name:, weight:); end
53
+ end
54
+ ```
55
+
56
+ Child RBI — includes both parent and child props:
57
+
58
+ ```ruby
59
+ # sorbet/rbi/dsl/dog.rbi
60
+ # typed: true
61
+
62
+ class Dog < Animal
63
+ # Generated from sorbet SorbetTyped::Props
64
+ sig { params(name: ::String, weight: ::Float, breed: ::String).void }
65
+ def initialize(name:, weight:, breed:); end
66
+ end
67
+ ```
68
+
69
+ ### Child adds a required prop
70
+
71
+ ```ruby
72
+ class Vehicle
73
+ include SorbetTyped::Props
74
+
75
+ const :make, String
76
+ const :model, String
77
+ end
78
+
79
+ class ElectricVehicle < Vehicle
80
+ const :range_km, Integer
81
+ end
82
+
83
+ ElectricVehicle.new(make: 'Tesla', model: 'Model 3', range_km: 450)
84
+ ```
85
+
86
+ Generated child RBI:
87
+
88
+ ```ruby
89
+ # sorbet/rbi/dsl/electric_vehicle.rbi
90
+ # typed: true
91
+
92
+ class ElectricVehicle < Vehicle
93
+ # Generated from sorbet SorbetTyped::Props
94
+ sig { params(make: ::String, model: ::String, range_km: ::Integer).void }
95
+ def initialize(make:, model:, range_km:); end
96
+ end
97
+ ```
98
+
99
+ ### Child adds a prop with a default
100
+
101
+ ```ruby
102
+ class Notification
103
+ include SorbetTyped::Props
104
+
105
+ const :recipient, String
106
+ const :subject, String
107
+ end
108
+
109
+ class EmailNotification < Notification
110
+ const :priority, String, default: 'normal'
111
+ end
112
+
113
+ EmailNotification.new(recipient: 'user@example.com', subject: 'Hello')
114
+ # priority defaults to 'normal'
115
+ ```
116
+
117
+ Generated child RBI — `priority` is optional:
118
+
119
+ ```ruby
120
+ # sorbet/rbi/dsl/email_notification.rbi
121
+ # typed: true
122
+
123
+ class EmailNotification < Notification
124
+ # Generated from sorbet SorbetTyped::Props
125
+ sig { params(recipient: ::String, subject: ::String, priority: ::String).void }
126
+ def initialize(recipient:, subject:, priority: T.unsafe(nil)); end
127
+ end
128
+ ```
129
+
130
+ ## Behavior notes
131
+
132
+ - Props are inherited in definition order: parent props come first in the generated `initialize` signature, followed by
133
+ child props.
134
+ - Required props appear before optional props (those with `default:` or `factory:`), regardless of declaration order
135
+ across parent and child.
136
+ - The Tapioca compiler generates a separate RBI for each concrete class. Each RBI includes all inherited props.
137
+ - Abstract classes (those with `abstract!`) are skipped by the Tapioca compiler — no `initialize` RBI is generated for
138
+ them.
139
+ - Props cannot be removed or re-typed in a subclass.