Effective Dart: Design

Here are some guidelines for writing consistent, usable APIs for libraries.

Names

Naming is an important part of writing readable, maintainable code.The following best practices can help you achieve that goal.

DO use terms consistently.

Use the same name for the same thing, throughout your code. If a precedentalready exists outside your API that users are likely to know, follow thatprecedent.

  1. pageCount // A field.
  2. updatePageCount() // Consistent with pageCount.
  3. toSomething() // Consistent with Iterable's toList().
  4. asSomething() // Consistent with List's asMap().
  5. Point // A familiar concept.
  1. renumberPages() // Confusingly different from pageCount.
  2. convertToSomething() // Inconsistent with toX() precedent.
  3. wrappedAsSomething() // Inconsistent with asX() precedent.
  4. Cartesian // Unfamiliar to most users.

The goal is to take advantage of what the user already knows. This includestheir knowledge of the problem domain itself, the conventions of the corelibraries, and other parts of your own API. By building on top of those, youreduce the amount of new knowledge they have to acquire before they can beproductive.

AVOID abbreviations.

Unless the abbreviation is more common than the unabbreviated term, don’tabbreviate. If you do abbreviate, capitalize it correctly.

  1. pageCount
  2. buildRectangles
  3. IOStream
  4. HttpRequest
  1. numPages // "num" is an abbreviation of number(of)
  2. buildRects
  3. InputOutputStream
  4. HypertextTransferProtocolRequest

PREFER putting the most descriptive noun last.

The last word should be the most descriptive of what the thing is. You canprefix it with other words, such as adjectives, to further describe the thing.

  1. pageCount // A count (of pages).
  2. ConversionSink // A sink for doing conversions.
  3. ChunkedConversionSink // A ConversionSink that's chunked.
  4. CssFontFaceRule // A rule for font faces in CSS.
  1. numPages // Not a collection of pages.
  2. CanvasRenderingContext2D // Not a "2D".
  3. RuleFontFaceCss // Not a CSS.

CONSIDER making the code read like a sentence.

When in doubt about naming, write some code that uses your API, and try to readit like a sentence.

  1. // "If errors is empty..."
  2. if (errors.isEmpty) ...
  3.  
  4. // "Hey, subscription, cancel!"
  5. subscription.cancel();
  6.  
  7. // "Get the monsters where the monster has claws."
  8. monsters.where((monster) => monster.hasClaws);
  1. // Telling errors to empty itself, or asking if it is?
  2. if (errors.empty) ...
  3.  
  4. // Toggle what? To what?
  5. subscription.toggle();
  6.  
  7. // Filter the monsters with claws *out* or include *only* those?
  8. monsters.filter((monster) => monster.hasClaws);

It’s helpful to try out your API and see how it “reads” when used in code, butyou can go too far. It’s not helpful to add articles and other parts of speechto force your names to literally read like a grammatically correct sentence.

  1. if (theCollectionOfErrors.isEmpty) ...
  2.  
  3. monsters.producesANewSequenceWhereEach((monster) => monster.hasClaws);

PREFER a noun phrase for a non-boolean property or variable.

The reader’s focus is on what the property is. If the user cares more abouthow a property is determined, then it should probably be a method with averb phrase name.

  1. list.length
  2. context.lineWidth
  3. quest.rampagingSwampBeast
  1. list.deleteItems

PREFER a non-imperative verb phrase for a boolean property or variable.

Boolean names are often used as conditions in control flow, so you want a namethat reads well there. Compare:

  1. if (window.closeable) ... // Adjective.
  2. if (window.canClose) ... // Verb.

Good names tend to start with one of a few kinds of verbs:

  • a form of “to be”: isEnabled, wasShown, willFire. These are, by far,the most common.

  • an auxiliary verb: hasElements, canClose,shouldConsume, mustSave.

  • an active verb: ignoresInput, wroteFile. These are rare because they areusually ambiguous. loggedResult is a bad name because it could mean“whether or not a result was logged” or “the result that was logged”.Likewise, closingConnection could be “whether the connection is closing”or “the connection that is closing”. Active verbs are allowed when the namecan only be read as a predicate.

What separates all these verb phrases from method names is that they are notimperative. A boolean name should never sound like a command to tell theobject to do something, because accessing a property doesn’t change the object.(If the property does modify the object in a meaningful way, it should be amethod.)

  1. isEmpty
  2. hasElements
  3. canClose
  4. closesWindow
  5. canShowPopup
  6. hasShownPopup
  1. empty // Adjective or verb?
  2. withElements // Sounds like it might hold elements.
  3. closeable // Sounds like an interface.
  4. // "canClose" reads better as a sentence.
  5. closingWindow // Returns a bool or a window?
  6. showPopup // Sounds like it shows the popup.

Exception: Input properties in Angular components sometimes useimperative verbs for boolean setters because these setters are invoked intemplates, not from other Dart code.

CONSIDER omitting the verb for a named boolean parameter.

This refines the previous rule. For named parameters that are boolean, the nameis often just as clear without the verb, and the code reads better at the callsite.

  1. Isolate.spawn(entryPoint, message, paused: false);
  2. var copy = List.from(elements, growable: true);
  3. var regExp = RegExp(pattern, caseSensitive: false);

PREFER the “positive” name for a boolean property or variable.

Most boolean names have conceptually “positive” and “negative” forms where theformer feels like the fundamental concept and the latter is itsnegation—“open” and “closed”, “enabled” and “disabled”, etc. Often thelatter name literally has a prefix that negates the former: “visible” and“in-visible”, “connected” and “dis-connected”, “zero” and “non-zero”.

