diff --git a/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php b/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php
new file mode 100644
index 0000000..113cb17
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php
@@ -0,0 +1,165 @@
+<?php
+
+namespace Drupal\Core\TypedData;
+
+/**
+ * Provides common functionality for computed item lists.
+ *
+ * @see \Drupal\Core\TypedData\ListInterface
+ * @see \Drupal\Core\TypedData\Plugin\DataType\ItemList
+ * @see \Drupal\Core\Field\FieldItemListInterface
+ * @see \Drupal\Core\Field\FieldItemList
+ *
+ * @ingroup typed_data
+ */
+trait ComputedItemListTrait {
+
+  /**
+   * Whether the values have already been computed or not.
+   *
+   * @var bool
+   */
+  protected $valueComputed = FALSE;
+
+  /**
+   * Computes the values for an item list.
+   */
+  abstract protected function computeValue();
+
+  /**
+   * Ensures that values are only computed once.
+   */
+  protected function ensureComputedValue() {
+    if ($this->valueComputed === FALSE) {
+      $this->computeValue();
+      $this->valueComputed = TRUE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    $this->ensureComputedValue();
+    return parent::getValue();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($values, $notify = TRUE) {
+    parent::setValue($values, $notify);
+
+    // Make sure that subsequent getter calls do not try to compute the values
+    // again.
+    $this->valueComputed = TRUE;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getString() {
+    $this->ensureComputedValue();
+    return parent::getString();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($index) {
+    if (!is_numeric($index)) {
+      throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.');
+    }
+
+    // Unlike the base implementation of
+    // \Drupal\Core\TypedData\ListInterface::get(), we do not add an empty item
+    // automatically because computed item lists need to behave like
+    // non-computed ones. For example, calling isEmpty() on a computed item list
+    // should return TRUE when the values were computed and the item list is
+    // truly empty.
+    // @see \Drupal\Core\TypedData\Plugin\DataType\ItemList::get().
+    $this->ensureComputedValue();
+
+    return isset($this->list[$index]) ? $this->list[$index] : NULL;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($index, $value) {
+    $this->ensureComputedValue();
+    return parent::set($index, $value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function appendItem($value = NULL) {
+    $this->ensureComputedValue();
+    return parent::appendItem($value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeItem($index) {
+    $this->ensureComputedValue();
+    return parent::removeItem($index);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    $this->ensureComputedValue();
+    return parent::isEmpty();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter($callback) {
+    // If the values were never computed, for example when the item list has not
+    // been interacted with during its lifecycle, there is no need to compute
+    // them now.
+    if ($this->valueComputed === FALSE) {
+      return $this;
+    }
+
+    return parent::filter($callback);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetExists($offset) {
+    $this->ensureComputedValue();
+    return parent::offsetExists($offset);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    $this->ensureComputedValue();
+    return parent::getIterator();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count() {
+    $this->ensureComputedValue();
+    return parent::count();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyDefaultValue($notify = TRUE) {
+    // Default values do not make sense for computed item lists. However, this
+    // method can be overridden if needed.
+    return $this;
+  }
+
+}
diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
index 4f756da..f2861d6 100644
--- a/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
+++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/ItemList.php
@@ -98,7 +98,10 @@ public function get($index) {
       throw new \InvalidArgumentException('Unable to get a value with a non-numeric delta in a list.');
     }
     // Automatically create the first item for computed fields.
+    // @deprecated in Drupal 8.4.x, will be removed before Drupal 9.0.0.
+    // Use \Drupal\Core\TypedData\ComputedItemListTrait instead.
     if ($index == 0 && !isset($this->list[0]) && $this->definition->isComputed()) {
+      @trigger_error('Automatically creating the first item for computed fields is deprecated in Drupal 8.4.x and will be removed before Drupal 9.0.0. Use \Drupal\Core\TypedData\ComputedItemListTrait instead.', E_USER_DEPRECATED);
       $this->list[0] = $this->createItem(0);
     }
     return isset($this->list[$index]) ? $this->list[$index] : NULL;
diff --git a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php
index 864d33f..0dfa3e1 100644
--- a/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php
+++ b/core/modules/system/tests/modules/entity_test/src/Plugin/Field/ComputedTestFieldItemList.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\entity_test\Plugin\Field;
 
+use Drupal\Core\TypedData\ComputedItemListTrait;
 use Drupal\Core\Field\FieldItemList;
 
 /**
@@ -9,29 +10,20 @@
  */
 class ComputedTestFieldItemList extends FieldItemList {
 
+  use ComputedItemListTrait;
+
   /**
    * Compute the list property from state.
    */
-  protected function computedListProperty() {
+  protected function computeValue() {
+    // Count the number of times this method has been executed during the
+    // lifecycle of an entity.
+    $execution_count = \Drupal::state()->get('computed_test_field_execution', 0);
+    \Drupal::state()->set('computed_test_field_execution', ++$execution_count);
+
     foreach (\Drupal::state()->get('entity_test_computed_field_item_list_value', []) as $delta => $item) {
       $this->list[$delta] = $this->createItem($delta, $item);
     }
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function get($index) {
-    $this->computedListProperty();
-    return isset($this->list[$index]) ? $this->list[$index] : NULL;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIterator() {
-    $this->computedListProperty();
-    return parent::getIterator();
-  }
-
 }
diff --git a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
index 31b1f9e..a42a74b 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
@@ -739,13 +739,135 @@ public function testComputedProperties() {
   }
 
   /**
-   * Test computed fields.
+   * Tests all the interaction points of a computed field.
    */
   public function testComputedFields() {
+    $this->installEntitySchema('entity_test_computed_field');
+
     \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']);
 
+    // Check that the values are not computed unnecessarily during the lifecycle
+    // of an entity when the field is not interacted with directly.
+    \Drupal::state()->set('computed_test_field_execution', 0);
+    $entity = EntityTestComputedField::create([]);
+    $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    $entity->name->value = $this->randomString();
+    $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    $entity->save();
+    $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::getValue().
+    \Drupal::state()->set('computed_test_field_execution', 0);
     $entity = EntityTestComputedField::create([]);
-    $this->assertEquals($entity->computed_string_field->value, 'foo computed');
+    $this->assertSame([['value' => 'foo computed']], $entity->computed_string_field->getValue());
+
+    // Check that the values are only computed once.
+    $this->assertSame(1, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::setValue(). This also
+    // checks that a subsequent getter does not try to re-compute the value.
+    \Drupal::state()->set('computed_test_field_execution', 0);
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->setValue([
+      ['value' => 'foo computed 1'],
+      ['value' => 'foo computed 2'],
+    ]);
+    $this->assertSame([['value' => 'foo computed 1'], ['value' => 'foo computed 2']], $entity->computed_string_field->getValue());
+
+    // Check that the values have not been computed when they were explicitly
+    // set.
+    $this->assertSame(0, \Drupal::state()->get('computed_test_field_execution', 0));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::getString().
+    $entity = EntityTestComputedField::create([]);
+    $this->assertSame('foo computed', $entity->computed_string_field->getString());
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::get().
+    $entity = EntityTestComputedField::create([]);
+    $this->assertSame('foo computed', $entity->computed_string_field->get(0)->value);
+    $this->assertEmpty($entity->computed_string_field->get(1));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::set().
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->set(1, 'foo computed 1');
+    $this->assertSame('foo computed', $entity->computed_string_field[0]->value);
+    $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value);
+    $entity->computed_string_field->set(0, 'foo computed 0');
+    $this->assertSame('foo computed 0', $entity->computed_string_field[0]->value);
+    $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value);
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::appendItem().
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->appendItem('foo computed 1');
+    $this->assertSame('foo computed', $entity->computed_string_field[0]->value);
+    $this->assertSame('foo computed 1', $entity->computed_string_field[1]->value);
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::removeItem().
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->removeItem(0);
+    $this->assertTrue($entity->computed_string_field->isEmpty());
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::isEmpty().
+    \Drupal::state()->set('entity_test_computed_field_item_list_value', []);
+    $entity = EntityTestComputedField::create([]);
+    $this->assertTrue($entity->computed_string_field->isEmpty());
+
+    \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']);
+    $entity = EntityTestComputedField::create([]);
+    $this->assertFalse($entity->computed_string_field->isEmpty());
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::filter().
+    $filter_callback = function ($item) {
+      return !$item->isEmpty();
+    };
+    $entity = EntityTestComputedField::create([]);
+    $entity->computed_string_field->filter($filter_callback);
+    $this->assertCount(1, $entity->computed_string_field);
+
+    // Add an empty item to the list and check that it is filtered out.
+    $entity->computed_string_field->appendItem();
+    $entity->computed_string_field->filter($filter_callback);
+    $this->assertCount(1, $entity->computed_string_field);
+
+    // Add a non-empty item to the list and check that it is not filtered out.
+    $entity->computed_string_field->appendItem('foo computed 1');
+    $entity->computed_string_field->filter($filter_callback);
+    $this->assertCount(2, $entity->computed_string_field);
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::offsetExists().
+    $entity = EntityTestComputedField::create([]);
+    $this->assertTrue($entity->computed_string_field->offsetExists(0));
+    $this->assertFalse($entity->computed_string_field->offsetExists(1));
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::getIterator().
+    $entity = EntityTestComputedField::create([]);
+    foreach ($entity->computed_string_field as $delta => $item) {
+      $this->assertSame('foo computed', $item->value);
+    }
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::count().
+    $entity = EntityTestComputedField::create([]);
+    $this->assertCount(1, $entity->computed_string_field);
+
+    // Check that computed items are not auto-created when they have no values.
+    \Drupal::state()->set('entity_test_computed_field_item_list_value', []);
+    $entity = EntityTestComputedField::create([]);
+    $this->assertCount(0, $entity->computed_string_field);
+
+    // Test \Drupal\Core\Field\FieldItemList::equals() for a computed field.
+    \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']);
+    $entity = EntityTestComputedField::create([]);
+    $computed_item_list1 = $entity->computed_string_field;
+
+    $entity = EntityTestComputedField::create([]);
+    $computed_item_list2 = $entity->computed_string_field;
+
+    $this->assertTrue($computed_item_list1->equals($computed_item_list2));
+
+    $computed_item_list2->value = 'foo computed 2';
+    $this->assertFalse($computed_item_list1->equals($computed_item_list2));
   }
 
   /**
