Skip to content
Draft
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions examples/misc/lib/effective_dart/design_bad.dart
Original file line number Diff line number Diff line change
Expand Up @@ -133,9 +133,9 @@ void miscDeclAnalyzedButNotTested() {
}

{
// #docregion prefer-dynamic
// #docregion prefer-object-question
mergeJson(original, changes) => ellipsis();
// #enddocregion prefer-dynamic
// #enddocregion prefer-object-question
}

// #docregion avoid-function
Expand Down
31 changes: 16 additions & 15 deletions examples/misc/lib/effective_dart/design_good.dart
Original file line number Diff line number Diff line change
Expand Up @@ -271,21 +271,9 @@ void miscDeclAnalyzedButNotTested() {
}

{
// #docregion prefer-dynamic
dynamic mergeJson(dynamic original, dynamic changes) => ellipsis();
// #enddocregion prefer-dynamic
}

{
// #docregion infer-dynamic
Map<String, dynamic> readJson() => ellipsis();

void printUsers() {
var json = readJson();
var users = json['users'];
print(users);
}
// #enddocregion infer-dynamic
// #docregion prefer-object-question
Object? mergeJson(Object? original, Object? changes) => ellipsis();
// #enddocregion prefer-object-question
}

// #docregion avoid-function
Expand Down Expand Up @@ -320,6 +308,19 @@ void miscDeclAnalyzedButNotTested() {
// #enddocregion object-vs-dynamic
};

() {
// #docregion cast-for-dynamic-member
/// Returns whether the length of [value] is exactly [length].
///
/// The argument may be a [String], an [Iterable] or [Map], or any other
/// type that has a `length` field.
bool hasLength(Object? value, int length) {
var actualLength = (value as dynamic).length;
return length == actualLength;
}
// #enddocregion cast-for-dynamic-member
};

// #docregion future-or
Future<int> triple(FutureOr<int> value) async => (await value) * 3;
// #enddocregion future-or
Expand Down
2 changes: 1 addition & 1 deletion firebase.json
Original file line number Diff line number Diff line change
Expand Up @@ -274,7 +274,7 @@
{ "source": "/keyword/default", "destination": "/language/branches#switch", "type": 301 },
{ "source": "/keyword/deferred", "destination": "/language/libraries#lazily-loading-a-library", "type": 301 },
{ "source": "/keyword/do", "destination": "/language/loops#while-and-do-while", "type": 301 },
{ "source": "/keyword/dynamic", "destination": "/effective-dart/design#avoid-using-dynamic-unless-you-want-to-disable-static-checking", "type": 301 },
{ "source": "/keyword/dynamic", "destination": "/effective-dart/design#avoid-using-dynamic-unless-you-want-to-invoke-dynamic-members", "type": 301 },
{ "source": "/keyword/else", "destination": "/language/branches#if", "type": 301 },
{ "source": "/keyword/enum", "destination": "/language/enums", "type": 301 },
{ "source": "/keyword/export", "destination": "/tools/pub/create-packages", "type": 301 },
Expand Down
4 changes: 2 additions & 2 deletions src/content/effective-dart/_toc.md
Original file line number Diff line number Diff line change
Expand Up @@ -233,13 +233,13 @@ the project:
* <a href='/effective-dart/design#do-write-type-arguments-on-generic-invocations-that-arent-inferred'>DO write type arguments on generic invocations that aren't inferred.</a>
* <a href='/effective-dart/design#dont-write-type-arguments-on-generic-invocations-that-are-inferred'>DON'T write type arguments on generic invocations that are inferred.</a>
* <a href='/effective-dart/design#avoid-writing-incomplete-generic-types'>AVOID writing incomplete generic types.</a>
* <a href='/effective-dart/design#do-annotate-with-dynamic-instead-of-letting-inference-fail'>DO annotate with <code>dynamic</code> instead of letting inference fail.</a>
* <a href='/effective-dart/design#do-annotate-with-object-instead-of-letting-inference-fail'>DO annotate with <code>Object?</code> instead of letting inference fail.</a>
* <a href='/effective-dart/design#prefer-signatures-in-function-type-annotations'>PREFER signatures in function type annotations.</a>
* <a href='/effective-dart/design#dont-specify-a-return-type-for-a-setter'>DON'T specify a return type for a setter.</a>
* <a href='/effective-dart/design#dont-use-the-legacy-typedef-syntax'>DON'T use the legacy typedef syntax.</a>
* <a href='/effective-dart/design#prefer-inline-function-types-over-typedefs'>PREFER inline function types over typedefs.</a>
* <a href='/effective-dart/design#prefer-using-function-type-syntax-for-parameters'>PREFER using function type syntax for parameters.</a>
* <a href='/effective-dart/design#avoid-using-dynamic-unless-you-want-to-disable-static-checking'>AVOID using <code>dynamic</code> unless you want to disable static checking.</a>
* <a href='/effective-dart/design#avoid-using-dynamic-unless-you-want-to-invoke-dynamic-members'>AVOID using <code>dynamic</code> unless you want to invokde dynamic members.</a>
* <a href='/effective-dart/design#do-use-futurevoid-as-the-return-type-of-asynchronous-members-that-do-not-produce-values'>DO use <code>Future&lt;void&gt;</code> as the return type of asynchronous members that do not produce values.</a>
* <a href='/effective-dart/design#avoid-using-futureort-as-a-return-type'>AVOID using <code>FutureOr&lt;T&gt;</code> as a return type.</a>