When choosing which of the two cases that true represents — and thuswhich case the property is named for — prefer the positive or morefundamental one. Boolean members are often nested inside logical expressions,including negation operators. If your property itself reads like a negation,it’s harder for the reader to mentally perform the double negation andunderstand what the code means.

  1. if (socket.isConnected && database.hasData) {
  2. socket.write(database.read());
  3. }
  1. if (!socket.isDisconnected && !database.isEmpty) {
  2. socket.write(database.read());
  3. }

For some properties, there is no obvious positive form. Is a document that hasbeen flushed to disk “saved” or “un-changed”? Is a document that hasn’t beenflushed “un-saved” or “changed”? In ambiguous cases, lean towards the choicethat is less likely to be negated by users or has the shorter name.

Exception: With some properties, the negative form is what usersoverwhelmingly need to use. Choosing the positive case would force them tonegate the property with ! everywhere. Instead, it may be better to use thenegative case for that property.

PREFER an imperative verb phrase for a function or method whose main purpose is a side effect.

Callable members can return a result to the caller and perform other work orside effects. In an imperative language like Dart, members are often calledmainly for their side effect: they may change an object’s internal state,produce some output, or talk to the outside world.

Those kinds of members should be named using an imperative verb phrase thatclarifies the work the member performs.

  1. list.add("element");
  2. queue.removeFirst();
  3. window.refresh();

This way, an invocation reads like a command to do that work.

PREFER a noun phrase or non-imperative verb phrase for a function or method if returning a value is its primary purpose.

Other callable members have few side effects but return a useful result to thecaller. If the member needs no parameters to do that, it should generally be agetter. But, sometimes a logical “property” needs some parameters. For example,elementAt() returns a piece of data from a collection, but it needs aparameter to know which piece of data to return.

This means the member is syntactically a method, but conceptually it is aproperty, and should be named as such using a phrase that describes what themember returns.

  1. var element = list.elementAt(3);
  2. var first = list.firstWhere(test);
  3. var char = string.codeUnitAt(4);

This guideline is deliberately softer than the previous one. Sometimes a methodhas no side effects but is still simpler to name with a verb phrase likelist.take() or string.split().

CONSIDER an imperative verb phrase for a function or method if you want to draw attention to the work it performs.

When a member produces a result without any side effects, it should usually be agetter or a method with a noun phrase name describing the result it returns.However, sometimes the work required to produce that result is important. It maybe prone to runtime failures, or use heavyweight resources like networking orfile I/O. In cases like this, where you want the caller to think about the workthe member is doing, give the member a verb phrase name that describes thatwork.

  1. var table = database.downloadData();
  2. var packageVersions = packageGraph.solveConstraints();

Note, though, that this guideline is softer than the previous two. The work anoperation performs is often an implementation detail that isn’t relevant to thecaller, and performance and robustness boundaries change over time. Most of thetime, name your members based on what they do for the caller, not how theydo it.

AVOID starting a method name with get.

In most cases, the method should be a getter with get removed from the name.For example, instead of a method named getBreakfastOrder(), define a getternamed breakfastOrder.

Even if the member does need to be a method because it takes arguments orotherwise isn’t a good fit for a getter, you should still avoid get. Like theprevious guidelines state, either:

  • Simply drop get and use a noun phrase name like breakfastOrder()if the caller mostly cares about the value the method returns.

  • Use a verb phrase name if the caller cares about the work being done,but pick a verb that more precisely describes the work than get, likecreate, download, fetch, calculate, request, aggregate, etc.

PREFER naming a method to_() if it copies the object’s state to a new object.

Linter rule: use_to_and_as_if_applicable

A conversion method is one that returns a new object containing a copy ofalmost all of the state of the receiver but usually in some different form orrepresentation. The core libraries have a convention that these methods arenamed starting with to followed by the kind of result.

If you define a conversion method, it’s helpful to follow that convention.

  1. list.toSet();
  2. stackTrace.toString();
  3. dateTime.toLocal();

PREFER naming a method as_() if it returns a different representation backed by the original object.

Linter rule: use_to_and_as_if_applicable

Conversion methods are “snapshots”. The resulting object has its own copy of theoriginal object’s state. There are other conversion-like methods that returnviews—they provide a new object, but that object refers back to theoriginal. Later changes to the original object are reflected in the view.

The core library convention for you to follow is as().<!—?code-excerpt “misc/lib/effective_dart/design_good.dart (as)”?—>

  1. var map = table.asMap();
  2. var list = bytes.asFloat32List();
  3. var future = subscription.asFuture();

AVOID describing the parameters in the function’s or method’s name.

The user will see the argument at the callsite, so it usually doesn’t helpreadability to also refer to it in the name itself.

  1. list.add(element);
  2. map.remove(key);
  1. list.addElement(element)
  2. map.removeKey(key)

However, it can be useful to mention a parameter to disambiguate it from othersimilarly-named methods that take different types:

  1. map.containsKey(key);
  2. map.containsValue(value);

DO follow existing mnemonic conventions when naming type parameters.

Single letter names aren’t exactly illuminating, but almost all generic typesuse them. Fortunately, they mostly use them in a consistent, mnemonic way.The conventions are:

  • E for the element type in a collection:
  1. class IterableBase<E> {}
  2. class List<E> {}
  3. class HashSet<E> {}
  4. class RedBlackTree<E> {}
  • K and V for the key and value types in an associativecollection:
  1. class Map<K, V> {}
  2. class Multimap<K, V> {}
  3. class MapEntry<K, V> {}
  • R for a type used as the return type of a function or a class’smethods. This isn’t common, but appears in typedefs sometimes and in classesthat implement the visitor pattern:
  1. abstract class ExpressionVisitor<R> {
  2. R visitBinary(BinaryExpression node);
  3. R visitLiteral(LiteralExpression node);
  4. R visitUnary(UnaryExpression node);
  5. }
  • Otherwise, use T, S, and U for generics that have a single typeparameter and where the surrounding type makes its meaning obvious. Thereare multiple letters here to allow nesting without shadowing a surroundingname. For example:
  1. class Future<T> {
  2. Future<S> then<S>(FutureOr<S> onValue(T value)) => ...
  3. }

Here, the generic method then<S>() uses S to avoid shadowing the Ton Future<T>.

If none of the above cases are a good fit, then either another single-lettermnemonic name or a descriptive name is fine:

  1. class Graph<N, E> {
  2. final List<N> nodes = [];
  3. final List<E> edges = [];
  4. }
  5.  
  6. class Graph<Node, Edge> {
  7. final List<Node> nodes = [];
  8. final List<Edge> edges = [];
  9. }

In practice, the existing conventions cover most type parameters.

Libraries

A leading underscore character ( _ ) indicates that a member is private to itslibrary. This is not mere convention, but is built into the language itself.

PREFER making declarations private.

A public declaration in a library—either top level or in a class—isa signal that other libraries can and should access that member. It is also acommitment on your library’s part to support that and behave properly when ithappens.

If that’s not what you intend, add the little _ and be happy. Narrow publicinterfaces are easier for you to maintain and easier for users to learn. As anice bonus, the analyzer will tell you about unused private declarations so youcan delete dead code. It can’t do that if the member is public because itdoesn’t know if any code outside of its view is using it.

CONSIDER declaring multiple classes in the same library.

Some languages, such as Java, tie the organization of files to the organization ofclasses—each file may only define a single top level class. Dart does nothave that limitation. Libraries are distinct entities separate from classes.It’s perfectly fine for a single library to contain multiple classes, top levelvariables, and functions if they all logically belong together.

Placing multiple classes together in one library can enable some usefulpatterns. Since privacy in Dart works at the library level, not the class level,this is a way to define “friend” classes like you might in C++. Every classdeclared in the same library can access each other’s private members, but codeoutside of that library cannot.

Of course, this guideline doesn’t mean you should put all of your classes intoa huge monolithic library, just that you are allowed to place more than oneclass in a single library.

Classes and mixins

Dart is a “pure” object-oriented language in that all objects are instances ofclasses. But Dart does not require all code to be defined inside aclass—you can define top-level variables, constants, and functions likeyou can in a procedural or functional language.

AVOID defining a one-member abstract class when a simple function will do.

Linter rule: one_member_abstracts

Unlike Java, Dart has first-class functions, closures, and a nice light syntaxfor using them. If all you need is something like a callback, just use afunction. If you’re defining a class and it only has a single abstract memberwith a meaningless name like call or invoke, there is a good chance youjust want a function.

  1. typedef Predicate<E> = bool Function(E element);
  1. abstract class Predicate<E> {
  2. bool test(E element);
  3. }

AVOID defining a class that contains only static members.

Linter rule: avoid_classes_with_only_static_members

In Java and C#, every definition must be inside a class, so it’s common to see“classes” that exist only as a place to stuff static members. Other classes areused as namespaces—a way to give a shared prefix to a bunch of members torelate them to each other or avoid a name collision.

Dart has top-level functions, variables, and constants, so you don’t need aclass just to define something. If what you want is a namespace, a library is abetter fit. Libraries support import prefixes and show/hide combinators. Thoseare powerful tools that let the consumer of your code handle name collisions inthe way that works best for them.

If a function or variable isn’t logically tied to a class, put it at the toplevel. If you’re worried about name collisions, give it a more precise name ormove it to a separate library that can be imported with a prefix.

  1. DateTime mostRecent(List<DateTime> dates) {
  2. return dates.reduce((a, b) => a.isAfter(b) ? a : b);
  3. }
  4.  
  5. const _favoriteMammal = 'weasel';
  1. class DateUtils {
  2. static DateTime mostRecent(List<DateTime> dates) {
  3. return dates.reduce((a, b) => a.isAfter(b) ? a : b);
  4. }
  5. }
  6.  
  7. class _Favorites {
  8. static const mammal = 'weasel';
  9. }

In idiomatic Dart, classes define kinds of objects. A type that is neverinstantiated is a code smell.

However, this isn’t a hard rule. With constants and enum-like types, it may benatural to group them in a class.

  1. class Color {
  2. static const red = '#f00';
  3. static const green = '#0f0';
  4. static const blue = '#00f';
  5. static const black = '#000';
  6. static const white = '#fff';
  7. }

AVOID extending a class that isn’t intended to be subclassed.

If a constructor is changed from a generative constructor to a factoryconstructor, any subclass constructor calling that constructor will break.Also, if a class changes which of its own methods it invokes on this, thatmay break subclasses that override those methods and expect them to be calledat certain points.

Both of these mean that a class needs to be deliberate about whether or not itwants to allow subclassing. This can be communicated in a doc comment, or bygiving the class an obvious name like IterableBase. If the author of the classdoesn’t do that, it’s best to assume you should not extend the class.Otherwise, later changes to it may break your code.

DO document if your class supports being extended.

This is the corollary to the above rule. If you want to allow subclasses of yourclass, state that. Suffix the class name with Base, or mention it in theclass’s doc comment.

AVOID implementing a class that isn’t intended to be an interface.

Implicit interfaces are a powerful tool in Dart to avoid having to repeat thecontract of a class when it can be trivially inferred from the signatures of animplementation of that contract.

But implementing a class’s interface is a very tight coupling to that class. Itmeans virtually any change to the class whose interface you are implementingwill break your implementation. For example, adding a new member to a class isusually a safe, non-breaking change. But if you are implementing that class’sinterface, now your class has a static error because it lacks an implementationof that new method.

Library maintainers need the ability to evolve existing classes without breakingusers. If you treat every class like it exposes an interface that users are freeto implement, then changing those classes becomes very difficult. Thatdifficulty in turn means the libraries you rely on are slower to grow and adaptto new needs.

To give the authors of the classes you use more leeway, avoid implementingimplicit interfaces except for classes that are clearly intended to beimplemented. Otherwise, you may introduce a coupling that the author doesn’tintend, and they may break your code without realizing it.

DO document if your class supports being used as an interface.

If your class can be used as an interface, mention that in the class’s doccomment.

DO use mixin to define a mixin type.

Dart originally didn’t have a separate syntax for declaring a class intended tobe mixed in to other classes. Instead, any class that met certain restrictions(no non-default constructor, no superclass, etc.) could be used as a mixin. Thiswas confusing because the author of the class might not have intended it to bemixed in.

Dart 2.1.0 added a mixin keyword for explicitly declaring a mixin. Typescreated using that can only be used as mixins, and the language also ensuresthat your mixin stays within the restrictions. When defining a new type that youintend to be used as a mixin, use this syntax.

  1. mixin ClickableMixin implements Control {
  2. bool _isDown = false;
  3.  
  4. void click();
  5.  
  6. void mouseDown() {
  7. _isDown = true;
  8. }
  9.  
  10. void mouseUp() {
  11. if (_isDown) click();
  12. _isDown = false;
  13. }
  14. }

You might still encounter older code using class to define mixins, but the newsyntax is preferred.

AVOID mixing in a type that isn’t intended to be a mixin.

For compatibility, Dart still allows you to mix in classes that aren’t definedusing mixin. However, that’s risky. If the author of the class doesn’t intendthe class to be used as a mixin, they might change the class in a way thatbreaks the mixin restrictions. For example, if they add a constructor, yourclass will break.

If the class doesn’t have a doc comment or an obvious name like IterableMixin,assume you cannot mix in the class if it isn’t declared using mixin.

Constructors

Dart constructors are created by declaring a function with the same name as theclass and, optionally, an additional identifier. The latter are called namedconstructors.

CONSIDER making your constructor const if the class supports it.

If you have a class where all the fields are final, and the constructor doesnothing but initialize them, you can make that constructor const. That letsusers create instances of your class in places where constants arerequired—inside other larger constants, switch cases, default parametervalues, etc.

If you don’t explicitly make it const, they aren’t able to do that.

Note, however, that a const constructor is a commitment in your public API. Ifyou later change the constructor to non-const, it will break users that arecalling it in constant expressions. If you don’t want to commit to that, don’tmake it const. In practice, const constructors are most useful for simple,immutable data record sorts of classes.

Members

A member belongs to an object and can be either methods or instance variables.

PREFER making fields and top-level variables final.

Linter rule: prefer_final_fields

State that is not mutable—that does not change over time—iseasier for programmers to reason about. Classes and libraries that minimize theamount of mutable state they work with tend to be easier to maintain.

Of course, it is often useful to have mutable data. But, if you don’t need it,your default should be to make fields and top-level variables final when youcan.

DO use getters for operations that conceptually access properties.

Deciding when a member should be a getter versus a method is a challenging,subtle, but important part of good API design, hence this very long guideline.Some other language’s cultures shy away from getters. They only use them whenthe operation is almost exactly like a field—it does a miniscule amount ofcalculation on state that lives entirely on the object. Anything more complex orheavyweight than that gets () after the name to signal “computation goin’ onhere!” because a bare name after a . means “field”.

Dart is not like that. In Dart, all dotted names are member invocations thatmay do computation. Fields are special—they’re getters whoseimplementation is provided by the language. In other words, getters are not“particularly slow fields” in Dart; fields are “particularly fast getters”.

Even so, choosing a getter over a method sends an important signal to thecaller. The signal, roughly, is that the operation is “field-like”. Theoperation, at least in principle, could be implemented using a field, as faras the caller knows. That implies:

  • The operation does not take any arguments and returns a result.

  • The caller cares mostly about the result. If you want the caller toworry about how the operation produces its result more than they do theresult being produced, then give the operation a verb name that describesthe work and make it a method.

This does not mean the operation has to be particularly fast in order tobe a getter. IterableBase.length is O(n), and that’s OK. It’s fine for agetter to do significant calculation. But if it does a surprising amountof work, you may want to draw their attention to that by making it a methodwhose name is a verb describing what it does.

  1. connection.nextIncomingMessage; // Does network I/O.
  2. expression.normalForm; // Could be exponential to calculate.
  • The operation does not have user-visible side effects. Accessing a realfield does not alter the object or any other state in the program. Itdoesn’t produce output, write files, etc. A getter shouldn’t do those thingseither.

The “user-visible” part is important. It’s fine for getters to modify hiddenstate or produce out of band side effects. Getters can lazily calculate andstore their result, write to a cache, log stuff, etc. As long as the callerdoesn’t care about the side effect, it’s probably fine.

  1. stdout.newline; // Produces output.
  2. list.clear; // Modifies object.
  • The operation is idempotent. “Idempotent” is an odd word that, in thiscontext, basically means that calling the operation multiple times producesthe same result each time, unless some state is explicitly modified betweenthose calls. (Obviously, list.length produces different results if you addan element to the list between calls.)

“Same result” here does not mean a getter must literally produce anidentical object on successive calls. Requiring that would force manygetters to have brittle caching, which negates the whole point of using agetter. It’s common, and perfectly fine, for a getter to return a new futureor list each time you call it. The important part is that the futurecompletes to the same value, and the list contains the same elements.

In other words, the result value should be the same in the aspects that thecaller cares about.

  1. DateTime.now; // New result each time.
  • The resulting object doesn’t expose all of the original object’s state.A field exposes only a piece of an object. If your operation returns aresult that exposes the original object’s entire state, it’s likely betteroff as a to_() or as_() method.

If all of the above describe your operation, it should be a getter. It seemslike few members would survive that gauntlet, but surprisingly many do. Manyoperations just do some computation on some state and most of those can andshould be getters.

  1. rectangle.area;
  2. collection.isEmpty;
  3. button.canShow;
  4. dataSet.minimumValue;

DO use setters for operations that conceptually change properties.

Linter rule: use_setters_to_change_properties

Deciding between a setter versus a method is similar to deciding between agetter versus a method. In both cases, the operation should be “field-like”.

For a setter, “field-like” means:

  • The operation takes a single argument and does not produce a resultvalue.

  • The operation changes some state in the object.

  • The operation is idempotent. Calling the same setter twice with the samevalue should do nothing the second time as far as the caller is concerned.Internally, maybe you’ve got some cache invalidation or logging going on.That’s fine. But from the caller’s perspective, it appears that the secondcall does nothing.

  1. rectangle.width = 3;
  2. button.visible = false;

DON’T define a setter without a corresponding getter.

Linter rule: avoid_setters_without_getters

Users think of getters and setters as visible properties of an object. A“dropbox” property that can be written to but not seen is confusing andconfounds their intuition about how properties work. For example, a setterwithout a getter means you can use = to modify it, but not +=.

This guideline does not mean you should add a getter just to permit the setteryou want to add. Objects shouldn’t generally expose more state than they needto. If you have some piece of an object’s state that can be modified but notexposed in the same way, use a method instead.

Exception: An Angular component class may expose setters that areinvoked from a template to initialize the component. Often, these setters arenot intended to be invoked from Dart code and don’t need a corresponding getter.(If they are used from Dart code, they should have a getter.)

AVOID returning null from members whose return type is bool, double, int, or num.

Linter rule: avoid_returning_null

Even though all types are nullable in Dart, users assume those types almostnever contain null, and the lowercase names encourage a “Java primitive”mindset.

It can be occasionally useful to have a “nullable primitive” type in your API,for example to indicate the absence of a value for some key in a map, but theseshould be rare.

If you do have a member of this type that may return null, document it veryclearly, including the conditions under which null will be returned.

AVOID returning this from methods just to enable a fluent interface.

Linter rule: avoid_returning_this

Method cascades are a better solution for chaining method calls.

  1. var buffer = StringBuffer()
  2. ..write('one')
  3. ..write('two')
  4. ..write('three');
  1. var buffer = StringBuffer()
  2. .write('one')
  3. .write('two')
  4. .write('three');

Types

When you write down a type in your program, you constrain the kinds of valuesthat flow into different parts of your code. Types can appear in two kinds ofplaces: type annotations on declarations and type arguments to genericinvocations.

Type annotations are what you normally think of when you think of “statictypes”. You can type annotate a variable, parameter, field, or return type. Inthe following example, bool and String are type annotations. They hang offthe static declarative structure of the code and aren’t “executed” at runtime.

  1. bool isEmpty(String parameter) {
  2. bool result = parameter.isEmpty;
  3. return result;
  4. }

A generic invocation is a collection literal, a call to a generic class’sconstructor, or an invocation of a generic method. In the next example, numand int are type arguments on generic invocations. Even though they are types,they are first-class entities that get reified and passed to the invocation atruntime.

  1. var lists = <num>[1, 2];
  2. lists.addAll(List<num>.filled(3, 4));
  3. lists.cast<int>();

We stress the “generic invocation” part here, because type arguments can _also_appear in type annotations:

  1. List<int> ints = [1, 2];

Here, int is a type argument, but it appears inside a type annotation, not ageneric invocation. You usually don’t need to worry about this distinction, butin a couple of places, we have different guidance for when a type is used in ageneric invocation as opposed to a type annotation.

In most places, Dart allows you to omit a type annotation and infers a type foryou based on the nearby context, or defaults to the dynamic type. The factthat Dart has both type inference and a dynamic type leads to some confusionabout what it means to say code is “untyped”. Does that mean the code isdynamically typed, or that you didn’t write the type? To avoid that confusion,we avoid saying “untyped” and instead use the following terminology:

  • If the code is type annotated, the type was explicitly written in thecode.

  • If the code is inferred, no type annotation was written, and Dartsuccessfully figured out the type on its own. Inference can fail, in whichcase the guidelines don’t consider that inferred. In some places, inferencefailure is a static error. In others, Dart uses dynamic as the fallbacktype.

  • If the code is dynamic, then its static type is the special dynamictype. Code can be explicitly annotated dynamic or it can be inferred.

In other words, whether some code is annotated or inferred is orthogonal towhether it is dynamic or some other type.

Inference is a powerful tool to spare you the effort of writing and readingtypes that are obvious or uninteresting. Omitting types in obvious cases alsodraws the reader’s attention to explicit types when those types are important,for things like casts.

Explicit types are also a key part of robust, maintainable code. They define thestatic shape of an API. They document and enforce what kinds of values areallowed to reach different parts of the program.

The guidelines here strike the best balance we’ve found between brevity andexplicitness, flexibility and safety. When deciding which types to write, youneed to answer two questions:

  • Which types should I write because I think it’s best for them to be visible inthe code?
  • Which types should I write because inference can’t provide them for me?

These guidelines help you answer the first question:

These cover the second:

The remaining guidelines cover other more specific questions around types.

PREFER type annotating public fields and top-level variables if the type isn’t obvious.

Linter rule: type_annotate_public_apis

Type annotations are important documentation for how a library should be used.They form boundaries between regions of a program to isolate the source of atype error. Consider:

  1. install(id, destination) => ...

Here, it’s unclear what id is. A string? And what is destination? A stringor a File object? Is this method synchronous or asynchronous? This is clearer:

  1. Future<bool> install(PackageId id, String destination) => ...

In some cases, though, the type is so obvious that writing it is pointless:

  1. const screenWidth = 640; // Inferred as int.

“Obvious” isn’t precisely defined, but these are all good candidates:

  • Literals.
  • Constructor invocations.
  • References to other constants that are explicitly typed.
  • Simple expressions on numbers and strings.
  • Factory methods like int.parse(), Future.wait(), etc. that readers areexpected to be familiar with.

When in doubt, add a type annotation. Even when a type is obvious, you may stillwish to explicitly annotate. If the inferred type relies on values ordeclarations from other libraries, you may want to type annotate _your_declaration so that a change to that other library doesn’t silently change thetype of your own API without you realizing.

CONSIDER type annotating private fields and top-level variables if the type isn’t obvious.

Linter rule: prefer_typing_uninitialized_variables

Type annotations on your public declarations help users of your code. Types onprivate members help maintainers. The scope of a private declaration issmaller and those who need to know the type of that declaration are also morelikely to be familiar with the surrounding code. That makes it reasonable tolean more heavily on inference and omit types for private declarations, which iswhy this guideline is softer than the previous one.

If you think the initializer expression—whatever it is—issufficiently clear, then you may omit the annotation. But if you thinkannotating helps make the code clearer, then add one.

AVOID type annotating initialized local variables.

Linter rule: omit_local_variable_types

Local variables, especially in modern code where functions tend to be small,have very little scope. Omitting the type focuses the reader’s attention on themore important name of the variable and its initialized value.

  1. List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  2. var desserts = <List<Ingredient>>[];
  3. for (var recipe in cookbook) {
  4. if (pantry.containsAll(recipe)) {
  5. desserts.add(recipe);
  6. }
  7. }
  8.  
  9. return desserts;
  10. }
  1. List<List<Ingredient>> possibleDesserts(Set<Ingredient> pantry) {
  2. List<List<Ingredient>> desserts = <List<Ingredient>>[];
  3. for (List<Ingredient> recipe in cookbook) {
  4. if (pantry.containsAll(recipe)) {
  5. desserts.add(recipe);
  6. }
  7. }
  8.  
  9. return desserts;
  10. }

If the local variable doesn’t have an initializer, then its type can’t beinferred. In that case, it is a good idea to annotate. Otherwise, you getdynamic and lose the benefits of static type checking.

  1. List<AstNode> parameters;
  2. if (node is Constructor) {
  3. parameters = node.signature;
  4. } else if (node is Method) {
  5. parameters = node.parameters;
  6. }

AVOID annotating inferred parameter types on function expressions.

Anonymous functions are almost always immediately passed to a method taking acallback of some type. (If the function isn’t used immediately, it’s usuallyworth making it a named declaration.) When a function expression is created in atyped context, Dart tries to infer the function’s parameter types based on theexpected type.

For example, when you pass a function expression to Iterable.map(), yourfunction’s parameter type is inferred based on the type of callback that map()expects:

  1. var names = people.map((person) => person.name);
  1. var names = people.map((Person person) => person.name);

In rare cases, the surrounding context is not precise enough to provide a typefor one or more of the function’s parameters. In those cases, you may need toannotate.

AVOID redundant type arguments on generic invocations.

A type argument is redundant if inference would fill in the same type. If theinvocation is the initializer for a type-annotated variable, or is an argumentto a function, then inference usually fills in the type for you:

  1. Set<String> things = Set();
  1. Set<String> things = Set<String>();

Here, the type annotation on the variable is used to infer the type argument ofconstructor call in the initializer.

In other contexts, there isn’t enough information to infer the type and then youshould write the type argument:

  1. var things = Set<String>();
  1. var things = Set();

Here, since the variable has no type annotation, there isn’t enough context todetermine what kind of Set to create, so the type argument should be providedexplicitly.

DO annotate when Dart infers the wrong type.

Sometimes, Dart infers a type, but not the type you want. For example, you maywant a variable’s type to be a supertype of the initializer’s type so that youcan later assign some other sibling type to the variable:

  1. num highScore(List<num> scores) {
  2. num highest = 0;
  3. for (var score in scores) {
  4. if (score > highest) highest = score;
  5. }
  6. return highest;
  7. }
  1. num highScore(List<num> scores) {
  2. var highest = 0;
  3. for (var score in scores) {
  4. if (score > highest) highest = score;
  5. }
  6. return highest;
  7. }

Here, if scores contains doubles, like [1.2], then the assignment tohighest will fail since its inferred type is int, not num. In these cases,explicit annotations make sense.

PREFER annotating with dynamic instead of letting inference fail.

Dart allows you to omit type annotations in many places and will try to infer atype for you. In some cases, if inference fails, it silently gives youdynamic. If dynamic is the type you want, this is technically the most terseway to get it.

However, it’s not the most clear way. A casual reader of your code who sees anannotation is missing has no way of knowing if you intended it to be dynamic,expected inference to fill in some other type, or simply forgot to write theannotation.

When dynamic is the type you want, writing it explicitly makes your intentclear.

  1. dynamic mergeJson(dynamic original, dynamic changes) => ...
  1. mergeJson(original, changes) => ...

Before Dart 2, this guideline stated the exact opposite: don’t annotate withdynamic when it is implicit. With the new stronger type system and typeinference, users now expect Dart to behave like an inferred statically-typedlanguage. With that mental model, it is an unpleasant surprise to discover thata region of code has silently lost all of the safety and performance of statictypes.

PREFER signatures in function type annotations.

The identifier Function by itself without any return type or parametersignature refers to the special Function type. This type is onlymarginally more useful than using dynamic. If you’re going to annotate, prefera full function type that includes the parameters and return type of thefunction.

  1. bool isValid(String value, bool Function(String) test) => ...
  1. bool isValid(String value, Function test) => ...

