ZetCode

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.

basic_inheritance.dart
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.

super_parameters.dart
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.

polymorphism.dart
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.

super_members.dart
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.

constructor_chain.dart
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.

restricted_inheritance.dart
// 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

My name is Jan Bodnar, and I am a passionate programmer with extensive programming experience. I have been writing programming articles since 2007. To date, I have authored over 1,400 articles and 8 e-books. I possess more than ten years of experience in teaching programming.

List all Dart tutorials.