Skip to content

Tighten guidelines around dynamic #6286

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 7 commits into
base: main
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all 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 @@ -140,9 +140,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
33 changes: 17 additions & 16 deletions examples/misc/lib/effective_dart/design_good.dart
Original file line number Diff line number Diff line change
Expand Up @@ -278,22 +278,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 @@ -329,6 +316,20 @@ 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 @@ -276,7 +276,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 invoke 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
101 changes: 59 additions & 42 deletions src/content/effective-dart/design.md
Original file line number Diff line number Diff line change
Expand Up @@ -1425,52 +1425,48 @@ 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>

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.
### DO annotate with `Object?` instead of letting inference fail

When `dynamic` is the type you want, write that explicitly to make your intent
clear and highlight that this code has less static safety.
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 might fail a type cast that
is not visible in the syntax, while an `Object?` reference will
guarantee that the `as` cast is explicitly written.

<?code-excerpt "design_good.dart (prefer-dynamic)"?>
Use `Object?` to indicate in a signature that
any type of object, or null, is allowed.

<?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 tersest 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,
users expect Dart to behave like an inferred statically-typed language.
With that mental model,
With Dart's strong type system and type inference,
users expect Dart to behave like an inferred statically-typed language.
With that mental model,
it is an unpleasant surprise to discover that
a region of code has silently lost all of the
safety and performance of static types.
Expand Down Expand Up @@ -1667,16 +1663,18 @@ 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 @@ -1697,11 +1695,30 @@ 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 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
static type checking might not catch mistakes like misspellings.

<?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 @@ -567,7 +567,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 @@ -1154,7 +1154,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