Exception: Sometimes, you want a type that represents the union of multipledifferent function types. For example, you may accept a function that takes oneparameter or a function that takes two. Since we don’t have union types, there’sno way to precisely type that and you’d normally have to use dynamic.Function is at least a little more helpful than that:

  1. void handleError(void Function() operation, Function errorHandler) {
  2. try {
  3. operation();
  4. } catch (err, stack) {
  5. if (errorHandler is Function(Object)) {
  6. errorHandler(err);
  7. } else if (errorHandler is Function(Object, StackTrace)) {
  8. errorHandler(err, stack);
  9. } else {
  10. throw ArgumentError("errorHandler has wrong signature.");
  11. }
  12. }
  13. }

DON’T specify a return type for a setter.

Linter rule: avoid_return_types_on_setters

Setters always return void in Dart. Writing the word is pointless.

  1. void set foo(Foo value) { ... }
  1. set foo(Foo value) { ... }

DON’T use the legacy typedef syntax.

Linter rule: prefer_generic_function_type_aliases

Dart has two notations for defining a named typedef for a function type. Theoriginal syntax looks like:

  1. typedef int Comparison<T>(T a, T b);

That syntax has a couple of problems:

  • There is no way to assign a name to a generic function type. In the aboveexample, the typedef itself is generic. If you reference Comparison inyour code, without a type argument, you implicitly get the function typeint Function(dynamic, dynamic), not int Function<T>(T, T). Thisdoesn’t come up in practice often, but it matters in certain corner cases.

  • A single identifier in a parameter is interpreted as the parameter’s name,not its type. Given:

  1. typedef bool TestNumber(num);

Most users expect this to be a function type that takes a num and returnsbool. It is actually a function type that takes any object (dynamic)and returns bool. The parameter’s name (which isn’t used for anythingexcept documentation in the typedef) is “num”. This has been along-standing source of errors in Dart.

The new syntax looks like this:

  1. typedef Comparison<T> = int Function(T, T);

If you want to include a parameter’s name, you can do that too:

  1. typedef Comparison<T> = int Function(T a, T b);

The new syntax can express anything the old syntax could express and more, andlacks the error-prone misfeature where a single identifier is treated as theparameter’s name instead of its type. The same function type syntax after the= in the typedef is also allowed anywhere a type annotation may appear, givingus a single consistent way to write function types anywhere in a program.

The old typedef syntax is still supported to avoid breaking existing code, butit’s deprecated.

PREFER inline function types over typedefs.

Linter rule: avoid_private_typedef_functions

In Dart 1, if you wanted to use a function type for a field, variable, orgeneric type argument, you had to first define a typedef for it. Dart 2 supportsa function type syntax that can be used anywhere a type annotation is allowed:

  1. class FilteredObservable {
  2. final bool Function(Event) _predicate;
  3. final List<void Function(Event)> _observers;
  4.  
  5. FilteredObservable(this._predicate, this._observers);
  6.  
  7. void Function(Event) notify(Event event) {
  8. if (!_predicate(event)) return null;
  9.  
  10. void Function(Event) last;
  11. for (var observer in _observers) {
  12. observer(event);
  13. last = observer;
  14. }
  15.  
  16. return last;
  17. }
  18. }

It may still be worth defining a typedef if the function type is particularlylong or frequently used. But in most cases, users want to see what the functiontype actually is right where it’s used, and the function type syntax gives themthat clarity.

CONSIDER using function type syntax for parameters.

Linter rule: use_function_type_syntax_for_parameters

Dart has a special syntax when defining a parameter whose type is a function.Sort of like in C, you surround the parameter’s name with the function’s returntype and parameter signature:

  1. Iterable<T> where(bool predicate(T element)) => ...

Before Dart 2 added function type syntax, this was the only way to give aparameter a function type without defining a typedef. Now that Dart has ageneral notation for function types, you can use it for function-typedparameters as well:

  1. Iterable<T> where(bool Function(T) predicate) => ...

The new syntax is a little more verbose, but is consistent with other locationswhere you must use the new syntax.

DO annotate with Object instead of dynamic to indicate any object is allowed.

Some operations work with any possible object. For example, a log() methodcould take any object and call toString() on it. Two types in Dart permit allvalues: Object and dynamic. However, they convey different things. If yousimply want to state that you allow all objects, use Object, as you would inJava or C#.

Using dynamic sends a more complex signal. It may mean that Dart’s type systemisn’t sophisticated enough to represent the set of types that are allowed, orthat the values are coming from interop or otherwise outside of the purview ofthe static type system, or that you explicitly want runtime dynamism at thatpoint in the program.

  1. void log(Object object) {
  2. print(object.toString());
  3. }
  4.  
  5. /// Returns a Boolean representation for [arg], which must
  6. /// be a String or bool.
  7. bool convertToBool(dynamic arg) {
  8. if (arg is bool) return arg;
  9. if (arg is String) return arg == 'true';
  10. throw ArgumentError('Cannot convert $arg to a bool.');
  11. }

DO use Future<void> as the return type of asynchronous members that do not produce values.

When you have a synchronous function that doesn’t return a value, you use voidas the return type. The asynchronous equivalent for a method that doesn’tproduce a value, but that the caller might need to await, is Future<void>.

You may see code that uses Future or Future<Null> instead because olderversions of Dart didn’t allow void as a type argument. Now that it does, youshould use it. Doing so more directly matches how you’d type a similarsynchronous function, and gives you better error-checking for callers and in thebody of the function.

For asynchronous functions that do not return a useful value and where nocallers need to await the asynchronous work or handle an asynchronous failure,use a return type of void.

AVOID using FutureOr<T> as a return type.

If a method accepts a FutureOr<int>, it is generous in what itaccepts. Users can call the method with either an int or aFuture<int>, so they don’t need to wrap an int in Future that you aregoing to unwrap anyway.

