Skip to content

Allow nillable hash params#692

Open
ignacio-chiazzo wants to merge 1 commit intomainfrom
solve-sorbet-nilable-args-issue
Open

Allow nillable hash params#692
ignacio-chiazzo wants to merge 1 commit intomainfrom
solve-sorbet-nilable-args-issue

Conversation

@ignacio-chiazzo
Copy link
Copy Markdown
Member

@ignacio-chiazzo ignacio-chiazzo commented Mar 26, 2026

The Tapioca JobIteration DSL compiler expands a build_enumerator params argument typed as a Sorbet shape (T::Types::FixedHash) into separate keyword parameters on perform / perform_now / perform_ltaer.

For optional shape keys, Sorbet represents the value as T.nilable(inner), which at runtime is a T::Types::Union. The compiler already turned those into KwOptParams, but it always used default: "nil". For an inner T::Hash[...] (or untyped Hash), the natural Ruby default for an optional keyword is {}, not nil. Generating nil makes the RBI misleading and out of line with typical call sites.

This change keeps default: "nil" for non-hash nilable types and uses default: "{}" when the nilable’s non-nil inner type is hash-like (T::Types::TypedHash or Simple with Hash).

Problem

Before — shape with an optional hash flag:

Params = T.type_alias { { user_id: Integer, flags: T.nilable(T::Hash[Symbol, T::Boolean]) } }
sig { params(params: Params, cursor: T.untyped).returns(T::Array[T.untyped]) }
def build_enumerator(params, cursor:)
  # ...
end

Generated RBI include flags as:

def perform(user_id:, flags: nil); end

After— same signature, generated RBI matches the usual default for an optional hash keyword:

def perform(user_id:, flags: {}); end`

Proposed solution

Detect nilable FixedHash values via the runtime type, not only name.start_with?("T.nilable"): require T::Types::Union, then use unwrap_nilable to get the non-nil inner type (same representation Sorbet uses for T.nilable).

Choose the RBI default string from that inner type:

If it is hash-like — T::Types::TypedHash (e.g. T::Hash[K, V]) or T::Types::Simple with raw_type == Hash — use "{}". Otherwise use "nil" (unchanged for e.g. T.nilable(Integer)).

@ignacio-chiazzo ignacio-chiazzo force-pushed the solve-sorbet-nilable-args-issue branch 2 times, most recently from 7cb3ddf to cce64c5 Compare March 26, 2026 22:10
@ignacio-chiazzo ignacio-chiazzo force-pushed the solve-sorbet-nilable-args-issue branch from cce64c5 to 4511c12 Compare March 26, 2026 22:25
@ignacio-chiazzo ignacio-chiazzo marked this pull request as ready for review March 26, 2026 22:26
@ignacio-chiazzo ignacio-chiazzo requested a review from a team as a code owner March 26, 2026 22:26
@Mangara
Copy link
Copy Markdown
Contributor

Mangara commented Mar 27, 2026

Generating nil makes the RBI misleading and out of line with typical call sites.

Does this actually cause problems? Is the default used for type checking (other than the presence / absence of a default value)?

For an inner hash, the natural Ruby default for an optional keyword is {}, not nil.

Given def perform(params), if you call it with perform_later(user_id: 0), won't params[:flags] be nil? Isn't def perform_later(user_id:, flags: nil); end more accurate in that sense?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants