| |

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.

KeywordWhen SetReassignableDeeply ImmutableCompile-Time Known
varRuntimeYesNoNo
finalRuntimeNoNoNo
constCompile timeNoYesYes

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

KeywordType InferenceReassignableWhen SetDeeply Immutable
varYesYesRuntimeNo
finalYesNoRuntimeNo
constYesNoCompile timeYes

The Type Inference Examples

DeclarationInferred Type
var count = 42int
var name = 'Bob'String
var ratio = 3.14double
var flag = truebool
var items = [1, 2, 3]List<int>

The Const Contexts

ContextExample
Const variableconst x = 5;
Const list literalconst [1, 2, 3]
Const map literalconst {'a': 1}
Const constructorconst Point(1, 2)
Annotation@immutable

The Null Safety Operators

OperatorPurpose
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

PitfallWhy It HappensFix
const with runtime valueDateTime.now() is not compile-timeUse final
final list is modifiedfinal marks the reference, not the contentsUse const for immutable contents
unnecessary_const warningconst in a const contextRemove the redundant keyword
Type inference failsvar without initializerSpecify the type explicitly
Null dereference errorAccessing a nullable without checkingUse ?., ??, 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

ItemValue
Type inferencevar infers from initializer
finalSingle-assignment at runtime
constCompile-time constant, deeply immutable
final listReference is final, contents mutable
const listContents immutable
CanonicalizationIdentical const values share one instance
Null safetyType? for nullable, ! to assert
lateAssigned after declaration
dynamicDefers type checking to runtime

Key takeaways:

  • var declares 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 .
  • final means single-assignment at runtime. The variable must be initialized, and once set, cannot be reassigned. The initializer can be a runtime expression like DateTime.now() .
  • const means compile-time constant. The value must be computable at compile time. The value is deeply, transitively immutable. Two identical const expressions produce the same object instance (canonicalization) .
  • A final list can be modified; a const list cannot. The final keyword marks the reference as single-assignment, not the contents. The const keyword marks the entire value as immutable .
  • The const keyword can be omitted in a const context. When an expression is already in a constant context, adding const is redundant. The analyzer reports unnecessary_const .
  • Null safety is enforced by the compiler. A variable of type String cannot 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 late modifier allows deferred initialization. A late variable 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!