skip to content

In Dart 3.12 and later, what does a private named parameter like {required this._currency} do, and where is it not allowed?

level: middleimportance: nice to knowfreq 18%

answer

  1. public argument, private field
  2. the underscore is stripped for callers
  3. initializing formals only
  4. no underscore-only names
  5. the old initializer-list boilerplate

basics

~20 s

Since Dart 3.12 a constructor can write {required this._currency}: the field stays private while callers pass currency:. It works only for initializing formals, not for ordinary named parameters, and replaces an initializer list that just stripped the underscore.

solid answer

~40 s

Before 3.12 a named parameter could not start with an underscore, so a private field needed `PriceFormatter({required String currency}) : _currency = currency;`. Dart 3.12 lets you write `PriceFormatter({required this._currency, this._decimals = 2})`: the compiler strips the underscore, so callers write `currency:` and `decimals:`, while the fields stay library-private. Like any named parameter it can be optional, required or defaulted; in the initializer list you use the private name, and a subclass forwards it with the public name, `super.currency`. Limits: it applies only to initializing formals, so `void f({String? _x})` is still an error; the public name must be a valid identifier, so `this._` is out; it must not clash with another parameter; and the package needs language version 3.12 or later.

code

dart · 20 lines
dart
class PriceFormatter {
  PriceFormatter({required this._currency, this._decimals = 2})
    : assert(_decimals >= 0);

  final String _currency;
  final int _decimals;

  String format(num amount) =>
      '${amount.toStringAsFixed(_decimals)} $_currency';
}

class WholeUnitFormatter extends PriceFormatter {
  WholeUnitFormatter({required super.currency}) : super(decimals: 0);
}

void main() {
  final eur = PriceFormatter(currency: 'EUR');
  print(eur.format(12.5)); // 12.50 EUR
  print(WholeUnitFormatter(currency: 'JPY').format(1200)); // 1200 JPY
}

go deeper

for a junior

Recall that {required this._x} lets callers write x: while the field stays private, and that it needs Dart 3.12.

for a middle

Explain that the underscore is stripped only for initializing formals, how super parameters use the public name, and the naming constraints.

for a senior

Plan the migration: raise the SDK lower bound, use dart fix for prefer_initializing_formals, and review that no field was meant to be public.

for a principal

Weigh encapsulation against simplicity in shared models: private fields with public constructor names versus plain public final fields.

## The problem it solves Dart makes a field **private to its library** by starting its name with an underscore: `_currency`. Until Dart 3.12, a named parameter could not start with an underscore, so a constructor that wanted a **public argument name** but a **private field** needed boilerplate: ```dart class PriceFormatter { final String _currency; PriceFormatter({required String currency}) : _currency = currency; } ``` The initializer list exists only to strip the underscore. ## What Dart 3.12 added With a language version of 3.12 or later, a named **initializing formal** can use the private name directly, and the compiler strips the underscore for callers: ```dart class PriceFormatter { final String _currency; final int _decimals; PriceFormatter({required this._currency, this._decimals = 2}); } final fmt = PriceFormatter(currency: 'EUR', decimals: 0); ``` - Callers write the **public name** `currency:`; the field stays `_currency`. - The parameter can be **optional or required**, and it can have a **default value**, like any named parameter. - Inside the constructor's initializer list, you refer to it by its **private name**, for example `: assert(_decimals >= 0)`. - A subclass forwarding it with a super parameter uses the **public name**: `Money({required super.currency})`. ## The constraints | Rule | Example that fails | |---|---| | Only for **initializing formals** (`this._field`) | `void f({String? _name})` on a function stays an error | | The public name must be a **valid identifier** | `this._` or `this._2x` have no public counterpart | | **No conflicts** with other parameter names | `{this._x, int? x}` in the same constructor | | Needs **language version 3.12+** | a package whose SDK lower bound is below 3.12 | The first row is the key interview point: named parameters in general still cannot be private. The feature is an exception for parameters that exist only to initialise a private field. ## Tooling around it - The `prefer_initializing_formals` lint, in the `recommended` and `flutter` sets, now highlights named parameters that could become private named parameters, and `dart fix --code=prefer_initializing_formals` rewrites them. - Dart 3.13's **primary constructors** build on the same idea: parameters in the class header can declare fields directly. That is a separate feature; the private-named-parameter rules above still apply to ordinary constructors. ## When to use it 1. **Immutable value classes and formatters** with private state and a public constructor API: the scenario above. 2. **Flutter widgets** whose configuration is stored in private fields for encapsulation while the constructor keeps public argument names. 3. **Migrating older code**: delete initializer lists whose only job was `_x = x`, which removes a class of copy-paste mistakes where the wrong field was assigned. Do not reach for it when the field should simply be public; a public `final` field with `this.currency` stays the simplest option. ## Common mistakes - **Writing the underscore at the call site**: `PriceFormatter(_currency: 'EUR')` fails; callers only ever see `currency`. - **Expecting it on functions**: a top-level or instance method still cannot have a named parameter called `_label`; the exception exists only because `this._field` names a private field. - **Forgetting the language version**: a package whose SDK lower bound is below 3.12 keeps getting the old error even when built with a newer SDK, because the feature is gated on the package's language version, not on the installed SDK. - **Forwarding with the private name**: a subclass writes `super.currency`, never `super._currency`. ## What an interviewer listens for - That callers see the public name while the field is private. - That it works only for `this._field` initializing formals, not for arbitrary named parameters. - The version requirement, 3.12. - The before-and-after: an initializer list that existed only to strip the underscore.

  • How does a Dart subclass forward a private named parameter of its superclass?
    It uses the public name in a super parameter: if the base class declares `Tool({required this._price})`, the subclass writes `Hammer({required super.price})`. The public name is the one callers and subclasses see; only the base class's own initializer list refers to `_price`.
  • What did Dart code look like before private named parameters, and how do you migrate it?
    It declared a public-named parameter and copied it in the initializer list, `Point({required double x}) : _x = x;`. Once the package's SDK lower bound is 3.12 or later, `prefer_initializing_formals` flags these, and `dart fix --code=prefer_initializing_formals` can rewrite them to `Point({required this._x})`.

saying these in an interview costs you the question

  • Thinks callers must now write the underscore name
  • Believes any named parameter can now be private
  • Says the field becomes public
  • Uses super._price in a subclass
  • Assumes it works on Dart versions before 3.12