Hey view_component people! I'm filing this issue to solicit opinions on whether we should adopt static type checking into view_component and which system(s) we should consider. Off the top of my head, there are three options (but please feel free to suggest others):
RBI + Sorbet
RBS + some type checker
rux
Some definitions:
Sorbet: Stripe's type annotation and static type checking library.
RBI: So-called "Ruby Interface" files used by Sorbet. Generally .rbi files are used instead of having annotations in the code itself, i.e. when supplying types for a 3rd-party lib, etc.
RBS: A language for defining Ruby types written by the Ruby maintainers themselves.
rux: A Ruby transpiler I wrote that's capable of generating RBI files from inline type annotations.
Motivation
Type information can be extremely handy for developing and using Ruby libraries. Here are the major benefits as I see them:
Increased confidence in code correctness. Type checking can catch subtle (or even overt) bugs.
Helpful for IDE-level tooling like language servers and intellisense/autocomplete.
Makes code easier to understand because types help indicate intent (i.e. "is this thing a string or...?" vs "ah, this thing is a string, that means I can do x with it'). Leads to faster onboarding of new contributors.
Examples
In each of these examples I'm going to be annotating ViewComponent::Base#render_in.
Sorbet + RBI
Sorbet type annotations are written in Ruby and defined in the code itself. Separate .rbi files can be stored in the project's sorbet/rbi directory and are generally used to specify types in 3rd-party libraries, etc. RBI files are also Ruby code, but with method bodies and such missing.
Notice the nice little type alias for returning strings of HTML :)
Sorbet is perhaps the most mature of the three systems, but there are a couple of drawbacks in my opinion:
The sig blocks distract from the method bodies. I find my eyes having a harder time finding the method I'm looking for because the sigs get in the way.
sigs are executable Ruby code, meaning the sig method has to be defined on the class when the class is loaded. Sorbet comes with runtime type checking as well as static, but if you don't want it you have to ship your library with the sorbet-runtime gem anyway and disable the checks. This feels ugly and cumbersome to me. Why should I have to ship a dependency with my lib that effectively doesn't do anything?
RBS
RBS works differently than Sorbet in that all type definitions live outside the code in separate .rbs files. As of now, there's no type checker available that can actually check RBS-defined types, but the Stripe folks say Sorbet will eventually be able to use them.
It's nice that all the types can be defined inline, but there are some drawbacks:
Separate .rbs files mean having to keep type info perpetually in sync with Ruby code, which seems like a huge hassle and something we're likely to forget. It's reminiscent of the old SASS/LESS days where you'd have both a .sass and an .html.erb file with identical hierarchies that you'd have to keep matched up. Ugh.
I personally don't like that wonky block syntax.
Rux
Rux was originally written as a JSX-inspired way to render view components, but is becoming a Ruby transpiler framework of sorts. I have a branch that can extract inline type annotations and spit out .rbi files that can then be type checked by Sorbet. Although totally accidental, rux and RBS use very similar syntax. The difference of course is that RBS types are defined in a separate file while rux types are specified inline. Rux types are heavily influenced by Python's mypy.
NOTE: Right now type aliases aren't supported, but they wouldn't be hard to add.
@camertron thanks for bringing this up! I've been wondering how types might benefit both writing ViewComponent itself and consumers of the framework. I'd be interested to hear your thoughts on using static types when authoring ViewComponents.
As for which tool I'd prefer, I'm not sure. I think you've assessed our options here fairly. I don't know if we have any sort of preference internally.
I know we've considered investigating autocorrect/intellisense for Primer ViewComponents. Perhaps we could start with an experiment that adds that functionality there? I believe it might be possible with our YARD annotations as well.
Yeah, our YARD annotations could be a great stepping-stone, and I'd love to experiment with a prototype 😄
Here are a few other thoughts from one of our Slack conversations:
How would we type system_arguments? They can literally contain anything. Sorbet lets you specify a single type for keyword arguments, i.e. Float or String , but I don't think that's powerful enough for our use case. RBS has a record type that does what we want, but it's not clear if it works with keyword args.
We define a lot of methods dynamically. Fortunately Sorbet and RBS both support putting type definitions in a separate file, meaning a tool could auto-generate the signatures for them. I don't think such tooling exists right now, so we'd have to write it ourselves.
Feature request
Hey view_component people! I'm filing this issue to solicit opinions on whether we should adopt static type checking into view_component and which system(s) we should consider. Off the top of my head, there are three options (but please feel free to suggest others):
Some definitions:
Motivation
Type information can be extremely handy for developing and using Ruby libraries. Here are the major benefits as I see them:
Examples
In each of these examples I'm going to be annotating
ViewComponent::Base#render_in.Sorbet + RBI
Sorbet type annotations are written in Ruby and defined in the code itself. Separate .rbi files can be stored in the project's sorbet/rbi directory and are generally used to specify types in 3rd-party libraries, etc. RBI files are also Ruby code, but with method bodies and such missing.
Notice the nice little type alias for returning strings of HTML :)
Sorbet is perhaps the most mature of the three systems, but there are a couple of drawbacks in my opinion:
sigblocks distract from the method bodies. I find my eyes having a harder time finding the method I'm looking for because thesigs get in the way.sigs are executable Ruby code, meaning thesigmethod has to be defined on the class when the class is loaded. Sorbet comes with runtime type checking as well as static, but if you don't want it you have to ship your library with the sorbet-runtime gem anyway and disable the checks. This feels ugly and cumbersome to me. Why should I have to ship a dependency with my lib that effectively doesn't do anything?RBS
RBS works differently than Sorbet in that all type definitions live outside the code in separate .rbs files. As of now, there's no type checker available that can actually check RBS-defined types, but the Stripe folks say Sorbet will eventually be able to use them.
It's nice that all the types can be defined inline, but there are some drawbacks:
Rux
Rux was originally written as a JSX-inspired way to render view components, but is becoming a Ruby transpiler framework of sorts. I have a branch that can extract inline type annotations and spit out .rbi files that can then be type checked by Sorbet. Although totally accidental, rux and RBS use very similar syntax. The difference of course is that RBS types are defined in a separate file while rux types are specified inline. Rux types are heavily influenced by Python's mypy.
NOTE: Right now type aliases aren't supported, but they wouldn't be hard to add.
Rux files are transpiled to Ruby using the
ruxctool, or optionally automatically onrequire.Please let me know what you think!
The text was updated successfully, but these errors were encountered: