Dart Sound Type System
Core Definition
Dart is a sound statically typed language secured by dual safeguards: static compile-time checks plus runtime validation. At runtime, the value stored in any variable will always match its statically declared type.
Type rules are enforced rigorously, yet type annotations are optional — types are automatically inferred by the compiler.
Static analysis during compilation catches most type errors in advance.
What Is Soundness
Your code can never enter an invalid type state. If an expression has a static type of String, it will only yield string values at runtime with no other types allowed. Comparable languages: Java, C#.
- Compile-time interception:
int a = "abc";throws an immediate error. - Runtime interception: Casting via
asthrows an exception if type matching fails.
Four Key Benefits
Type bugs surface at compile time, avoiding hard-to-reproduce production crashes caused by type mismatches.
Improved code readability — types never mislead developers about data shapes.
Easier maintainability: Editing one piece of code triggers type checks that flag every dependent section affected by the change.
Higher AOT compilation performance: Type metadata helps the compiler generate more optimized machine code.
Examples
Static error triggered by dynamic collections
// Expects List<int>
void printInts(List<int> a) => print(a);
void main() {
// No generic declaration, inferred as List<dynamic>
final list = [];
list.add(1);
list.add('2'); // Dynamic list permits mixed types
printInts(list);
// Compile error: List<dynamic> cannot be assigned to List<int>
}Code language: Dart (dart)
Explicit generic constraints declaration
void printInts(List<int> a) => print(a);
void main() {
final list = <int>[]; // Explicitly specify generic int
list.add(1);
// list.add('2'); // Compile error, cannot insert string
list.add(2);
printInts(list); // Passes static validation normally
}Code language: Dart (dart)
Three Core Rules
1. When overriding methods, the return type must be the original type or its subtype (Producer Rule)
Return values of superclass methods act as producers. Subclasses may return more specific subtypes but cannot return unrelated types.
Example
Valid Case
class Animal {
Animal get parent => Animal();
}
class HoneyBadger extends Animal {
@override
HoneyBadger get parent => HoneyBadger(); // Valid subtype
}Code language: Dart (dart)
Invalid Case
class Root {}
class HoneyBadger extends Animal {
@override
Root get parent => Root(); // Unrelated type, compile error
}Code language: Dart (dart)
2. When overriding methods, parameter types must match the original type or its supertype
Method parameters are consumers. Subclasses may only widen parameter types to supertypes; narrowing to subtypes breaks type safety.
Valid example (parameter widened to Object)
class Animal {
void chase(Animal a) {}
}
class HoneyBadger extends Animal {
@override
void chase(Object a) {} // Object is supertype of Animal, valid
}Code language: Dart (dart)
Invalid example (parameter narrowed to subclass Mouse)
class Mouse extends Animal {}
class Cat extends Animal {
@override
void chase(Mouse a) {} // Narrowed parameter type, static error
}
void main() {
Animal cat = Cat();
cat.chase(Alligator()); // Hypothetically allows Alligator input, causing unsafe typing
}Code language: Dart (dart)
Rule 3: Dynamic generic collections cannot be directly assigned to concrete generic collections
List<dynamic> and List<Dog> cannot be assigned directly to List<Cat> and will throw compile errors. Implicit conversion is unsupported between different generic collection types.
class Cat {}
class Dog {}
void main() {
List<Cat> foo = <dynamic>[Dog()]; // Static compile error
List<dynamic> bar = <dynamic>[Dog(), Cat()]; // Valid, dynamic list allows mixed types
}Code language: Dart (dart)
Runtime Type Validation
Static checks do not cover all scenarios. Additional runtime type validation runs and throws exceptions upon mismatches.
Runtime error from generic casting
class Animal {}
class Dog extends Animal {}
class Cat extends Animal {}
void main() {
List<Animal> animals = <Dog>[Dog()];
List<Cat> cats = animals as List<Cat>;
// Runtime exception: Cannot cast List<Dog> to List<Cat>
}Code language: Dart (dart)
Implicit downcast from dynamic (runtime checked)
Assigning a dynamic variable to a concrete type triggers implicit conversion validated at runtime, with exceptions thrown for mismatched types.
int assumeString(dynamic object) {
String string = object; // Runtime check verifying object is String
return string.length;
}
void main() {
assumeString("test"); // Executes normally
assumeString(123); // Runtime error: int cannot be cast to String
}Code language: Dart (dart)
Disable Implicit Casts (Strict Type Mode)
Enable strict casting inside analysis_options.yaml to block implicit downcasts from dynamic
analyzer:
language:
strict-casts: trueCode language: JavaScript (javascript)
Type Inference
Dart automatically infers types for variables, generics, and method return values. If insufficient type information exists, the type defaults to dynamic.
Basic variable inference
// Automatically inferred as Map<String, Object>
var arguments = {'argA': 'hello', 'argB': 42};
var x = 3; // x inferred as int
x = 4.0; // Error, int cannot store double
num y = 3; // Explicitly declared num, accepts int/double
y = 4.0; // ValidCode language: Dart (dart)
Generic constructor / collection inference
List<int> listOfInt = []; // Inferred as <int>[]
var listOfDouble = [3.0]; // Inferred List<double>
var ints = listOfDouble.map((x) => x.toInt());
// x inferred double, callback returns int → ints inferred Iterable<int>Code language: Dart (dart)
Sound Type System