{"id":20684017,"url":"https://github.com/aaronc81/sorbet_duck","last_synced_at":"2026-05-29T08:04:34.114Z","repository":{"id":98729911,"uuid":"253340170","full_name":"AaronC81/sorbet_duck","owner":"AaronC81","description":"Statically-checked duck typing (structural typing) for Sorbet. Quack!","archived":false,"fork":false,"pushed_at":"2022-06-10T20:46:58.000Z","size":10,"stargazers_count":2,"open_issues_count":0,"forks_count":1,"subscribers_count":1,"default_branch":"master","last_synced_at":"2026-01-13T06:12:27.139Z","etag":null,"topics":["duck-typing","ruby","sorbet","structural-typing"],"latest_commit_sha":null,"homepage":null,"language":"Ruby","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"mit","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/AaronC81.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null}},"created_at":"2020-04-05T21:58:36.000Z","updated_at":"2025-05-12T19:05:34.000Z","dependencies_parsed_at":"2023-05-25T01:15:21.517Z","dependency_job_id":null,"html_url":"https://github.com/AaronC81/sorbet_duck","commit_stats":null,"previous_names":[],"tags_count":0,"template":false,"template_full_name":null,"purl":"pkg:github/AaronC81/sorbet_duck","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronC81%2Fsorbet_duck","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronC81%2Fsorbet_duck/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronC81%2Fsorbet_duck/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronC81%2Fsorbet_duck/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/AaronC81","download_url":"https://codeload.github.com/AaronC81/sorbet_duck/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/AaronC81%2Fsorbet_duck/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":33642320,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-05-26T15:22:16.424Z","status":"online","status_checked_at":"2026-05-29T02:00:06.066Z","response_time":107,"last_error":null,"robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":true,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["duck-typing","ruby","sorbet","structural-typing"],"created_at":"2024-11-16T22:18:39.613Z","updated_at":"2026-05-29T08:04:34.098Z","avatar_url":"https://github.com/AaronC81.png","language":"Ruby","funding_links":[],"categories":[],"sub_categories":[],"readme":"# Sorbet Duck\n\nSorbet Duck is a\n**tool which adds statically-checked duck typing (sometimes called structural typing) to Sorbet using static code generation**. This means you can\n**define a Sorbet type which accepts any object with a particular method**.\n\nSuppose we wanted to define our own `empty?` function, and allow it to accept\nabsolutely any object which has a `length` method returning an Integer. This\nisn't possible in pure Sorbet (you'd need to explicitly implement an interface\non all types passed in), but you can do it with Sorbet Duck! It looks like this:\n\n```ruby\n# Define our statically-checked duck type\n#      ,--- A name to describe what this type is checking for\n#      |            ,--- The method to check for\n#      |            |             ,--- The expected sig body of that method\n#     .-------.   .----.    .--------------.\nduck(:HasLength, :length) { returns(Integer) }\n#               \n#                     ,--- Now use our duck type!\n#               .-------------.\nsig { params(x: Duck::HasLength).returns(T::Boolean) }\ndef empty?(x)\n    x.length == 0\nend\n\n# Later...\nempty?([1, 2, 3]) # passes static type check\nempty?(\"hello\")   # also passes\nempty?(64)        # fails static type check - there is no Integer#length method\n```\n\nSorbet Duck runs as a pre-processing step before Sorbet, generating a single\nRuby file which creates interfaces and implements them where required.\n\nThis works using Sorbet's LSP implementation to detect what new interface\nimplementations are needed, then dynamically generates them using\n[Parlour](https://github.com/AaronC81/parlour).\n\nThis is **absolutely not production-ready**: it has no formal tests, has\nseveral limitations (see below), and is all-round a bit clunky. Still, it's an\ninteresting proof-of-concept to show that some amount of duck typing is possible\nin Sorbet.\n\n## Limitations\n- **The method specified for the duck type can't take parameters**. This should\n  be fairly easy to implement, but I haven't got round to it yet.\n- The only methods supported for the duck type are instance methods on classes.\n- The duck type can only specify a single method requirement.\n\n## Usage\n1. Add `sorbet_duck` to your Gemfile/gemspec and install it\n2. Make sure you require `sorbet_duck`, like you would require `sorbet-runtime`\n3. Define your duck types as shown in the example above\n4. Run `srb-duck` in the root of your project to generate `duck.rb` (don't\n   require `duck.rb` at runtime, this is entirely for static checking)\n5. Run `srb tc` to typecheck your project\n\nYou **must run `srb-duck` before every time you run `srb tc`** to regenerate\n`duck.rb`. If you want to make this easy, you could always create a Rake task\nwhich runs one after the other.\n\n## Implementation\nFirst of all, no waterfowl were harmed in the creation of this gem.\n\nThis works by generating an interface for each defined duck type (like\n`HasLength` in that usage example), and then implementing that interface for any\ntype we attempt to pass into a method accepting that duck type. The interfaces\nand implementations are written into `duck.rb`. (This can't be `duck.rbi` and\nI'm not entirely sure why...)\n\nThe process of doing this is roughly as follows:\n\n1. Find all duck type definitions (usages of `duck`) by searching the project's\n   AST with the `ffast` gem\n2. Define interfaces corresponding to these duck types in Parlour's RBI object\n   tree\n3. Write that to a file so the interfaces are resolvable by Sorbet\n4. Launch Sorbet's LSP implementation, connect to it, and use the type errors\n   to find which types have been passed into methods accepting duck types\n5. Implement the corresponding duck type interfaces for those types, saving\n   these implementations to Parlour's RBI object tree\n\n## Troubleshooting\n- You must be using Bundler! Sorbet Duck invokes some shell commands which\n  assume `bundle exec` is available.\n- If Sorbet ever generates `hidden-definitions` or similar for the `Duck` \n  module, it'll stop typechecking correctly.","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronc81%2Fsorbet_duck","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Faaronc81%2Fsorbet_duck","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Faaronc81%2Fsorbet_duck/lists"}