If you return a FutureOr<int>, users need to check whether get back an intor a Future<int> before they can do anything useful. (Or they’ll just awaitthe value, effectively always treating it as a Future.) Just return aFuture<int>, it’s cleaner. It’s easier for users to understand that a functionis either always asynchronous or always synchronous, but a function that can beeither is hard to use correctly.

  1. Future<int> triple(FutureOr<int> value) async => (await value) * 3;
  1. FutureOr<int> triple(FutureOr<int> value) {
  2. if (value is int) return value * 3;
  3. return (value as Future<int>).then((v) => v * 3);
  4. }

The more precise formulation of this guideline is to only use FutureOr<T> in[contravariant](https://en.wikipedia.org/wiki/Covariance_and_contravariance(computerscience)) positions. Parameters are contravariant and return types arecovariant. In nested function types, this gets flipped—if you have aparameter whose type is itself a function, then the callback’s return type isnow in contravariant position and the callback’s parameters are covariant. Thismeans it’s OK for a callback’s type to return FutureOr<T>:

  1. Stream<S> asyncMap<T, S>(
  2. Iterable<T> iterable, FutureOr<S> Function(T) callback) async* {
  3. for (var element in iterable) {
  4. yield await callback(element);
  5. }
  6. }

Parameters

In Dart, optional parameters can be either positional or named, but not both.

AVOID positional boolean parameters.

Linter rule: avoid_positional_boolean_parameters

Unlike other types, booleans are usually used in literal form. Things likenumbers are usually wrapped in named constants, but we usually just pass aroundtrue and false directly. That can make callsites unreadable if it isn’tclear what the boolean represents:

  1. new Task(true);
  2. new Task(false);
  3. new ListBox(false, true, true);
  4. new Button(false);

Instead, consider using named arguments, named constructors, or named constantsto clarify what the call is doing.

  1. Task.oneShot();
  2. Task.repeating();
  3. ListBox(scroll: true, showScrollbars: true);
  4. Button(ButtonState.enabled);

Note that this doesn’t apply to setters, where the name makes it clear what thevalue represents:

  1. listBox.canScroll = true;
  2. button.isEnabled = false;

AVOID optional positional parameters if the user may want to omit earlier parameters.

Optional positional parameters should have a logical progression such thatearlier parameters are passed more often than later ones. Users should almostnever need to explicitly pass a “hole” to omit an earlier positional argument topass later one. You’re better off using named arguments for that.

  1. String.fromCharCodes(Iterable<int> charCodes, [int start = 0, int end]);
  2.  
  3. DateTime(int year,
  4. [int month = 1,
  5. int day = 1,
  6. int hour = 0,
  7. int minute = 0,
  8. int second = 0,
  9. int millisecond = 0,
  10. int microsecond = 0]);
  11.  
  12. Duration(
  13. {int days = 0,
  14. int hours = 0,
  15. int minutes = 0,
  16. int seconds = 0,
  17. int milliseconds = 0,
  18. int microseconds = 0});

AVOID mandatory parameters that accept a special “no argument” value.

If the user is logically omitting a parameter, prefer letting them actually omitit by making the parameter optional instead of forcing them to pass null, anempty string, or some other special value that means “did not pass”.

Omitting the parameter is more terse and helps prevent bugs where a sentinelvalue like null is accidentally passed when the user thought they wereproviding a real value.

  1. var rest = string.substring(start);
  1. var rest = string.substring(start, null);

DO use inclusive start and exclusive end parameters to accept a range.

If you are defining a method or function that lets a user select a range ofelements or items from some integer-indexed sequence, take a start index, whichrefers to the first item and a (likely optional) end index which is one greaterthan the index of the last item.

This is consistent with core libraries that do the same thing.

  1. [0, 1, 2, 3].sublist(1, 3) // [1, 2]
  2. 'abcd'.substring(1, 3) // 'bc'

It’s particularly important to be consistent here because these parameters areusually unnamed. If your API takes a length instead of an end point, thedifference won’t be visible at all at the callsite.

Equality

Implementing custom equality behavior for a class can be tricky. Users have deepintuition about how equality works that your objects need to match, andcollection types like hash tables have subtle contracts that they expectelements to follow.

DO override hashCode if you override ==.

Linter rule: hash_and_equals

The default hash code implementation provides an identity hash—twoobjects generally only have the same hash code if they are the exact sameobject. Likewise, the default behavior for == is identity.

If you are overriding ==, it implies you may have different objects that areconsidered “equal” by your class. Any two objects that are equal must have thesame hash code. Otherwise, maps and other hash-based collections will fail torecognize that the two objects are equivalent.

DO make your == operator obey the mathematical rules of equality.

An equivalence relation should be:

  • Reflexive: a == a should always return true.

  • Symmetric: a == b should return the same thing as b == a.

  • Transitive: If a == b and b == c both return true, then a == cshould too.

Users and code that uses == expect all of these laws to be followed. If yourclass can’t obey these rules, then == isn’t the right name for the operationyou’re trying to express.

AVOID defining custom equality for mutable classes.

When you define ==, you also have to define hashCode. Both of those shouldtake into account the object’s fields. If those fields change then thatimplies the object’s hash code can change.

Most hash-based collections don’t anticipate that—they assume an object’shash code will be the same forever and may behave unpredictably if that isn’ttrue.

DON’T check for null in custom == operators.

Linter rule: avoid_null_checks_in_equality_operators

The language specifies that this check is done automatically and your ==method is called only if the right-hand side is not null.

  1. class Person {
  2. final String name;
  3. // ···
  4. bool operator ==(other) => other is Person && name == other.name;
  5.  
  6. int get hashCode => name.hashCode;
  7. }
  1. class Person {
  2. final String name;
  3. // ···
  4. bool operator ==(other) => other != null && ...
  5. }