skip to content

In Dart patterns, when would you use `var t?`, `var t!` or `t as double` on a reading whose temperature may be null, and what happens on a bad value?

level: seniorimportance: should knowfreq 28%

answer

  1. ? fails the match on null
  2. ! throws on null
  3. as throws on the wrong type
  4. all bind a narrowed type
  5. refutable versus throwing

basics

~20 s

Use the null-check pattern var t? when null is expected: null fails the match. Use null-assert var t! when null is a bug: it throws. A cast pattern t as double insists on a type and throws on a wrong one.

solid answer

~50 s

All three narrow a value while destructuring, but they differ on failure. The **null-check pattern** `var t?` matches only a non-null value and binds `t` with the non-nullable type, `double` for a `double?` field; on `null` the match **fails**, so it belongs in `if-case` or switch cases. The **null-assert pattern** `var t!` also binds a non-nullable `t` but **throws** on `null`; it is the tool for declarations, as in `var (city: c, temp: t!) = reading;`, where null means a bug. The **cast pattern** `t as double` converts the static type in the middle of a pattern and **throws** if the runtime value is not a `double`, as in `var (i as int, s as String) = record;`. Pick by the question: is a bad value an expected branch (`?`, or a typed variable pattern), or a broken invariant (`!`, `as`)?

code

dart · 21 lines
dart
typedef Reading = ({String city, double? temp});

String label(Reading r) {
  if (r case (city: var city, temp: var t?)) {
    return '$city ${t.toStringAsFixed(1)}'; // t is double here
  }
  return '${r.city} no data';
}

void main() {
  print(label((city: 'Oslo', temp: 3.46))); // Oslo 3.5
  print(label((city: 'Rome', temp: null))); // Rome no data

  final Reading cached = (city: 'Oslo', temp: 3.5);
  final (city: c, temp: t!) = cached; // would throw if temp were null
  print('$c $t'); // Oslo 3.5

  (num, Object) raw = (3, 'Oslo');
  var (n as int, name as String) = raw;
  print(n + name.length); // 7
}

go deeper

for a junior

Remember the pairing: ? quietly fails on null, ! throws on null, and as throws on a wrong type.

for a middle

Explain that all three bind a narrowed type, and why the null-check form suits matching contexts while null-assert and cast suit declarations.

for a senior

Decide per boundary whether a bad value is an expected branch or a broken invariant, and keep throwing patterns away from unvalidated external data.

for a principal

Define how the codebase signals broken invariants versus expected absence, so crashes from ! and as point at real bugs rather than at data the app should tolerate.

## Three narrowing patterns Suppose a weather service gives you readings as records with a nullable temperature: ```dart typedef Reading = ({String city, double? temp}); ``` Dart 3 has three postfix patterns that narrow a value while destructuring. They share one precedence level and differ in what happens when the value is wrong. | Pattern | Matches | On a bad value | Typical context | |---|---|---|---| | **null-check** `var t?` | non-null values | the match **fails** | `if-case`, switch cases | | **null-assert** `var t!` | non-null values | **throws** | declarations, or cases where null is a bug | | **cast** `t as double` | any value, then asserts the type | **throws** if the value is not a `double` | declarations over broader static types | In each case the bound variable gets the narrowed static type: `double` rather than `double?`, or `double` rather than `num`/`Object`. ## Null-check: null is an expected case ```dart String label(Reading r) { if (r case (city: var city, temp: var t?)) { return '$city ${t.toStringAsFixed(1)}'; // t is double } return '${r.city} no data'; } ``` `var t?` first checks that the value is not `null`, then matches the inner pattern `var t` against the same value, so `t` is a non-nullable `double`. When the sensor did not report, the pattern fails and the second `return` runs — no exception, no `!` in sight. To match the null case explicitly, use the **constant pattern** `null`. ## Null-assert: null is a bug ```dart final (city: city, temp: t!) = readingFromCache(); // throws if temp is null ``` Declarations cannot fail quietly, so the null-check pattern has no place there. The **null-assert** pattern lets non-null values through and throws on `null`. It is also useful inside a case when you want a `null` to be loud rather than silently skipped, for example `case ['user', var name!]` on a row that must always have a name. ## Cast: insisting on a type mid-pattern A **cast pattern** inserts a type cast between the outer pattern and a subpattern: ```dart (num, Object) raw = (3, 'Oslo'); var (t as int, city as String) = raw; // t: int, city: String ``` If the runtime value does not have the stated type, the cast throws. That makes it the declaration-friendly counterpart of a **typed variable pattern**: in an `if-case`, `(int t, String city)` would simply fail on a mismatch; in a declaration, you need `as` to state the type because a mismatch has to throw. ## Choosing between them - **Untrusted or optional data** (network, user input, nullable fields that are legitimately empty) → refutable forms: `var t?`, typed variables like `double t`, or the constant pattern `null` for the empty case. - **Invariants** (a cache that always has a temperature, a record built by your own code) → `!` or `as`, so a violation fails fast at the point it happens. - **Do not** use `!` or `as` to silence the analyzer on data you have not validated; it converts a missing field into a crash in production. ## A checklist for a nullable field 1. **Is null a legitimate state** (sensor offline, optional section)? Match it: `var t?` for the value, the constant pattern `null` for the gap. 2. **Is null a bug** in data your own code produced? Assert it in a declaration with `t!` so the failure points at the broken producer. 3. **Is the static type broader** than the value you expect, such as `num` or `Object`? In a case, use a typed variable like `double t`; in a declaration, use `t as double`. 4. **Is the data external?** Prefer the refutable forms, and handle the else branch explicitly. ## Precedence and spelling - The three postfix patterns share one precedence level: **above** relational, `&&` and `||` patterns, **below** primary patterns such as records, lists, maps and objects. When you combine them with logical patterns, parenthesise for the reader even where the grammar does not require it. - The `!` operator and the `as` operator as **expressions** are separate features of the language; the patterns reuse their spelling, but only the pattern forms take part in matching and binding. - A **guard** is not a pattern: `case (temp: var t?) when t > 40` first matches, then evaluates the condition with `t` already non-nullable.

  • In a switch over a `double?` reading, how do you handle the null case explicitly alongside `var t?`?
    Add a case with the constant pattern `null`, for example `case null:` for the missing reading and `case var t?:` for the rest. The null-check pattern alone would leave null unmatched; the constant pattern `null` is the documented way to match the null value itself.
  • What is the difference between `(int t, String c)` and `(t as int, c as String)` as patterns?
    The typed variable pattern tests the type and fails the match if it is wrong, so it is for if-case and switch cases. The cast pattern asserts the type and throws if it is wrong, so it also works in declarations, where a failing match is not an option.

A null-check pattern is a turnstile that quietly refuses anyone without a ticket and lets the queue move on; a null-assert pattern is a door alarm that goes off the moment someone without a ticket walks through.

saying these in an interview costs you the question

  • Uses var t! in an if-case to skip readings with no temperature
  • Thinks the null-check pattern throws on null
  • Believes a cast pattern converts values, such as int to double
  • Writes final (x?, y?) = position; to drop nulls in a declaration
  • Uses as in patterns to silence the analyzer on unvalidated JSON