Bringing Primary Constructors to Dart Dart 3.13 introduces primary constructors, a syntactic sugar feature that simplifies class declarations without adding new functionality, according to a blog post by a Dart language team member. The design process involved extensive iteration to balance complexity and usability, with the feature aimed at improving code readability while maintaining backward compatibility. Bringing Primary Constructors to Dart An inside look at the design decisions, trade-offs, and syntactic sugar behind adding primary constructors to Dart 3.13. AI disclosure: I wrote every sentence of this myself—including the em dashes. My favorite feature in Dart 3.13 is primary constructors https://dart.dev/language/primary-constructors . Getting there took a lot of time and iteration before the language team had a design we felt was solid. Since many of you have been patiently waiting for this feature, I thought it would be worth writing about some of the challenges we worked through to bring this large syntax change to Dart. On syntactic sugar on-syntactic-sugar Users have been asking for something like primary constructors for years. It's a highly desired feature, which is kind of strange when you think about it. Primary constructors don't let you do anything you can't already do in Dart. They're just a different—hopefully better —syntax for what you can already express. In the 1960s, Peter Landin coined the term "syntactic sugaring" to refer to layering some textual niceties on top of a more fundamental but unpleasant language. Today, we tend to use the term more like a noun and call features like these "syntactic sugar". The immortal enemy of every programming language is complexity. Even the tiniest feature must be designed, specified, implemented, tested, and documented. The cost is large. I think of complexity in a language like weight in an airplane. Some amount of it is necessary for the thing to work, but you have to be careful to not add weight unnecessarily or risk the whole apparatus not getting off the ground. From that angle, syntactic sugar seems like a bad idea. It's additional complexity with no additional utility. Even worse, once we add it, we pass complexity onto our users too. Now they have to choose which syntax to use each time they are trying to express something. When are these kinds of features ever a good idea? I admit I feel some need to justify this because so much of my work over the past several years has been adding these kinds of features to Dart. I think syntactic sugar can carry its weight in a couple of ways: The new way is simply better the-new-way-is-simply-better Despite our somewhat robotic affect and fondness for EBNF https://en.wikipedia.org/wiki/Extended Backus%E2%80%93Naur form , we language designers are human and make mistakes. Further, we are always learning, the ecosystem we serve is constantly discovering new ways to make software, and user expectations drift over time. When Dart was first designed, you had to use an explicit new keyword to call a constructor. This was deliberate to be familiar to users coming from C++, Java, JavaScript, and other languages. The intent was to make it clearer in the code when a call allocates a new object. As garbage collectors got better and users got more comfortable with automatic memory management, most users found new to be more noise than signal. Also, honestly, Dart has always undermined that signal by supporting factory constructors https://dart.dev/language/constructors factory-constructors . A factory constructor can return some previously created object even when you invoke it with new . In Dart 2.0, we shipped a language change that allowed you to omit the new keyword and const in many places when calling a constructor. We still support the old syntax, so this language change is essentially syntactic sugar, but we really only kept the old syntax around for backwards compatibility. We always want you to use the new shorter syntax. We shipped tooling https://dart.dev/tools/dart-fix to automatically remove the unnecessary new keywords, and have a lint https://dart.dev/tools/linter-rules/unnecessary new that reminds you when you forget. The old syntax is effectively deprecated and over time you see it less and less. If you're new to Dart, you may not have even realized we supported using new in constructor calls. That means the complexity for supporting constructor calls both with and without new is low. There is a transition cost for existing users to learn the new syntax. But new users will mostly just learn the new way and never encounter the old. There's little cognitive load when choosing between the two syntaxes because you simply always use the new one and our tools will gently remind you if you don't . Short of having a time machine to go back and do it right the first time, this is the next best thing we can do to fix a mistake in the language. The syntax can be much better for a common use case the-syntax-can-be-much-better-for-a-common-use-case For the first several years of its public existence, Dart had no support for enum declarations. The language didn't let you write: enum Color { red, blue, yellow } Instead, you had to write something like this: js class Color { static const Color red = Color. 0, 'red' ; static const Color blue = Color. 1, 'blue' ; static const Color yellow = Color. 2, 'yellow' ; ​ const Color. this.index, this.name ; ​ final int index; final String name; } Old Java heads will remember this as Josh Bloch's "typesafe enum pattern". Under the hood, this more verbose class declaration does almost exactly the same thing as an enum declaration in Dart today. Dart enum declarations are almost entirely sugar. I say "almost" because enum declarations give you exhaustiveness checks https://dart.dev/language/branches exhaustiveness-checking in switches. However, as you can see from these two examples, enum declarations are really nice sugar. A simple enum declaration unpacks to a lot of Dart code. Now, if almost no one was writing enumerated types, then it might still not be worth adding syntax to optimize for this use case. But in a language that prioritizes type safety and data validation, enums are quite common. The Flutter framework alone defines dozens of them. A relatively small amount of syntactic sugar can sometimes make a lot of user code shorter and simpler. The syntax can make the intent clearer the-syntax-can-make-the-intent-clearer The previous section makes it sound like brevity is the whole point. I suppose in a world where we are increasingly paying AI agents per-token costs to read and write code there is a direct financial incentive. But it's not just about character count. Consider: js class Color { static const Color red = Color 'red', 0xff0000 ; static const Color blue = Color 'blue', 0x0000ff ; static const Color yellow = Color 'yellow', 0xff00ff ; ​ const Color this.name, this.rgb ; ​ final String name; final int rgb; } Is this an enumerated type? By that, I really mean enumerated : Should someone using this class assume that the only instances of Color they will have to worry about are red , blue , or yellow ? Note that the constructor is public, so other libraries are free to invoke the constructor and create other colors. Is the intent of this class to be a closed list of colors, or an open factory of them with a handful of pre-defined values? Reading the code, we don't know. The code is a lot of machinery that defines a type and some constants. It looks like the code you'd write if you did want an enum, but the machinery doesn't reveal the intent. The code tells the compiler what the code means, but it doesn't tell a reader how to use it. If we change this to an enum declaration, then the policy that it's a closed set of values becomes obvious. And, now that Dart has real enums, choosing to not change this code to an enum declaration likely sends a signal that it's not a closed set. For me, this is a compelling reason to add syntactic sugar. Code is written and executed as syntax, but what every user working with the code cares about is what it means —its semantics. To maintain code correctly, we need to understand its intentions and policy. This is increasingly true in a world where AI is often generating code faster than we have time to diligently review it. Even when it's possible to make the compiler do what you want by cobbling together the machinery of several existing language features, it can be worth it to have syntactic sugar that yields the same behavior because better syntax raises that behavior into a higher level of abstraction where the intended semantics are more obvious. That can reduce the cognitive work required to understand the code even though the entire language is more complex. Why primary constructors why-primary-constructors Right, I'm supposed to be talking about primary constructors, not enums and new keywords. Though—foreshadowing —I will be talking about new too. For many years, the 1 open issue https://github.com/dart-lang/language/issues/314 on the Dart language repo has been a feature request for data classes. If you don't know, data classes are a feature in Kotlin https://kotlinlang.org/docs/data-classes.html that lets you define a class with some fields, and the compiler gives you equality, hash code, and some other stuff for free. If you read through the hundreds of comments on that issue, you'll see that most users are less interested in the value semantics part—the equality and hash code bits. It's mostly about having an easier way to define a class that has a constructor and stores some state. That functionality actually comes from a different, more fundamental feature in Kotlin: primary constructors https://kotlinlang.org/docs/classes.html primary-constructor . I believe Kotlin got this idea from Scala https://docs.scala-lang.org/scala3/book/domain-modeling-tools.html classes . Since then, C https://learn.microsoft.com/en-us/dotnet/csharp/programming-guide/classes-and-structs/instance-constructors primary-constructors and Java https://openjdk.org/jeps/395 have added their own takes on the concept. The value semantics part of data classes is useful too. We are exploring that separately https://github.com/dart-lang/language/blob/main/working/value-classes/feature-specification.md . There are two key pieces to primary constructors: - You can define a constructor by writing a parameter list right inside the class header. That avoids needing to write a keyword or repeat the class name to declare the constructor. It also avoids two levels of nesting and indentation, one for the class body and one for the constructor parameter list, in very simple classes that only contain some state. - Inside that parameter list, you can indicate that some parameters should declare corresponding instance fields that are automatically initialized from the parameter. Without primary constructors or any other kind of syntactic sugar, we have to do something like this in Dart: class Point { final int x; final int y; ​ Point int x, int y : x = x, y = y; } In this example, we had to write the class name twice. For each bit of state, we wrote its type twice and its name four times. It's not too heinous in this example because there are only two fields and the names are all short. Once you start dealing with complex domain-specific stuff with long names and piles of state, it gets ugly. This is not a new problem, and Dart has long had a bit of syntactic sugar called " initializing formals https://dart.dev/language/constructors use-initializing-formal-parameters " to help: class Point { final int x; final int y; ​ Point this.x, this.y ; } Using this. on constructor parameters means you only have to write each field's type once and name twice. Better But you still have to write the class name twice and each field's name twice. Initializing formals are nice, but users still tell us they don't go far enough. On borrowing features from other languages on-borrowing-features-from-other-languages So some users coming to Dart from another language tell us that they miss a feature. What do we do with that kind of feedback? Personally, I like borrowing features from other languages. The creators of those languages have already put a lot of work into designing and validating the feature. We can learn a lot from them, and that other language is an existence proof that the feature is conceptually coherent and tractable to implement. Taking inspiration from other languages can also make our language easier to learn. Unless a user is completely new to programming, they aren't learning Dart from scratch. They come to us with all that they have already learned from other languages. What remains for them to learn is the difference between what they know and what Dart contains. When we borrow syntax and semantics from other languages, we reduce the size of that difference and lower the effort to learn Dart. This philosophy has been key to Dart's success. From little semicolons all the way up to classes, Dart was designed through and through to be familiar and easy to learn for users of other mainstream languages like JavaScript, Java, and C . At the same time, good language design is contextual and holistic. "What's a good pair of shoes?" has very different answers when you are standing on the arctic tundra versus a Hawaiian beach. A language feature that works beautifully in, say, Rust might not slot gracefully into Dart with its distinct syntax, semantics, history, user base, and ecosystem. I don't want Dart to feel like Frankenstein's monster stitched together from body parts ripped off of other languages. Thus, when the Dart language team looks at features from other languages, we're simultaneously looking at how the feature solves problems in that language's context and also at how well that context matches Dart's own. Adding primary constructors to Dart adding-primary-constructors-to-dart We knew users wanted a nicer notation to define a class that initializes some fields from constructor parameters. With primary constructors, you write the constructor and the compiler synthesizes the fields. A language could also go the other way. You write the field declarations and the compiler gives you the constructor for free. Swift does that with memberwise initializers https://docs.swift.org/swift-book/documentation/the-swift-programming-language/initialization/ Memberwise-Initializers-for-Structure-Types . A challenge any time your language derives two declarations from one piece of syntax is that one syntax needs to handle all of the various ways you might configure both of those declarations. In our case here, the instance field may be final or not. It might have metadata like @override or doc comments on it. The constructor can be named or unnamed, const or not. A constructor parameter can be positional or named, optional or required. If it's optional, it might need to specify a default value. We spent some time investigating inferring a constructor from field declarations https://github.com/dart-lang/language/issues/698 , but eventually decided that parameters were the more useful declaration for a user to hand-author. Since the constructor is often public API, it's important to control the signature fully: the constructor's name and const -ness, which parameters are named or positional, the order of the positional ones, and their default values. In order to infer an instance field from a constructor parameter, the only missing piece a user needs to provide is whether the field should be final. It's fairly natural to allow a leading final or var on the parameter to control that. The absence of both modifiers then means the parameter doesn't declare an instance field at all. That's similar to what Scala and Kotlin do with val and var . The result in Dart looks like this: class Point final int x, final int y, ; Since primary constructors make empty class bodies more common, we also now allow you to use ; instead of {} for an empty class body. On syntactic cliffs on-syntactic-cliffs This looks pretty nice, but what if the primary constructor also needs a body or an initializer list? One option is to simply say, "Well, in that case, don't use a primary constructor." Syntactic sugar often takes a subset of use cases and offers more concise syntax for them. If you fall outside of that subset, it's reasonable to require the user to fall back to the older, more elaborate syntax. That's the right call in some cases. But the language team is very mindful that code evolves over time. Let's say you're writing a class. It starts off simple with just a few fields initialized from constructor parameters: class FormatterOptions { final int indent = 0, final int pageWidth = 80, } { // ... } A perfect use case for a primary constructor. Later you add some more fields and parameters. Great. Before long, you have a constructor with a bunch of parameters declaring fields: class FormatterOptions final int indent = 0, final int pageWidth = 80, final Version? languageVersion, final TrailingCommas? trailingCommas, final bool followLinks = false, final Show show = Show.changed, final Output output = Output.write, final Summary summary = Summary.none, final bool setExitIfChanged = false, final List