In Dart, how does a variable typed dynamic differ from one typed Object?, and how do is, is! and as fit in?
answer
- both accept every value
- one keeps static checking
- is promotes, as casts
- failed as throws TypeError
- Object excludes null
basics
~20 sIn Dart, Object? and dynamic both accept any value, including null, but Object? keeps static checking: you test with is or cast with as before using String members. dynamic switches checking off, so calls compile and fail only at runtime.
solid answer
~50 s`Object` is the superclass of every class except `Null`, so `Object` means any non-null value and `Object?` means any value at all. On such a variable you can only call `Object` members like `toString()`; to use a `String` method you check `if (value is String)`, which promotes a local to `String`, or cast with `value as String`, which throws a `TypeError` if wrong. `is!` is the negated test. `dynamic` also accepts every value, but it **disables static checking**: any member access compiles and is checked at runtime, where a missing member throws `NoSuchMethodError`, and a `dynamic` value can be assigned to a `String` variable without a cast. Effective Dart says to use `Object?` or `Object` unless you really want dynamic dispatch; APIs such as decoded JSON (`Map<String, dynamic>`) are the usual exception, and even there you cast to precise types early.
code
dart · 26 linesvoid describe(Object? value) {
if (value is String) {
print('name with ${value.length} code units'); // promoted to String
} else if (value is! num) {
print('unsupported: ${value.runtimeType}');
} else {
print('price ${value * 2}'); // promoted to num
}
}
void main() {
describe('Margherita');
describe(12);
dynamic raw = 42;
// raw.toUpperCase(); // compiles, but throws NoSuchMethodError
try {
final String name = raw; // implicit downcast compiles
print(name);
} on TypeError {
print('raw was not a String');
}
Object boxed = 42;
// final String bad = boxed; // compile error: needs a cast or a check
print((boxed as int) + 1); // explicit cast
}go deeper
Recall that Object? accepts anything but keeps checking, while dynamic accepts anything and turns checking off.
Explain implicit downcasts from dynamic, promotion after is and is!, and that a failed as throws TypeError.
Contain dynamic at API boundaries such as JSON, cast early, and enable strict-casts and avoid_dynamic_calls in the project.
Set a codebase policy on dynamic and analyzer strictness that balances migration effort against runtime type failures.
## Three ways to say "anything" Dart's type hierarchy has `Object` at the top of all classes, with one exception: `Null`, the class of `null`, is not a subtype of `Object`. Since sound null safety that gives three types that accept broad sets of values: | Type | Accepts | Static checking | Member access | |---|---|---|---| | `Object` | Every value except `null` | On | Only `Object` members (`toString`, `hashCode`, `==`, `runtimeType`) | | `Object?` | Every value, including `null` | On | Same, plus null checks | | `dynamic` | Every value, including `null` | **Off** | Anything compiles; checked at runtime | The null-safety guide calls `Object?` Dart's top type: if you need "any value", that is the type to write. ## What dynamic really changes `dynamic` is not a bigger `Object`; it is a request to **turn off static type checking** for that expression. dart.dev's built-in types page describes it exactly that way. Two consequences: 1. **Any member access compiles.** `value.toUpperCase()` compiles on a `dynamic` value; if the object has no such member at runtime, Dart throws `NoSuchMethodError`. 2. **Implicit downcasts are allowed.** `String name = value;` compiles when `value` is `dynamic`, and throws a `TypeError` at runtime if it holds, say, an `int`. The same assignment from an `Object` value is a compile-time error (`invalid_assignment`). ## is, is! and as With `Object?` you move from "anything" to a specific type explicitly: - **`is`** tests the runtime type and returns a `bool`. On a local variable, a successful test **promotes** it, so inside `if (value is String) { ... }` you can call `value.length` without a cast. - **`is!`** is the negation: `if (value is! String) return;` also promotes on the path that continues. - **`as`** casts: `value as String` returns the value typed as `String`, or throws a **`TypeError`** if it is not one. Dart 3 removed the old `CastError` class; `TypeError` is what a failed cast throws. Prefer `is` when the value might legitimately be another type, and `as` when any other type is a bug that should fail loudly. ## Menu importer example A menu importer reading a decoded JSON payload receives values typed `dynamic`. Writing `final String name = item['name'];` compiles, and a malformed payload with a number there crashes later with a `TypeError`. Treating the value as `Object?` and testing it keeps the failure where you can handle it: ```dart final Object? raw = item['name']; if (raw is! String) throw FormatException('name must be a string'); final name = raw; // promoted to String ``` ## Tooling that helps - The analyzer's `strict-casts` language mode reports implicit downcasts from `dynamic`, such as passing `jsonDecode(text)` straight to a `List<String>` parameter. - The `avoid_dynamic_calls` lint flags member access on `dynamic` targets. - Effective Dart's rule **"AVOID using `dynamic` unless you want to disable static checking"** states the policy; its companion rule says to annotate with `dynamic` explicitly rather than let inference fail silently. ## dynamic hiding inside generic types `dynamic` often arrives without anyone writing it: - A `Map<String, dynamic>` from decoded JSON makes every `map['key']` a `dynamic` expression. - A `List<dynamic>` makes every element access dynamic, so `items[0].price` compiles whatever the element is. - A declaration whose type inference fails, such as a variable with no initializer and no annotation, is silently `dynamic`. Each of these spreads unchecked code outward: a `dynamic` value assigned to an inferred local makes that local `dynamic` too. The defence is to convert at the boundary, reading `Object?` and checking with `is`, or casting once with `as` into a precise type, so the rest of the program is checked again. ## A short answer to give "`Object?` and `dynamic` both hold any value. `Object?` keeps the analyzer on, so I must prove the type with `is` or assert it with `as`. `dynamic` turns the analyzer off for that value, so mistakes compile and fail at runtime with `NoSuchMethodError` or `TypeError`. I use `Object?` by default and keep `dynamic` at API edges like JSON." ## Summary - Use `Object?` for "any value", `Object` for "any non-null value". - Use `dynamic` only when you deliberately want runtime dispatch or an API hands it to you. - Move from broad to specific types with `is` (safe test and promotion) or `as` (cast that throws `TypeError` on failure).
- When is dynamic the right type in Dart?When you really want runtime dispatch, or when an existing API uses it, such as decoded JSON as `Map<String, dynamic>`. Effective Dart still advises casting those values to precise types early, and annotating `dynamic` explicitly rather than letting inference fall back to it silently.
- How does the analyzer's strict-casts mode relate to dynamic?With `strict-casts: true` under `analyzer: language:` in analysis_options.yaml, the analyzer no longer allows implicit downcasts from `dynamic`, so passing a `dynamic` value where a `List<String>` is expected becomes an error until you add an explicit cast or check. It defaults to false.
saying these in an interview costs you the question
- dynamic and Object are the same type with different names
- A failed as cast returns null instead of throwing
- Calling a missing method on a dynamic value is a compile error
- Object accepts null, so Object? is redundant
- Assigning an Object value to a String variable compiles without a cast