Build1 publisher2 min readPublished
Storing PdfDocument.new in a Dart enum member deletes the factory switch ladder
Dart's enhanced enums and constructor tearoffs let each member carry the constructor it builds, so adding a case is one line in one file, as long as every target constructor takes exactly the same arguments.
The Engineer · Build desk

What happened
- Dart 2.15 turned constructors into first-class closures, so a default constructor can be passed as a function with .new and a named constructor by its own name.
- Dart 2.17's enhanced enums let an enum declare final fields, take a const constructor, implement interfaces and mixins, and define methods, getters and operator overloads.
- A dev.to post combines the two: each DocumentType member passes a constructor tearoff into a final field typed Document Function(String title), supplied through a const enum constructor.
- In the post's main, const selectedType = DocumentType.markdown followed by selectedType.create('Architecture_Notes.md') yields a MarkdownDocument whose render prints Rendering Markdown: Architecture_Notes.md.
- The shape the post starts from is a ten-line buildNotification function that switches on NotificationType and returns one of three widget classes, with no default clause.
Compiled by The EngineerSomething wrong?How this is made
Why it matters
- decision Teams that keep enums in a domain layer now have to choose: let that layer import the concrete widget classes it names, or keep the factory where the implementations already live.
- capability Adding a document type becomes a one-line edit in one file instead of a coordinated change to the enum and a separate factory.
- cost A package adopting the pattern raises its SDK lower bound to 2.17; consumers pinned below that cannot take the upgrade.
The constraint is in the field declaration. `final Document Function(String title) create;` is the line every member has to satisfy [6]. PdfDocument, MarkdownDocument and HtmlDocument each take one String title in the post's listing, so all three tearoffs fit [7]. Give HtmlDocument a second required argument and its tearoff no longer matches that field type, and the case goes back to a wrapper lambda or a switch [6]. When the signatures do line up, the type checker carries the exhaustiveness; the post notes that a new enum value cannot be added without supplying a matching constructor tearoff [10].
`pdf(PdfDocument.new)` sits inside the enum body, so PdfDocument has to be in scope wherever DocumentType is declared [4]. In the example the post opens with, the three targets are widgets: EmailNotificationWidget, SmsNotificationWidget and PushNotificationWidget [11]. An enum written that way imports the widget layer. The post's complaint about the old shape is that creation logic is "split across multiple files and layers" [13]; this version collects it in the file that can see every implementation.
The post promises self-instantiating factories "in under 15 lines of code" [14]. For three cases the enum body is seven lines plus a comment [2], against the ten-line switch function it replaces [11], a saving of three lines that would not on its own justify restructuring anything [8]. The difference that compounds is per case. A switch case and its return are two lines, plus one more for the bare enum member; the enhanced enum spends one line, in one file [3].
The const constructor is the good part. Because `const DocumentType(this.create)` is const, the post says, "the entire enum remains compile-time constant" [9], and `const selectedType = DocumentType.markdown` in its main is a constant expression [8]. Membership is then fixed at compile time. A `Map<NotificationType, Function>` registry, which the post lists among the shapes to escape [12], can be written to at runtime, and the const enum field cannot [7]. If types have to be registered from outside your compilation unit, the map is the workable option.
The post says the pattern shines when deserializing polymorphic JSON payloads such as webhooks, analytics events and push notifications, and the text breaks off at "Imagine" before that example arrives [15]. The reconstruction is mechanical. Named constructors tear off by name, as in the post's `final parsers = [User.fromJson];` [3], so the field would take the decoded payload, not a title string [5]. Both language features are required, so a package adopting this sets its SDK lower bound to 2.17 [1].
What to watch
- The next installment in the series, and what field signature it uses for polymorphic JSON parsers.
- Whether package authors adopting the pattern raise their SDK floor to 2.17 or keep a switch for consumers pinned lower.
- Whether layering lints that forbid domain code importing the UI library rule out the enum-holds-its-widget form in Flutter apps.