Inheritance and Polymorphism in Dart
last modified October 18, 2026
In this article we explore inheritance and polymorphism in Dart, the mechanisms that let one class build on another and let a single call site work with many related types.
Inheritance Overview
Inheritance lets a class reuse and specialise the state and behaviour of another class. The class being extended is the superclass (or base class) and the class that extends it is the subclass (or derived class). Dart supports single inheritance: a class may extend exactly one superclass, though it can implement many interfaces and mix in many mixins.
Polymorphism is the payoff of inheritance. A variable typed as the superclass can hold an instance of any subclass, and method calls are resolved against the runtime type of the object rather than its declared type. This is called dynamic dispatch and it lets you write code against a stable abstraction while the concrete behaviour varies.
| Keyword | Purpose | Example |
|---|---|---|
extends |
Create a subclass that inherits members | class Dog extends Animal |
super |
Refer to the superclass constructor or members | Dog(super.name); |
@override |
Mark a member that replaces a superclass member | @override void speak() |
abstract |
Prevent direct construction and allow abstract members | abstract class Shape |
is / as |
Test or cast to a type | if (s is Circle) |
base / final |
Restrict how a class may be extended | base class Account |
Extending a Class
A subclass is declared with the extends keyword. It inherits the
superclass's instance members and may override them. Constructors and static
members are not inherited, and private members are accessible only within their
defining library. The
@override annotation is optional but strongly recommended: it makes
the analyzer report when no matching member exists in a supertype.
class Animal {
final String name;
Animal(this.name);
void speak() => print('$name makes a sound');
}
class Dog extends Animal {
Dog(super.name);
@override
void speak() => print('$name barks');
}
void main() {
Animal a = Animal('Generic');
a.speak();
Animal d = Dog('Rex'); // upcast to the superclass type
d.speak();
}
Dog extends Animal and inherits its name
field. Constructors are not inherited: Dog(super.name) declares a
constructor that forwards its argument to Animal. The
speak method is overridden to produce
a dog-specific message. Because Dog is a subtype of
Animal, a Dog instance can be assigned to an
Animal-typed variable, shown by the upcast in main.
$ dart run basic_inheritance.dart Generic makes a sound Rex barks
Super Parameters
Dart offers super parameters: writing super.name in
a constructor forwards the argument to the superclass constructor without an
explicit initializer list. This removes the boilerplate of declaring a parameter
and passing it to super by hand.
class Vehicle {
final String brand;
final int wheels;
Vehicle(this.brand, {this.wheels = 4});
String describe() => '$brand with $wheels wheels';
}
class Car extends Vehicle {
final int doors;
Car(super.brand, {super.wheels, this.doors = 4});
@override
String describe() => '${super.describe()}, $doors doors';
}
void main() {
var car = Car('Toyota', doors: 2);
print(car.describe());
var bike = Car('Honda', wheels: 2, doors: 0);
print(bike.describe());
}
The Car constructor forwards brand and the optional
wheels straight to Vehicle using super parameters,
while adding its own doors field. The overridden
describe calls super.describe() to reuse the base
formatting and appends extra detail, so the derived behaviour builds on the
inherited one instead of duplicating it.
$ dart run super_parameters.dart Toyota with 4 wheels, 2 doors Honda with 2 wheels, 0 doors
Polymorphism and Dynamic Dispatch
When an instance method is called on a variable, Dart dispatches to the implementation of
the object's actual runtime type. This is polymorphism in action: the call site is
written against the superclass, but the subclass implementation runs. The
is operator tests the runtime type, and runtimeType
returns a Type object describing the runtime type. Prefer
is for type checks rather than comparing printed type names.
abstract class Shape {
double get area;
String get name;
@override
String toString() => '$name(${area.toStringAsFixed(2)})';
}
class Circle extends Shape {
final double r;
Circle(this.r);
@override
double get area => 3.14159 * r * r;
@override
String get name => 'Circle';
}
class Rectangle extends Shape {
final double w, h;
Rectangle(this.w, this.h);
@override
double get area => w * h;
@override
String get name => 'Rectangle';
}
void main() {
List<Shape> shapes = [Circle(2), Rectangle(3, 4)];
for (final s in shapes) {
print(s);
}
for (final s in shapes) {
if (s is Circle) print('circle radius ${s.r}');
}
print(shapes[0].runtimeType);
Object first = shapes.first;
print(first is Shape);
}
A List<Shape> holds both a Circle and a
Rectangle. The first loop calls toString through the
superclass reference, but each object uses its own name and
area implementation. The is Circle check promotes the
element so the subclass-only field r becomes accessible. Finally,
runtimeType confirms the concrete type, and the Object-typed
variable shows the same object still passes an is Shape test.
$ dart run polymorphism.dart Circle(12.57) Rectangle(12.00) circle radius 2.0 Circle true
Accessing Super Members
An overridden method can still invoke the superclass version with
super.member. This is the standard way to extend inherited behaviour
rather than replace it, and it keeps the base implementation as the single source
of truth.
class Employee {
final String name;
Employee(this.name);
double get salary => 1000;
@override
String toString() => 'Employee($name)';
}
class Manager extends Employee {
final double bonus;
Manager(super.name, this.bonus);
@override
double get salary => super.salary + bonus;
@override
String toString() => 'Manager($name, bonus: $bonus)';
}
void main() {
var e = Employee('Ann');
var m = Manager('Bob', 500);
print('${e.salary} ${m.salary}');
print(e);
print(m);
Object obj = m;
print(obj is Employee);
}
The Manager.salary getter calls super.salary to obtain
the base amount and then adds the bonus, so the derived value always reflects the
base salary. Both classes override toString to provide their own
formatting. The final is Employee check, made through an
Object-typed reference, confirms that a Manager is
substitutable for an Employee.
$ dart run super_members.dart 1000.0 1500.0 Employee(Ann) Manager(Bob, bonus: 500.0) true
Constructor Chaining Order
When a subclass is instantiated, constructor bodies run from the top of the
hierarchy down: the superclass constructor body executes before the subclass constructor
body. A subclass constructor must make sure the superclass is initialised, either
implicitly or through an explicit super call.
class Base {
Base() {
print('Base constructor');
}
}
class Middle extends Base {
Middle() {
print('Middle constructor');
}
}
class Leaf extends Middle {
Leaf() : super() {
print('Leaf constructor');
}
}
void main() => Leaf();
Constructing a Leaf triggers the whole chain. The
Base body runs first, then Middle, then
Leaf. Field initializers and the superclass constructor always
complete before a subclass constructor body runs, which guarantees that inherited
fields are initialized before that body executes, except fields explicitly
declared late. This does not mean all initialization runs base-first:
the subclass's field initializers and initializer list execute before the
superclass constructor. Avoid calling overridable members from constructors,
since the subclass constructor body has not run yet.
$ dart run constructor_chain.dart Base constructor Middle constructor Leaf constructor
Restricting Inheritance
Dart 3 class modifiers let a library author control how a class may be extended.
A base class can be extended but not implemented by outside code, and
any subclass must also be marked base, final, or
sealed. A final class cannot be extended or implemented
outside its defining library, but both operations are allowed inside that library.
Its subtypes must also be marked base, final, or
sealed. A Dart file and any of its part files form a
library; importing a file does not make it part of the importing library.
// base: subclasses may extend, but outside code may not implement
base class Account {
final String id;
Account(this.id);
void deposit(double amount) => print('deposit $amount to $id');
}
base class SavingsAccount extends Account {
SavingsAccount(super.id);
@override
void deposit(double amount) {
super.deposit(amount);
print('interest applied');
}
}
// final: no extending or implementing from another library
final class ClosedAccount extends SavingsAccount {
ClosedAccount(super.id);
}
void main() {
var acc = SavingsAccount('A-1');
acc.deposit(100);
var closed = ClosedAccount('A-2');
closed.deposit(50);
}
Marking Account as base means subclasses inherit its
implementation when they are declared in another library, which is exactly what
SavingsAccount does here through extends. Because the superclass is base,
SavingsAccount must also carry a modifier, so it is declared
base too. ClosedAccount is final, preventing
other libraries from adding subtypes. Further subtypes within this file's library
are still possible if they carry the required modifier.
$ dart run restricted_inheritance.dart deposit 100.0 to A-1 interest applied deposit 50.0 to A-2 interest applied
Source
Dart Language: Inheritance
Dart Language: Constructors
Dart Language: Class Modifiers
Dart Language: Methods
Inheritance lets classes share and specialise behaviour, while polymorphism lets
clients treat a family of types uniformly through a common superclass. Dart keeps
the model simple with single inheritance, adds convenience through super
parameters, and lets libraries constrain hierarchies with base,
final, and sealed modifiers.
Author
List all Dart tutorials.