Expand Down
87 changes: 48 additions & 39 deletions src/content/effective-dart/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1410,47 +1410,37 @@ var completer = Completer<Map<String, int>>();
```


### DO annotate with `dynamic` instead of letting inference fail
<a id="do-annotate-with-dynamic-instead-of-letting-inference-fail" aria-hidden="true"></a>
### DO annotate with `Object?` instead of letting inference fail

When inference doesn't fill in a type, it usually defaults to `dynamic`. If
`dynamic` is the type you want, this is technically the most terse way to get
it. However, it's not the most *clear* way. A casual reader of your code who
sees that an annotation 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 the annotation.
When inference doesn't fill in a type, it usually defaults to `dynamic`, which
is rarely the best type to use. A `dynamic` reference allows for unsafe
operations that use identical syntax to operations that are statically safe when
the type is not `dynamic`. An `Object?` reference is safer. For example a
`dynamic` reference may fail a type cast that is not visible in the syntax,
while an `Object?` reference will guarantee that the `as` cast is written in
source.

When `dynamic` is the type you want, write that explicitly to make your intent
clear and highlight that this code has less static safety.
Use `Object?` to indicate in a signature that any type of object, or null, is
allowed.
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The overall metarule for the guidelines is "unless stated otherwise, express things the most terse way". If you want some return type or parameter type to have type dynamic, the most terse way is to simply write nothing.

The original intent of this guideline was to opt out of that default "use the shortest thing" rule. That's why the guideline is about "instead of letting inference fail".

The new guideline here is really about "when should you use Object? versus dynamic, which is a different thing. I think the second updated guideline below does a pretty good job of covering that.

So for this one, I think it would be better to recast it to something like `DON'T let inference fall back to dynamic".


<?code-excerpt "design_good.dart (prefer-dynamic)"?>
<?code-excerpt "design_good.dart (prefer-object-question)"?>
```dart tag=good
dynamic mergeJson(dynamic original, dynamic changes) => ...
Object? mergeJson(Object? original, Object? changes) => ...
```

<?code-excerpt "design_bad.dart (prefer-dynamic)"?>
<?code-excerpt "design_bad.dart (prefer-object-question)"?>
```dart tag=bad
mergeJson(original, changes) => ...
```

Note that it's OK to omit the type when Dart *successfully* infers `dynamic`.

<?code-excerpt "design_good.dart (infer-dynamic)"?>
```dart tag=good
Map<String, dynamic> readJson() => ...

void printUsers() {
var json = readJson();
var users = json['users'];
print(users);
}
```

Here, Dart infers `Map<String, dynamic>` for `json` and then from that infers
`dynamic` for `users`. It's fine to leave `users` without a type annotation. The
distinction is a little subtle. It's OK to allow inference to *propagate*
`dynamic` through your code from a `dynamic` type annotation somewhere else, but
you don't want it to inject a `dynamic` type annotation in a place where your
code did not specify one.
In the cases where a dynamic member will be invoked this is technically the most
terse way to get a dynamic reference. However, it's not the most *clear* way. A
casual reader of your code who sees that an annotation 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 the annotation. When `dynamic` is the type
you want, write that explicitly to make your intent clear and highlight that
this code has less static safety.

