diff --git a/core/modules/file/src/Entity/File.php b/core/modules/file/src/Entity/File.php
index a9ade9e7d6..e35ff2ec91 100644
--- a/core/modules/file/src/Entity/File.php
+++ b/core/modules/file/src/Entity/File.php
@@ -8,6 +8,7 @@
 use Drupal\Core\Entity\EntityTypeInterface;
 use Drupal\Core\Field\BaseFieldDefinition;
 use Drupal\file\FileInterface;
+use Drupal\file\FileUriItem;
 use Drupal\user\UserInterface;
 
 /**
@@ -250,6 +251,11 @@ public static function baseFieldDefinitions(EntityTypeInterface $entity_type) {
       ->setSetting('case_sensitive', TRUE)
       ->addConstraint('FileUriUnique');
 
+    // Override the default item class with a file specific implementation that
+    // provides an additional computed URL property.
+    // @see \Drupal\file\FileUriItem::propertyDefinitions()
+    $fields['uri']->getItemDefinition()->setClass(FileUriItem::class);
+
     $fields['filemime'] = BaseFieldDefinition::create('string')
       ->setLabel(t('File MIME type'))
       ->setSetting('is_ascii', TRUE)
diff --git a/core/modules/file/src/FileUriItem.php b/core/modules/file/src/FileUriItem.php
new file mode 100644
index 0000000000..74ef1593b0
--- /dev/null
+++ b/core/modules/file/src/FileUriItem.php
@@ -0,0 +1,30 @@
+<?php
+
+namespace Drupal\file;
+
+use Drupal\Core\Field\FieldStorageDefinitionInterface;
+use Drupal\Core\Field\Plugin\Field\FieldType\UriItem;
+use Drupal\Core\TypedData\DataDefinition;
+
+/**
+ * File specific implementation of a URI Item to provide a full URL.
+ */
+class FileUriItem extends UriItem {
+
+  /**
+   * {@inheritdoc}
+   */
+  public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
+    $properties = parent::propertyDefinitions($field_definition);
+
+    // @todo should this just be a string?
+    $properties['url'] = DataDefinition::create('uri')
+      ->setLabel(t('Full URL'))
+      ->setSetting('case_sensitive', $field_definition->getSetting('case_sensitive'))
+      ->setComputed(TRUE)
+      ->setClass(FileUrl::class);
+
+    return $properties;
+  }
+
+}
diff --git a/core/modules/file/src/FileUrl.php b/core/modules/file/src/FileUrl.php
new file mode 100644
index 0000000000..65a31f69ce
--- /dev/null
+++ b/core/modules/file/src/FileUrl.php
@@ -0,0 +1,48 @@
+<?php
+
+namespace Drupal\file;
+
+use Drupal\Core\TypedData\TypedData;
+
+/**
+ * Computed file URL property class.
+ */
+class FileUrl extends TypedData {
+
+  /**
+   * Cached URL.
+   *
+   * @var string|null
+   */
+  protected $url = NULL;
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getValue() {
+    if ($this->url !== NULL) {
+      return $this->url;
+    }
+
+    $this->url = $this->getParent()
+      ->getEntity()
+      ->toUrl()
+      ->setAbsolute()
+      ->toString();
+
+    return $this->url;
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  public function setValue($value, $notify = TRUE) {
+    $this->url = $value;
+
+    // Notify the parent of any changes.
+    if ($notify && isset($this->parent)) {
+      $this->parent->onChange($this->name);
+    }
+  }
+
+}
diff --git a/core/modules/file/tests/src/Kernel/FileUriItemTest.php b/core/modules/file/tests/src/Kernel/FileUriItemTest.php
new file mode 100644
index 0000000000..f2439da5f0
--- /dev/null
+++ b/core/modules/file/tests/src/Kernel/FileUriItemTest.php
@@ -0,0 +1,35 @@
+<?php
+
+namespace Drupal\Tests\file\Kernel;
+use Drupal\file\Entity\File;
+
+/**
+ * Custom URI field item test.
+ *
+ * @group file
+ */
+class FileUriItemTest extends FileManagedUnitTestBase {
+
+  /**
+   * Tests the file entity override of the Url field.
+   */
+  public function testCustomFileUriField() {
+    $uri = 'public://druplicon.txt';
+
+    // Create a new file entity.
+    $file = File::create([
+      'uid' => 1,
+      'filename' => 'druplicon.txt',
+      'uri' => $uri,
+      'filemime' => 'text/plain',
+      'status' => FILE_STATUS_PERMANENT,
+    ]);
+    file_put_contents($file->getFileUri(), 'hello world');
+
+    $file->save();
+
+    $this->assertSame($uri, $file->uri->value);
+    $this->assertSame(file_create_url($uri), $file->uri->url);
+  }
+
+}
diff --git a/core/modules/file/tests/src/Unit/FileUrlTest.php b/core/modules/file/tests/src/Unit/FileUrlTest.php
new file mode 100644
index 0000000000..ec0f22ce6f
--- /dev/null
+++ b/core/modules/file/tests/src/Unit/FileUrlTest.php
@@ -0,0 +1,85 @@
+<?php
+
+namespace Drupal\Tests\file\Unit;
+
+use Drupal\Core\Entity\EntityInterface;
+use Drupal\Core\Field\FieldItemInterface;
+use Drupal\Core\TypedData\DataDefinitionInterface;
+use Drupal\file\FileUrl;
+use Drupal\Tests\UnitTestCase;
+
+/**
+ * @coversDefaultClass \Drupal\file\FileUrl
+ *
+ * @group file
+ */
+class FileUrlTest extends UnitTestCase {
+
+  /**
+   * The test URL to use.
+   *
+   * @var string
+   */
+  protected $testUrl = 'https://localhost.test';
+
+  /**
+   * @covers ::getValue
+   */
+  public function testGetValue() {
+    $entity = $this->prophesize(EntityInterface::class);
+    $entity->url()
+      ->willReturn($this->testUrl);
+
+    $parent = $this->prophesize(FieldItemInterface::class);
+    $parent->getEntity()
+      ->shouldBeCalledTimes(1)
+      ->willReturn($entity->reveal());
+
+    $definition = $this->prophesize(DataDefinitionInterface::class);
+
+    $typed_data = new FileUrl($definition->reveal(), 'test', $parent->reveal());
+
+    $this->assertSame($this->testUrl, $typed_data->getValue());
+    // Do this a second time to confirm the same value is returned but the value
+    // isn't retrieved from the parent entity again.
+    $this->assertSame($this->testUrl, $typed_data->getValue());
+  }
+
+  /**
+   * @covers ::setValue
+   */
+  public function testSetValue() {
+    $parent = $this->prophesize(FieldItemInterface::class);
+    $parent->onChange('test')
+      ->shouldBeCalled();
+
+    $definition = $this->prophesize(DataDefinitionInterface::class);
+    $typed_data = new FileUrl($definition->reveal(), 'test', $parent->reveal());
+
+    // Setting the value should explicitly should mean the parent entity is
+    // never called into.
+    $typed_data->setValue($this->testUrl);
+
+    $this->assertSame($this->testUrl, $typed_data->getValue());
+    // Do this a second time to confirm the same value is returned but the value
+    // isn't retrieved from the parent entity again.
+    $this->assertSame($this->testUrl, $typed_data->getValue());
+  }
+
+  /**
+   * @covers ::setValue
+   */
+  public function testSetValueNoNotify() {
+    $parent = $this->prophesize(FieldItemInterface::class);
+    $parent->onChange('test')
+      ->shouldNotBeCalled();
+
+    $definition = $this->prophesize(DataDefinitionInterface::class);
+    $typed_data = new FileUrl($definition->reveal(), 'test', $parent->reveal());
+
+    // Setting the value should explicitly should mean the parent entity is
+    // never called into.
+    $typed_data->setValue($this->testUrl, FALSE);
+  }
+
+}
