diff --git a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php
index 3de0666d35b1f7e4999bf9c99ae906428b0e25e0..98f0f831810072fdbdf940f74e83e4fdffbc8015 100644
--- a/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php
+++ b/core/lib/Drupal/Core/Field/Plugin/Field/FieldType/DecimalItem.php
@@ -40,7 +40,7 @@ public static function defaultStorageSettings() {
    * {@inheritdoc}
    */
   public static function propertyDefinitions(FieldStorageDefinitionInterface $field_definition) {
-    $properties['value'] = DataDefinition::create('string')
+    $properties['value'] = DataDefinition::create('decimal')
       ->setLabel(new TranslatableMarkup('Decimal value'))
       ->setRequired(TRUE);
 
@@ -92,24 +92,6 @@ public function storageSettingsForm(array &$form, FormStateInterface $form_state
     return $element;
   }
 
-  /**
-   * {@inheritdoc}
-   */
-  public function getConstraints() {
-    $constraint_manager = \Drupal::typedDataManager()->getValidationConstraintManager();
-    $constraints = parent::getConstraints();
-
-    $constraints[] = $constraint_manager->create('ComplexData', [
-      'value' => [
-        'Regex' => [
-          'pattern' => '/^[+-]?((\d+(\.\d*)?)|(\.\d+))$/i',
-        ],
-      ],
-    ]);
-
-    return $constraints;
-  }
-
   /**
    * {@inheritdoc}
    */
diff --git a/core/lib/Drupal/Core/TypedData/Plugin/DataType/DecimalData.php b/core/lib/Drupal/Core/TypedData/Plugin/DataType/DecimalData.php
new file mode 100644
index 0000000000000000000000000000000000000000..534207eef0c5258d015aac3b44d2ce1378ea58e1
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Plugin/DataType/DecimalData.php
@@ -0,0 +1,28 @@
+<?php
+
+namespace Drupal\Core\TypedData\Plugin\DataType;
+
+use Drupal\Core\TypedData\Type\DecimalInterface;
+
+/**
+ * The decimal data type.
+ *
+ * Decimal type is stored as "decimal" in the relational database. Because PHP
+ * does not have a primitive type decimal and using float can result in
+ * unexpected rounding behavior, it is implemented and displayed as string.
+ *
+ * @DataType(
+ *   id = "decimal",
+ *   label = @Translation("Decimal")
+ * )
+ */
+class DecimalData extends StringData implements DecimalInterface {
+
+  /**
+   * {@inheritdoc}
+   */
+  public function getCastedValue() {
+    return $this->getString() ?: '0.0';
+  }
+
+}
diff --git a/core/lib/Drupal/Core/TypedData/Type/DecimalInterface.php b/core/lib/Drupal/Core/TypedData/Type/DecimalInterface.php
new file mode 100644
index 0000000000000000000000000000000000000000..c7a09a6b4677fd5a4d6f73e65d4bd8befd1d3930
--- /dev/null
+++ b/core/lib/Drupal/Core/TypedData/Type/DecimalInterface.php
@@ -0,0 +1,17 @@
+<?php
+
+namespace Drupal\Core\TypedData\Type;
+
+use Drupal\Core\TypedData\PrimitiveInterface;
+
+/**
+ * Interface for decimal numbers.
+ *
+ * The plain value of a decimal is a PHP string. For setting the value
+ * any PHP variable that casts to an numeric string may be passed.
+ *
+ * @ingroup typed_data
+ */
+interface DecimalInterface extends PrimitiveInterface {
+
+}
diff --git a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
index aa9c5bb4057c77a35a6f69050b547d01a77e726f..5533be502931d753d3a44ba0951302b5f6551099 100644
--- a/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
+++ b/core/lib/Drupal/Core/Validation/Plugin/Validation/Constraint/PrimitiveTypeConstraintValidator.php
@@ -5,6 +5,7 @@
 use Drupal\Core\TypedData\Type\BinaryInterface;
 use Drupal\Core\TypedData\Type\BooleanInterface;
 use Drupal\Core\TypedData\Type\DateTimeInterface;
+use Drupal\Core\TypedData\Type\DecimalInterface;
 use Drupal\Core\TypedData\Type\DurationInterface;
 use Drupal\Core\TypedData\Type\FloatInterface;
 use Drupal\Core\TypedData\Type\IntegerInterface;
@@ -48,6 +49,9 @@ public function validate($value, Constraint $constraint) {
     if ($typed_data instanceof IntegerInterface && filter_var($value, FILTER_VALIDATE_INT) === FALSE) {
       $valid = FALSE;
     }
+    if ($typed_data instanceof DecimalInterface && !preg_match('/^[+-]?((\d+(\.\d*)?)|(\.\d+))$/i', $value)) {
+      $valid = FALSE;
+    }
     if ($typed_data instanceof StringInterface && !is_scalar($value) && !($value instanceof MarkupInterface)) {
       $valid = FALSE;
     }
diff --git a/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataTest.php b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataTest.php
index 30acca31c269db048f3dd62757a7383ff6af4caa..147f39a1d581e458b177832509644c2eaafc8bbb 100644
--- a/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataTest.php
+++ b/core/tests/Drupal/KernelTests/Core/TypedData/TypedDataTest.php
@@ -10,6 +10,7 @@
 use Drupal\Core\TypedData\Type\BinaryInterface;
 use Drupal\Core\TypedData\Type\BooleanInterface;
 use Drupal\Core\TypedData\Type\DateTimeInterface;
+use Drupal\Core\TypedData\Type\DecimalInterface;
 use Drupal\Core\TypedData\Type\DurationInterface;
 use Drupal\Core\TypedData\Type\FloatInterface;
 use Drupal\Core\TypedData\Type\IntegerInterface;
@@ -118,6 +119,27 @@ public function testGetAndSet() {
     $typed_data->setValue('invalid');
     $this->assertEquals(1, $typed_data->validate()->count(), 'Validation detected invalid value.');
 
+    // Decimal type.
+    $value = (string) (mt_rand(1, 10000) / 100);
+    $typed_data = $this->createTypedData(['type' => 'decimal'], $value);
+    $this->assertInstanceOf(DecimalInterface::class, $typed_data);
+    $this->assertSame($value, $typed_data->getValue(), 'Decimal value was fetched.');
+    $this->assertEquals(0, $typed_data->validate()->count());
+    $new_value = (string) (mt_rand(1, 10000) / 100);
+    $typed_data->setValue($new_value);
+    $this->assertSame($new_value, $typed_data->getValue(), 'Decimal value was changed.');
+    $this->assertIsString($typed_data->getString());
+    $this->assertEquals(0, $typed_data->validate()->count());
+    $typed_data->setValue(NULL);
+    $this->assertNull($typed_data->getValue(), 'Decimal wrapper is null-able.');
+    $this->assertEquals(0, $typed_data->validate()->count());
+    $typed_data->setValue(0);
+    $this->assertSame('0.0', $typed_data->getCastedValue(), '0.0 casted value was fetched.');
+    $typed_data->setValue('1337e0');
+    $this->assertEquals(1, $typed_data->validate()->count(), 'Scientific notation is not allowed in numeric type.');
+    $typed_data->setValue('invalid');
+    $this->assertEquals(1, $typed_data->validate()->count(), 'Validation detected invalid value.');
+
     // Float type.
     $value = 123.45;
     $typed_data = $this->createTypedData(['type' => 'float'], $value);