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/path/src/Plugin/Field/FieldType/PathFieldItemList.php b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
index a98e718..1a5fbe0 100644
--- a/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
+++ b/core/modules/path/src/Plugin/Field/FieldType/PathFieldItemList.php
@@ -5,12 +5,42 @@
 use Drupal\Core\Access\AccessResult;
 use Drupal\Core\Field\FieldItemList;
 use Drupal\Core\Session\AccountInterface;
+use Drupal\Core\TypedData\ComputedItemListTrait;
 
 /**
  * Represents a configurable entity path field.
  */
 class PathFieldItemList extends FieldItemList {
 
+  use ComputedItemListTrait;
+
+  /**
+   * {@inheritdoc}
+   */
+  protected function computeValue() {
+    // Default the langcode to the current language if this is a new entity or
+    // there is no alias for an existent entity.
+    // @todo Set the langcode to not specified for untranslatable fields
+    //   in https://www.drupal.org/node/2689459.
+    $value = ['langcode' => $this->getLangcode()];
+
+    $entity = $this->getEntity();
+    if (!$entity->isNew()) {
+      // @todo Support loading languge neutral aliases in
+      //   https://www.drupal.org/node/2511968.
+      $alias = \Drupal::service('path.alias_storage')->load([
+        'source' => '/' . $entity->toUrl()->getInternalPath(),
+        'langcode' => $this->getLangcode(),
+      ]);
+
+      if ($alias) {
+        $value = $alias;
+      }
+    }
+
+    $this->list[0] = $this->createItem(0, $value);
+  }
+
   /**
    * {@inheritdoc}
    */
@@ -34,42 +64,4 @@ public function delete() {
     \Drupal::service('path.alias_storage')->delete($conditions);
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getValue($include_computed = FALSE) {
-    $this->ensureLoaded();
-    return parent::getValue($include_computed);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function isEmpty() {
-    $this->ensureLoaded();
-    return parent::isEmpty();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIterator() {
-    $this->ensureLoaded();
-    return parent::getIterator();
-  }
-
-  /**
-   * Automatically create the first item for computed fields.
-   *
-   * This ensures that ::getValue() and ::isEmpty() calls will behave like a
-   * non-computed field.
-   *
-   * @todo: Move this to the base class in https://www.drupal.org/node/2392845.
-   */
-  protected function ensureLoaded() {
-    if (!isset($this->list[0]) && $this->definition->isComputed()) {
-      $this->list[0] = $this->createItem(0);
-    }
-  }
-
 }
diff --git a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
index 9b5b67d..65cb14b 100644
--- a/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
+++ b/core/modules/path/src/Plugin/Field/FieldType/PathItem.php
@@ -24,13 +24,6 @@
 class PathItem extends FieldItemBase {
 
   /**
-   * Whether the alias has been loaded from the alias storage service yet.
-   *
-   * @var bool
-   */
-  protected $isLoaded = FALSE;
-
-  /**
    * {@inheritdoc}
    */
   public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
@@ -46,14 +39,6 @@ public static function propertyDefinitions(FieldStorageDefinitionInterface $fiel
   /**
    * {@inheritdoc}
    */
-  public function __get($name) {
-    $this->ensureLoaded();
-    return parent::__get($name);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public static function schema(FieldStorageDefinitionInterface $field_definition) {
     return [];
   }
@@ -61,62 +46,17 @@ public static function schema(FieldStorageDefinitionInterface $field_definition)
   /**
    * {@inheritdoc}
    */
-  public function getValue() {
-    $this->ensureLoaded();
-    return parent::getValue();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
   public function isEmpty() {
-    $this->ensureLoaded();
-    return parent::isEmpty();
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function getIterator() {
-    $this->ensureLoaded();
-    return parent::getIterator();
+    return ($this->alias === NULL || $this->alias === '') && ($this->pid === NULL || $this->pid === '') && ($this->langcode === NULL || $this->langcode === '');
   }
 
   /**
    * {@inheritdoc}
    */
   public function preSave() {
-    $this->alias = trim($this->alias);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function __set($name, $value) {
-    // Also ensure that existing values are loaded when setting a value, this
-    // ensures that it is possible to set a new value immediately after loading
-    // an entity.
-    $this->ensureLoaded();
-    parent::__set($name, $value);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function set($property_name, $value, $notify = TRUE) {
-    // Also ensure that existing values are loaded when setting a value, this
-    // ensures that it is possible to set a new value immediately after loading
-    // an entity.
-    $this->ensureLoaded();
-    return parent::set($property_name, $value, $notify);
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public function get($property_name) {
-    $this->ensureLoaded();
-    return parent::get($property_name);
+    if ($this->alias !== NULL) {
+      $this->alias = trim($this->alias);
+    }
   }
 
   /**
@@ -160,38 +100,4 @@ public static function mainPropertyName() {
     return 'alias';
   }
 
-  /**
-   * Ensures the alias properties are loaded if available.
-   *
-   * This ensures that the properties will always be loaded and act like
-   * non-computed fields when calling ::__get() and getValue().
-   *
-   * @todo: Determine if this should be moved to the base class in
-   *   https://www.drupal.org/node/2392845.
-   */
-  protected function ensureLoaded() {
-    if (!$this->isLoaded) {
-      $entity = $this->getEntity();
-      if (!$entity->isNew()) {
-        // @todo Support loading languge neutral aliases in
-        //   https://www.drupal.org/node/2511968.
-        $alias = \Drupal::service('path.alias_storage')->load([
-          'source' => '/' . $entity->toUrl()->getInternalPath(),
-          'langcode' => $this->getLangcode(),
-        ]);
-        if ($alias) {
-          $this->setValue($alias);
-        }
-        else {
-          // If there is no existing alias, default the langcode to the current
-          // language.
-          // @todo Set the langcode to not specified for untranslatable fields
-          //   in https://www.drupal.org/node/2689459.
-          $this->langcode = $this->getLangcode();
-        }
-      }
-      $this->isLoaded = TRUE;
-    }
-  }
-
 }
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);
   }
 
   /**
