diff --git a/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php b/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php
new file mode 100644
index 0000000..a9b5673
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/ComputedItemListTrait.php
@@ -0,0 +1,148 @@
+<?php
+
+namespace Drupal\Core\TypedData;
+
+/**
+ * Provides common functionality for computed item lists.
+ *
+ * @see \Drupal\Core\TypedData\ListInterface
+ * @see \Drupal\Core\TypedData\Plugin\DataType\ItemList
+ *
+ * @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 doComputeValue() {
+    if ($this->valueComputed === FALSE) {
+      $this->computeValue();
+      $this->valueComputed = TRUE;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    $this->doComputeValue();
+    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->doComputeValue();
+    return parent::getString();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function get($index) {
+    $this->doComputeValue();
+    return parent::get($index);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function set($index, $value) {
+    // Ensure that existing values are loaded when setting a value, this also
+    // ensures that it is possible to set a new value immediately after loading
+    // an entity.
+    $this->doComputeValue();
+    return parent::set($index, $value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function appendItem($value = NULL) {
+    $this->doComputeValue();
+    return parent::appendItem($value);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function removeItem($index) {
+    $this->doComputeValue();
+    return parent::removeItem($index);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function isEmpty() {
+    $this->doComputeValue();
+    return parent::isEmpty();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function filter($callback) {
+    $this->doComputeValue();
+    return parent::filter($callback);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function offsetExists($offset) {
+    $this->doComputeValue();
+    return parent::offsetExists($offset);
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getIterator() {
+    $this->doComputeValue();
+    return parent::getIterator();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function count() {
+    $this->doComputeValue();
+    return parent::count();
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function applyDefaultValue($notify = TRUE) {
+    // Default values do not make sense for computed fields. However, this
+    // method can be overridden if needed.
+    return;
+  }
+
+}
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..cacc6e0 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
@@ -3,35 +3,22 @@
 namespace Drupal\entity_test\Plugin\Field;
 
 use Drupal\Core\Field\FieldItemList;
+use Drupal\Core\TypedData\ComputedItemListTrait;
 
 /**
  * A computed field item list.
  */
 class ComputedTestFieldItemList extends FieldItemList {
 
+  use ComputedItemListTrait;
+
   /**
    * Compute the list property from state.
    */
-  protected function computedListProperty() {
+  protected function computeValue() {
     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..adb04ab 100644
--- a/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
+++ b/core/tests/Drupal/KernelTests/Core/Entity/EntityFieldTest.php
@@ -739,13 +739,98 @@ public function testComputedProperties() {
   }
 
   /**
-   * Test computed fields.
+   * Tests all the interaction points of a computed field.
    */
   public function testComputedFields() {
     \Drupal::state()->set('entity_test_computed_field_item_list_value', ['foo computed']);
 
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::getValue().
     $entity = EntityTestComputedField::create([]);
-    $this->assertEquals($entity->computed_string_field->value, 'foo computed');
+    $this->assertSame([['value' => 'foo computed']], $entity->computed_string_field->getValue());
+
+    // Test \Drupal\Core\TypedData\ComputedItemListTrait::setValue(). This also
+    // checks that a subsequent getter does not try to re-compute the value.
+    $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());
+
+    // 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);
+
+    $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);
   }
 
   /**
