Dart 3 🎯 Variables, Type Inference (var, final, const), and Compile-time Constants
Dart is a strongly typed language, but you rarely write type annotations explicitly. The compiler infers types from the values you assign. This chapter covers the three ways to declare variables — var, final, and const — and the crucial distinction between values that are computed at runtime and values that are computed at compile time. Understanding this distinction is the foundation for writing Dart code that is both safe and efficient.
Key point: var declares a variable whose type is inferred from its initializer. final declares a variable that can be assigned only once. const declares a value that is a compile-time constant — its entire state is known and frozen at compile time. The difference between final and const is not about mutability alone; it is about when the value is determined. final is set once at runtime. const is set at compile time and is deeply, transitively immutable.
Why the Variable Keywords Matter
Dart’s variable system is built around two principles: sound null safety and type inference. Every variable has a type, but you often let the compiler figure it out. The keyword you choose determines how the variable behaves.
The type inference problem. Dart is strongly typed, but writing String name = 'Bob' everywhere is verbose. The var keyword lets the compiler infer the type from the initializer. var name = 'Bob' produces the same result — name is a String. The compiler reads the value and determines the type .
The immutability problem. Some values change; others do not. A counter increments. A configuration value is set once and never modified. The final keyword marks a variable as single-assignment: it must be initialized, and once set, it cannot be reassigned .
The compile-time problem. Some values are known before the program runs. const declares values that the compiler can compute and freeze. A const list cannot be modified. A const object is deeply immutable and canonicalized — two identical const expressions produce the same object instance .
The trade-off. var is flexible and concise. final is safer for values that should not change. const is the most restrictive but enables optimizations the compiler cannot apply otherwise. The choice depends on the value and when it is known.
a. Variables and Type Inference with var
The var keyword declares a variable without specifying its type. The compiler infers the type from the initializer.
var name = 'Bob'; // inferred as String
var count = 42; // inferred as int
var ratio = 3.14; // inferred as double
var isActive = true; // inferred as bool
var items = [1, 2, 3]; // inferred as List<int>
The variable is still statically typed. You cannot assign a value of a different type later. name = 42 produces a compile-time error because name is a String .
You can also specify the type explicitly if you prefer. The two forms are equivalent:
var name = 'Bob';
String name = 'Bob';
The explicit form is useful when the variable is declared without an initializer. The var keyword requires an initializer because there is nothing to infer from otherwise .
var name; // ❌ Error: no initializer, type cannot be inferred
String name; // ✅ Valid: type is explicit
name = 'Bob';
For variables that can hold any type, use Object or dynamic. The Object type is the superclass of all types except Null. The dynamic type defers all type checking to runtime .
Object name = 'Bob'; // can be reassigned to any non-null value
dynamic name = 'Bob'; // no static type checking
The dynamic type should be avoided when possible. It disables the compiler’s type checking and moves errors from compile time to runtime.
Null safety. Dart enforces sound null safety. A variable of type String cannot contain null. To allow null, add a question mark to the type .
String name = 'Bob'; // non-nullable
String? middleName; // nullable, defaults to null
A nullable variable must be checked before use. The compiler prevents you from calling methods on a potentially null value unless you prove it is not null. The ! operator asserts that a nullable value is not null, but it throws at runtime if the assertion is wrong .
b. final and const: Runtime vs Compile-Time Immutability
The final and const keywords both create variables that cannot be reassigned. The difference is when the value is determined.
final means single-assignment at runtime. The variable must be initialized, and once set, it cannot be changed. The initializer can be a runtime expression.
final appName = 'CodeArchaeology';
final now = DateTime.now(); // ✅ runtime value — final works
final items = [1, 2, 3]; // ✅ the list is final, but the contents can change
items.add(4); // ✅ valid — the list reference is final, not the list
A final list can be modified. The variable holds a reference to the list. The reference cannot change, but the list itself can .
const means compile-time constant. The value must be computable at compile time. The value is deeply, transitively immutable. Everything inside it must also be const .
const pi = 3.14159; // ✅ literal — compile-time constant
const daysInWeek = 7; // ✅ literal — compile-time constant
const maxRetries = 3 * 2; // ✅ arithmetic on constants — compile-time constant
const appName = 'CodeArchaeology'; // ✅ string literal — compile-time constant
// ❌ Error: DateTime.now() is not a compile-time constant
const now = DateTime.now();
A const list cannot be modified. Attempting to add an element produces a compile-time error if the list is a const literal .
const items = [1, 2, 3];
items.add(4); // ❌ Error: cannot modify an unmodifiable list
The canonicalization rule. Two identical const expressions produce the same object instance. This is called canonicalization. It is like string interning for all const values .
const a = const ImmutablePoint(1, 1);
const b = const ImmutablePoint(1, 1);
assert(identical(a, b)); // ✅ true — same instance
The compiler creates a single instance and reuses it. This saves memory and makes equality checks faster for const values.
Where const can be used. A const variable must be initialized with a constant expression. The expression can include literals, arithmetic on literals, other const variables, and const constructor invocations .
const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond; // ✅ 5000
The const constructor. Classes can define const constructors. When invoked with const, the result is a compile-time constant .
class ImmutablePoint {
final double x;
final double y;
const ImmutablePoint(this.x, this.y);
}
const origin = const ImmutablePoint(0, 0);
A class with a const constructor must have all final instance variables. The constructor cannot have a body — only an initializer list .
Const in a const context. When an expression is already in a constant context, the const keyword can be omitted. The analyzer reports unnecessary_const when it is redundant .
const list = <int>[]; // ✅ const is implied
const list = const <int>[]; // ⚠️ unnecessary_const — analyzer warning
A constant context is introduced by a const variable declaration, a const list or map literal, a const constructor invocation, and a few other places .
The difference in one table.
| Keyword | When Set | Reassignable | Deeply Immutable | Compile-Time Known |
|---|---|---|---|---|
var | Runtime | Yes | No | No |
final | Runtime | No | No | No |
const | Compile time | No | Yes | Yes |
Complete Example Session
This session demonstrates type inference, final, and const in a single Dart file.
// ============================================
// PART 1: TYPE INFERENCE WITH VAR
// ============================================
void main() {
var count = 42;
var message = 'Hello, Dart!';
var ratio = 3.14;
var isActive = true;
var items = [1, 2, 3];
print('count is ${count.runtimeType}: $count');
print('message is ${message.runtimeType}: $message');
print('ratio is ${ratio.runtimeType}: $ratio');
print('isActive is ${isActive.runtimeType}: $isActive');
print('items is ${items.runtimeType} with ${items.length} elements');
// count = 'not a number'; // ❌ Error: String is not assignable to int
}
// ============================================
// PART 2: FINAL — RUNTIME IMMUTABILITY
// ============================================
final appName = 'CodeArchaeology';
final now = DateTime.now(); // runtime value — only final works
final items = [1, 2, 3];
// items = [4, 5, 6]; // ❌ Error: cannot reassign a final variable
items.add(4); // ✅ valid — modifying the list, not the reference
print('final appName: $appName');
print('final now: ${now.year}-${now.month}-${now.day}');
print('final items: $items');
// ============================================
// PART 3: CONST — COMPILE-TIME IMMUTABILITY
// ============================================
const pi = 3.14159;
const daysInWeek = 7;
const maxRetries = 3;
const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond; // ✅ 5000
// const now = DateTime.now(); // ❌ Error: not a compile-time constant
print('const pi: $pi');
print('const daysInWeek: $daysInWeek');
print('const maxRetries: $maxRetries');
print('const msUntilRetry: $msUntilRetry');
// ============================================
// PART 4: CONST LISTS AND CANONICALIZATION
// ============================================
const constList = [1, 2, 3];
// constList.add(4); // ❌ Error: unmodifiable list
const a = const ImmutablePoint(1, 1);
const b = const ImmutablePoint(1, 1);
print('identical(a, b): ${identical(a, b)}'); // ✅ true
// ============================================
// PART 5: CONST CONSTRUCTOR
// ============================================
class ImmutablePoint {
final double x;
final double y;
const ImmutablePoint(this.x, this.y);
}
// ============================================
// PART 6: FINAL LIST VS CONST LIST
// ============================================
final finalList = [1, 2, 3];
finalList.add(4); // ✅ valid — final reference, mutable list
const constList2 = [1, 2, 3];
// constList2.add(4); // ❌ Error: const list is immutable
// ============================================
// PART 7: NULL SAFETY
// ============================================
String name = 'Bob';
String? middleName; // nullable, defaults to null
print('name: $name');
print('middleName: $middleName');
// name = null; // ❌ Error: null is not assignable to String
middleName = null; // ✅ valid — nullable type
// ============================================
// PART 8: THE LATE MODIFIER
// ============================================
late String description;
description = 'This is late'; // ✅ assigned later
print(description);
// late final — lazily initialized, assigned once
late final String lazyValue = 'Computed once';
print(lazyValue);
The eight parts cover type inference, final, const, const lists and canonicalization, the const constructor, the difference between final and const lists, null safety, and the late modifier.
Quick Reference
The Variable Keywords
| Keyword | Type Inference | Reassignable | When Set | Deeply Immutable |
|---|---|---|---|---|
var | Yes | Yes | Runtime | No |
final | Yes | No | Runtime | No |
const | Yes | No | Compile time | Yes |
The Type Inference Examples
| Declaration | Inferred Type |
|---|---|
var count = 42 | int |
var name = 'Bob' | String |
var ratio = 3.14 | double |
var flag = true | bool |
var items = [1, 2, 3] | List<int> |
The Const Contexts
| Context | Example |
|---|---|
| Const variable | const x = 5; |
| Const list literal | const [1, 2, 3] |
| Const map literal | const {'a': 1} |
| Const constructor | const Point(1, 2) |
| Annotation | @immutable |
The Null Safety Operators
| Operator | Purpose |
|---|---|
Type? | Nullable type |
! | Assert non-null |
?. | Conditional access |
?? | Default value |
Best Practices
✅ Do This:
// Use var when the type is obvious
var count = 42; // ✅
// Use final for values that should not be reassigned
final appName = 'CodeArchaeology'; // ✅
// Use const for compile-time constants
const pi = 3.14159; // ✅
// Use const lists for immutable collections
const days = ['Mon', 'Tue', 'Wed']; // ✅
// Use String? for nullable variables
String? middleName; // ✅
❌ Don’t Do This:
// Don't use dynamic when a type is known
dynamic count = 42; // ❌
// Don't use final for compile-time constants
final pi = 3.14159; // const is better // ⚠️
// Don't try to modify a const list
const items = [1, 2, 3];
items.add(4); // ❌ Error // ❌
// Don't use var without an initializer
var name; // ❌ Error: type cannot be inferred // ❌
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
const with runtime value | DateTime.now() is not compile-time | Use final |
final list is modified | final marks the reference, not the contents | Use const for immutable contents |
unnecessary_const warning | const in a const context | Remove the redundant keyword |
| Type inference fails | var without initializer | Specify the type explicitly |
| Null dereference error | Accessing a nullable without checking | Use ?., ??, or ! |
Real-World Examples
1. Type Inference
var name = 'Bob'; // String
var count = 42; // int
2. Final Variable
final now = DateTime.now();
3. Const Variable
const pi = 3.14159;
4. Const List
const days = ['Mon', 'Tue', 'Wed'];
5. Const Constructor
class Point {
final double x, y;
const Point(this.x, this.y);
}
const origin = Point(0, 0);
6. Final List (Mutable Contents)
final items = [1, 2, 3];
items.add(4);
7. Nullable Variable
String? middleName;
8. Late Variable
late String description;
description = 'Assigned later';
9. Const Arithmetic
const msPerSecond = 1000;
const secondsUntilRetry = 5;
const msUntilRetry = secondsUntilRetry * msPerSecond;
10. Canonicalization
const a = Point(1, 1);
const b = Point(1, 1);
print(identical(a, b)); // true
Visual
The Variable Keywords
┌──────────────────────────────────────────────┐
│ var │
│ ├─ Type inferred │
│ ├─ Reassignable │
│ └─ Runtime value │
│ │
│ final │
│ ├─ Type inferred │
│ ├─ Single-assignment │
│ └─ Runtime value │
│ │
│ const │
│ ├─ Type inferred │
│ ├─ Single-assignment │
│ ├─ Compile-time value │
│ └─ Deeply immutable │
│ │
└──────────────────────────────────────────────┘
The final vs const Difference
┌──────────────────────────────────────────────┐
│ final items = [1, 2, 3]; │
│ items.add(4); // ✅ valid │
│ items = [4]; // ❌ Error │
│ │
│ const items = [1, 2, 3]; │
│ items.add(4); // ❌ Error │
│ items = [4]; // ❌ Error │
│ │
└──────────────────────────────────────────────┘
Canonicalization
┌──────────────────────────────────────────────┐
│ const a = Point(1, 1); │
│ const b = Point(1, 1); │
│ │
│ identical(a, b) → true │
│ │
│ The compiler creates one instance │
│ and reuses it for identical const values. │
│ │
└──────────────────────────────────────────────┘
Null Safety
┌──────────────────────────────────────────────┐
│ String name = 'Bob'; // non-nullable │
│ String? nickname; // nullable │
│ │
│ name = null; // ❌ Error │
│ nickname = null; // ✅ Valid │
│ │
│ nickname.length; // ❌ Error │
│ nickname?.length; // ✅ Valid │
│ nickname!.length; // ⚠️ Runtime risk │
│ │
└──────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Type inference | var infers from initializer |
final | Single-assignment at runtime |
const | Compile-time constant, deeply immutable |
final list | Reference is final, contents mutable |
const list | Contents immutable |
| Canonicalization | Identical const values share one instance |
| Null safety | Type? for nullable, ! to assert |
late | Assigned after declaration |
dynamic | Defers type checking to runtime |
Key takeaways:
vardeclares a variable with inferred type. The compiler reads the initializer and determines the type. The variable is still statically typed and cannot be reassigned to a different type .finalmeans single-assignment at runtime. The variable must be initialized, and once set, cannot be reassigned. The initializer can be a runtime expression likeDateTime.now().constmeans compile-time constant. The value must be computable at compile time. The value is deeply, transitively immutable. Two identicalconstexpressions produce the same object instance (canonicalization) .- A
finallist can be modified; aconstlist cannot. Thefinalkeyword marks the reference as single-assignment, not the contents. Theconstkeyword marks the entire value as immutable . - The
constkeyword can be omitted in a const context. When an expression is already in a constant context, addingconstis redundant. The analyzer reportsunnecessary_const. - Null safety is enforced by the compiler. A variable of type
Stringcannot contain null. A nullable variable must be checked before use. The!operator asserts non-null but throws at runtime if the assertion is wrong . - The
latemodifier allows deferred initialization. Alatevariable can be declared without an initializer and assigned later. It is useful for non-nullable variables that cannot be initialized at declaration .
Remember: Dart infers types but is strongly typed. var is the default. final is for values set once at runtime. const is for values known at compile time. The difference between final and const is not about mutability alone — it is about when the value is determined. const values are canonicalized and deeply immutable. Null safety prevents null dereference errors at compile time. The late modifier defers initialization. Choose the right keyword for the right value.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!