:::note
With Dart's strong type system and type inference,
Expand Down Expand Up @@ -1648,16 +1638,17 @@ The new syntax is a little more verbose, but is consistent with other locations
where you must use the new syntax.


### AVOID using `dynamic` unless you want to disable static checking
<a id="avoid-using-dynamic-unless-you-want-to-disable-static-checking" aria-hidden="true"></a>
### AVOID using `dynamic` unless you want to invoke dynamic members

Some operations work with any possible object. For example, a `log()` method
could take any object and call `toString()` on it. Two types in Dart permit all
values: `Object?` and `dynamic`. However, they convey different things. If you
simply want to state that you allow all objects, use `Object?`. If you want to
allow all objects *except* `null`, then use `Object`.

The type `dynamic` not only accepts all objects, but it also permits all
*operations*. Any member access on a value of type `dynamic` is allowed at
The type `dynamic` not only accepts all objects, but it also statically permits
all *operations*. Any member access on a value of type `dynamic` is allowed at
compile time, but may fail and throw an exception at runtime. If you want
exactly that risky but flexible dynamic dispatch, then `dynamic` is the right
type to use.
Expand All @@ -1678,11 +1669,29 @@ bool convertToBool(Object arg) {
}
```

The main exception to this rule is when working with existing APIs that use
`dynamic`, especially inside a generic type. For example, JSON objects have type
`Map<String, dynamic>` and your code will need to accept that same type. Even
so, when using a value from one of these APIs, it's often a good idea to cast it
to a more precise type before accessing members.
Prefer using `Object?` over `dynamic` in code that is not invoking a member
dynamically, even when working with existing APIs that use `dynamic`. For
example, the static types `Map<String, dynamic>` and `Map<String, Object?>` can
both be used as the static type for the same value, and the `Object?` form is
preferred.

For intentional dynamic member access, consider using a cast to `dynamic` for
the member access specifically. Separating the use of `Object?` for non-dynamic
behavior and limiting `dynamic` to the places where dynamic operations are
intended makes them syntactically distinct and highlights the places where
mistakes like misspellings may not be caught by static type checking.

<?code-excerpt "design_good.dart (cast-for-dynamic-member)"?>
```dart tag=good
/// Returns whether the length of [value] is exactly [length].
///
/// The argument may be a [String], an [Iterable] or [Map], or any other
/// type that has a `length` field.
bool hasLength(Object? value, int length) {
var actualLength = (value as dynamic).length;
return length == actualLength;
}
```


### DO use `Future<void>` as the return type of asynchronous members that do not produce values
Expand Down
2 changes: 1 addition & 1 deletion src/content/language/index.md
Original file line number Diff line number Diff line change
Expand Up @@ -563,7 +563,7 @@ This site's code follows the conventions in the
[ns]: /null-safety
[`Object`]: {{site.dart-api}}/dart-core/Object-class.html
[language version]: /resources/language/evolution#language-versioning
[ObjectVsDynamic]: /effective-dart/design#avoid-using-dynamic-unless-you-want-to-disable-static-checking
[ObjectVsDynamic]: /effective-dart/design#avoid-using-dynamic-unless-you-want-to-invoke-dynamic-members
[Libraries and imports]: /language/libraries
[conditional expression]: /language/operators#conditional-expressions
[if-else statement]: /language/branches#if
Expand Down
2 changes: 1 addition & 1 deletion src/content/resources/whats-new.md
Original file line number Diff line number Diff line change
Expand Up @@ -1045,7 +1045,7 @@ we made the following changes to this site:

[dart-tool]: /tools/dart-tool
[diagnostics]: /tools/diagnostic-messages
[dynamic]: /effective-dart/design#avoid-using-dynamic-unless-you-want-to-disable-static-checking
[dynamic]: /effective-dart/design#avoid-using-dynamic-unless-you-want-to-invoke-dynamic-members
[Effective Dart]: /effective-dart
[evolution]: /resources/language/evolution
[experiments]: /tools/experiment-flags#using-experiment-flags-with-ides
Expand Down
Loading