diff --git a/core/config/schema/core.data_types.schema.yml b/core/config/schema/core.data_types.schema.yml
index 56cc1506b4128eccc3d00578b2429c3da8edaf4b..8935e21ef151e9f8208f8cb13d4556958fd5d675 100644
--- a/core/config/schema/core.data_types.schema.yml
+++ b/core/config/schema/core.data_types.schema.yml
@@ -25,6 +25,9 @@ boolean:
 email:
   label: 'Email'
   class: '\Drupal\Core\TypedData\Plugin\DataType\Email'
+  constraints:
+    Email:
+      message: "%value is not a valid email address."
 integer:
   label: 'Integer'
   class: '\Drupal\Core\TypedData\Plugin\DataType\IntegerData'
diff --git a/core/lib/Drupal/Core/Entity/EntityForm.php b/core/lib/Drupal/Core/Entity/EntityForm.php
index 90d3d635ff0c69fa440e3e451947228a3467a722..18a70a62dfb78b92f5c7163e5686c417fe5b2b6a 100644
--- a/core/lib/Drupal/Core/Entity/EntityForm.php
+++ b/core/lib/Drupal/Core/Entity/EntityForm.php
@@ -323,6 +323,8 @@ public function buildEntity(array $form, FormStateInterface $form_state) {
    *   A nested array of form elements comprising the form.
    * @param \Drupal\Core\Form\FormStateInterface $form_state
    *   The current state of the form.
+   *
+   * @see \Drupal\Core\Form\ConfigFormBase::copyFormValuesToConfig()
    */
   protected function copyFormValuesToEntity(EntityInterface $entity, array $form, FormStateInterface $form_state) {
     $values = $form_state->getValues();
diff --git a/core/lib/Drupal/Core/Form/ConfigFormBase.php b/core/lib/Drupal/Core/Form/ConfigFormBase.php
index 707e580d5a6d822251109bb131fc5fc6c6733cf8..eefc0ea5ba7d22637e01cbcb7876fcb681ff00de 100644
--- a/core/lib/Drupal/Core/Form/ConfigFormBase.php
+++ b/core/lib/Drupal/Core/Form/ConfigFormBase.php
@@ -2,11 +2,17 @@
 
 namespace Drupal\Core\Form;
 
+use Drupal\Core\Config\Config;
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
 use Symfony\Component\DependencyInjection\ContainerInterface;
 
 /**
  * Base class for implementing system configuration forms.
+ *
+ * Subclasses of this form can choose to use config validation instead of form-
+ * -specific validation logic. To do that, override copyFormValuesToConfig().
  */
 abstract class ConfigFormBase extends FormBase {
   use ConfigFormBaseTrait;
@@ -16,9 +22,18 @@ abstract class ConfigFormBase extends FormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface|null $typedConfigManager
+   *   The typed config manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory) {
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    protected ?TypedConfigManagerInterface $typedConfigManager = NULL,
+  ) {
     $this->setConfigFactory($config_factory);
+    if ($this->typedConfigManager === NULL) {
+      @trigger_error('Calling ConfigFormBase::__construct() without the $typedConfigManager argument is deprecated in drupal:10.2.0 and will be required in drupal:11.0.0. See https://www.drupal.org/node/3373502', E_USER_DEPRECATED);
+      $this->typedConfigManager = \Drupal::service('config.typed');
+    }
   }
 
   /**
@@ -26,7 +41,8 @@ public function __construct(ConfigFactoryInterface $config_factory) {
    */
   public static function create(ContainerInterface $container) {
     return new static(
-      $container->get('config.factory')
+      $container->get('config.factory'),
+      $container->get('config.typed')
     );
   }
 
@@ -47,11 +63,173 @@ public function buildForm(array $form, FormStateInterface $form_state) {
     return $form;
   }
 
+  /**
+   * {@inheritdoc}
+   */
+  public function validateForm(array &$form, FormStateInterface $form_state) {
+    assert($this->typedConfigManager instanceof TypedConfigManagerInterface);
+    foreach ($this->getEditableConfigNames() as $config_name) {
+      $config = $this->config($config_name);
+      try {
+        static::copyFormValuesToConfig($config, $form_state);
+      }
+      catch (\BadMethodCallException $e) {
+        // Nothing to do: this config form does not yet use validation
+        // constraints. Continue trying the other editable config, to allow
+        // partial adoption.
+        continue;
+      }
+      $typed_config = $this->typedConfigManager->createFromNameAndData($config_name, $config->getRawData());
+
+      $violations = $typed_config->validate();
+      // Rather than immediately applying all violation messages to the
+      // corresponding form elements, first collect the messages. The structure
+      // of the form may cause a single form element to contain multiple config
+      // property paths thanks to `type: sequence`. Common example: a <textarea>
+      // with one line per sequence item.
+      // @see \Drupal\Core\Config\Schema\Sequence
+      // @see \Drupal\Core\Config\Schema\SequenceDataDefinition
+      $violations_per_form_element = [];
+      foreach ($violations as $violation) {
+        $property_path = $violation->getPropertyPath();
+        $form_element_name = static::mapConfigKeyToFormElementName($config_name, $property_path);
+        // Default to index 0.
+        $index = 0;
+        // Detect if this is a sequence property path, and if so, determine the
+        // actual sequence index.
+        $matches = [];
+        if (preg_match("/.*\.(\d+)$/", $property_path, $matches) === 1) {
+          $index = intval($matches[1]);
+        }
+        $violations_per_form_element[$form_element_name][$index] = $violation;
+      }
+
+      // Now that we know how many constraint violation messages exist per form
+      // element, set them. This is crucial because FormState::setErrorByName()
+      // only allows a single validation error message per form element.
+      // @see \Drupal\Core\Form\FormState::setErrorByName()
+      foreach ($violations_per_form_element as $form_element_name => $violations) {
+        // When only a single message exists, just set it.
+        if (count($violations) === 1) {
+          $form_state->setErrorByName($form_element_name, reset($violations)->getMessage());
+          continue;
+        }
+
+        // However, if multiple exist, that implies it's a single form element
+        // containing a `type: sequence`.
+        $form_state->setErrorByName($form_element_name, $this->formatMultipleViolationsMessage($form_element_name, $violations));
+      }
+    }
+  }
+
+  /**
+   * Formats multiple violation messages associated with a single form element.
+   *
+   * Validation constraints only know the internal data structure (the
+   * configuration schema structure), but this need not be a disadvantage:
+   * rather than informing the user some values are wrong, it is possible
+   * guide them directly to the Nth entry in the sequence.
+   *
+   * To further improve the user experience, it is possible to override
+   * method in subclasses to use specific knowledge about the structure of the
+   * form and the nature of the data being validated, to instead generate more
+   * precise and/or shortened violation messages.
+   *
+   * @param string $form_element_name
+   *   The form element for which to format multiple violation messages.
+   * @param \Symfony\Component\Validator\ConstraintViolationListInterface $violations
+   *   The list of constraint violations that apply to this form element.
+   *
+   * @return \Drupal\Core\StringTranslation\TranslatableMarkup
+   */
+  protected function formatMultipleViolationsMessage(string $form_element_name, array $violations): TranslatableMarkup {
+    $transformed_message_parts = [];
+    foreach ($violations as $index => $violation) {
+      // Note that `@validation_error_message` (should) already contain a
+      // trailing period, hence it is intentionally absent here.
+      $transformed_message_parts[] = $this->t('Entry @human_index: @validation_error_message', [
+        // Humans start counting from 1, not 0.
+        '@human_index' => $index + 1,
+        // Translators may not necessarily know what "violation constraint
+        // messages" are, but they definitely know "validation errors".
+        '@validation_error_message' => $violation->getMessage(),
+      ]);
+    }
+    return $this->t(implode("\n", $transformed_message_parts));
+  }
+
   /**
    * {@inheritdoc}
    */
   public function submitForm(array &$form, FormStateInterface $form_state) {
+    foreach ($this->getEditableConfigNames() as $config_name) {
+      $config = $this->config($config_name);
+      try {
+        static::copyFormValuesToConfig($config, $form_state);
+        $config->save();
+      }
+      catch (\BadMethodCallException $e) {
+        // Nothing to do: this config form does not yet use validation
+        // constraints. Continue trying the other editable config, to allow
+        // partial adoption.
+        continue;
+      }
+    }
     $this->messenger()->addStatus($this->t('The configuration options have been saved.'));
   }
 
+  /**
+   * Copies form values to Config keys.
+   *
+   * This should not change existing Config key-value pairs that are not being
+   * edited by this form.
+   *
+   * @param \Drupal\Core\Config\Config $config
+   *   The configuration being edited.
+   * @param \Drupal\Core\Form\FormStateInterface $form_state
+   *   The current state of the form.
+   *
+   * @see \Drupal\Core\Entity\EntityForm::copyFormValuesToEntity()
+   */
+  protected static function copyFormValuesToConfig(Config $config, FormStateInterface $form_state): void {
+    // This allows ::submitForm() and ::validateForm() to know that this config
+    // form is not yet using constraint-based validation.
+    throw new \BadMethodCallException();
+  }
+
+  /**
+   * Maps the given Config key to a form element name.
+   *
+   * @param string $config_name
+   *   The name of the Config whose value triggered a validation error.
+   * @param string $key
+   *   The Config key that triggered a validation error (which corresponds to a
+   *   property path on the validation constraint violation).
+   *
+   * @return string
+   *   The corresponding form element name.
+   */
+  protected static function mapConfigKeyToFormElementName(string $config_name, string $key) : string {
+    return self::defaultMapConfigKeyToFormElementName($config_name, $key);
+  }
+
+  /**
+   * Default implementation for ::mapConfigKeyToFormElementName().
+   *
+   * Suitable when the configuration is mapped 1:1 to form elements: when the
+   * keys in the Config match the form element names exactly.
+   *
+   * @param string $config_name
+   *   The name of the Config whose value triggered a validation error.
+   * @param string $key
+   *   The Config key that triggered a validation error (which corresponds to a
+   *   property path on the validation constraint violation).
+   *
+   * @return string
+   *   The corresponding form element name.
+   */
+  final protected static function defaultMapConfigKeyToFormElementName(string $config_name, string $key) : string {
+    return str_replace('.', '][', $key);
+  }
+
 }
diff --git a/core/misc/cspell/dictionary.txt b/core/misc/cspell/dictionary.txt
index a4cd7d4c83f08dec90ba76227815186e8e0b7f03..b1a8df4dce915ae7899a6b496c19a12dd0eaf8a5 100644
--- a/core/misc/cspell/dictionary.txt
+++ b/core/misc/cspell/dictionary.txt
@@ -1324,6 +1324,7 @@ userid
 userinfo
 userref
 usuario
+validatable
 vampirize
 varchar
 veniam
diff --git a/core/modules/language/src/Form/NegotiationBrowserForm.php b/core/modules/language/src/Form/NegotiationBrowserForm.php
index 1ffd1a2c5c3fc8cda6f34d61b5a1c4d36a71b3a9..e0e50442de8a5e9c4d5f19cb60f79f3afa312fb2 100644
--- a/core/modules/language/src/Form/NegotiationBrowserForm.php
+++ b/core/modules/language/src/Form/NegotiationBrowserForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\language\Form;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Url;
@@ -26,8 +27,8 @@ class NegotiationBrowserForm extends ConfigFormBase {
   /**
    * {@inheritdoc}
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ConfigurableLanguageManagerInterface $language_manager) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ConfigurableLanguageManagerInterface $language_manager) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->languageManager = $language_manager;
   }
 
@@ -37,6 +38,7 @@ public function __construct(ConfigFactoryInterface $config_factory, Configurable
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('language_manager')
     );
   }
diff --git a/core/modules/language/src/Form/NegotiationConfigureForm.php b/core/modules/language/src/Form/NegotiationConfigureForm.php
index 2b808d54d408d54d041f5801e91dfaf0e4ed83ee..d25305f0306ca6faaf15e575f87e14aedf1f29a4 100644
--- a/core/modules/language/src/Form/NegotiationConfigureForm.php
+++ b/core/modules/language/src/Form/NegotiationConfigureForm.php
@@ -4,6 +4,7 @@
 
 use Drupal\Core\Block\BlockManagerInterface;
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Entity\EntityStorageInterface;
 use Drupal\Core\Extension\ThemeHandlerInterface;
 use Drupal\Core\Form\ConfigFormBase;
@@ -68,6 +69,8 @@ class NegotiationConfigureForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\language\ConfigurableLanguageManagerInterface $language_manager
    *   The language manager.
    * @param \Drupal\language\LanguageNegotiatorInterface $negotiator
@@ -79,8 +82,8 @@ class NegotiationConfigureForm extends ConfigFormBase {
    * @param \Drupal\Core\Entity\EntityStorageInterface $block_storage
    *   The block storage, or NULL if not available.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, BlockManagerInterface $block_manager, ThemeHandlerInterface $theme_handler, EntityStorageInterface $block_storage = NULL) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ConfigurableLanguageManagerInterface $language_manager, LanguageNegotiatorInterface $negotiator, BlockManagerInterface $block_manager, ThemeHandlerInterface $theme_handler, EntityStorageInterface $block_storage = NULL) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->languageTypes = $this->config('language.types');
     $this->languageManager = $language_manager;
     $this->negotiator = $negotiator;
@@ -97,6 +100,7 @@ public static function create(ContainerInterface $container) {
     $block_storage = $entity_type_manager->hasHandler('block', 'storage') ? $entity_type_manager->getStorage('block') : NULL;
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('language_manager'),
       $container->get('language_negotiator'),
       $container->get('plugin.manager.block'),
diff --git a/core/modules/language/src/Form/NegotiationUrlForm.php b/core/modules/language/src/Form/NegotiationUrlForm.php
index f0f0a39f0bb887534be8ed8245868ffd2c379afe..6cb54e4a322843cf722c2c03fcaf1f379f58017f 100644
--- a/core/modules/language/src/Form/NegotiationUrlForm.php
+++ b/core/modules/language/src/Form/NegotiationUrlForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\language\Form;
 
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Language\LanguageInterface;
@@ -30,11 +31,13 @@ class NegotiationUrlForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Language\LanguageManagerInterface $language_manager
    *   The language manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, LanguageManagerInterface $language_manager) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, LanguageManagerInterface $language_manager) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->languageManager = $language_manager;
   }
 
@@ -44,6 +47,7 @@ public function __construct(ConfigFactoryInterface $config_factory, LanguageMana
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('language_manager')
     );
   }
diff --git a/core/modules/media/src/Form/MediaSettingsForm.php b/core/modules/media/src/Form/MediaSettingsForm.php
index b70aa19d24f64cd99f1c6242c3e00d8c71053a2e..3d8a4e373c570fe66928e9d290e48305d04d5c71 100644
--- a/core/modules/media/src/Form/MediaSettingsForm.php
+++ b/core/modules/media/src/Form/MediaSettingsForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\media\Form;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Entity\EntityTypeManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Form\ConfigFormBase;
@@ -35,13 +36,15 @@ class MediaSettingsForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The config factory service.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\media\IFrameUrlHelper $iframe_url_helper
    *   The iFrame URL helper service.
    * @param \Drupal\Core\Entity\EntityTypeManagerInterface $entity_type_manager
    *   The entity type manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, IFrameUrlHelper $iframe_url_helper, EntityTypeManagerInterface $entity_type_manager) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, IFrameUrlHelper $iframe_url_helper, EntityTypeManagerInterface $entity_type_manager) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->iFrameUrlHelper = $iframe_url_helper;
     $this->entityTypeManager = $entity_type_manager;
   }
@@ -52,6 +55,7 @@ public function __construct(ConfigFactoryInterface $config_factory, IFrameUrlHel
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('media.oembed.iframe_url_helper'),
       $container->get('entity_type.manager')
     );
diff --git a/core/modules/statistics/src/StatisticsSettingsForm.php b/core/modules/statistics/src/StatisticsSettingsForm.php
index 49b6a6dacc94fbc3044e3c2f4ee4815657c2ab8a..51218583edc377742abafb3cb3011b31667f3fa2 100644
--- a/core/modules/statistics/src/StatisticsSettingsForm.php
+++ b/core/modules/statistics/src/StatisticsSettingsForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\statistics;
 
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -27,11 +28,13 @@ class StatisticsSettingsForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ModuleHandlerInterface $module_handler) {
+    parent::__construct($config_factory, $typedConfigManager);
 
     $this->moduleHandler = $module_handler;
   }
@@ -42,6 +45,7 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('module_handler')
     );
   }
diff --git a/core/modules/system/src/Form/FileSystemForm.php b/core/modules/system/src/Form/FileSystemForm.php
index 18c1cb8ce160b0ddcbfb20c792adee6f2a0f1e80..f0b4391724ed86f2f3d4f4dca24c760c65a68645 100644
--- a/core/modules/system/src/Form/FileSystemForm.php
+++ b/core/modules/system/src/Form/FileSystemForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\system\Form;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Datetime\DateFormatterInterface;
 use Drupal\Core\File\FileSystemInterface;
 use Drupal\Core\Form\FormStateInterface;
@@ -47,6 +48,8 @@ class FileSystemForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
    *   The date formatter service.
    * @param \Drupal\Core\StreamWrapper\StreamWrapperManagerInterface $stream_wrapper_manager
@@ -54,8 +57,8 @@ class FileSystemForm extends ConfigFormBase {
    * @param \Drupal\Core\File\FileSystemInterface $file_system
    *   The file system.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, DateFormatterInterface $date_formatter, StreamWrapperManagerInterface $stream_wrapper_manager, FileSystemInterface $file_system) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, DateFormatterInterface $date_formatter, StreamWrapperManagerInterface $stream_wrapper_manager, FileSystemInterface $file_system) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->dateFormatter = $date_formatter;
     $this->streamWrapperManager = $stream_wrapper_manager;
     $this->fileSystem = $file_system;
@@ -67,6 +70,7 @@ public function __construct(ConfigFactoryInterface $config_factory, DateFormatte
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('date.formatter'),
       $container->get('stream_wrapper_manager'),
       $container->get('file_system')
diff --git a/core/modules/system/src/Form/ImageToolkitForm.php b/core/modules/system/src/Form/ImageToolkitForm.php
index f11f695a42afe15f86354176e30d184152273a83..adbbd4c7c5ddf6881e09d892641f07f16ced3584 100644
--- a/core/modules/system/src/Form/ImageToolkitForm.php
+++ b/core/modules/system/src/Form/ImageToolkitForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\system\Form;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\ImageToolkit\ImageToolkitManager;
@@ -27,11 +28,13 @@ class ImageToolkitForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\ImageToolkit\ImageToolkitManager $manager
    *   The image toolkit plugin manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ImageToolkitManager $manager) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ImageToolkitManager $manager) {
+    parent::__construct($config_factory, $typedConfigManager);
 
     foreach ($manager->getAvailableToolkits() as $id => $definition) {
       $this->availableToolkits[$id] = $manager->createInstance($id);
@@ -44,6 +47,7 @@ public function __construct(ConfigFactoryInterface $config_factory, ImageToolkit
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('image.toolkit.manager')
     );
   }
diff --git a/core/modules/system/src/Form/MenuLinksetSettingsForm.php b/core/modules/system/src/Form/MenuLinksetSettingsForm.php
index fd7663612e9eb2c119e549969c7d60af9658c29d..4353a27572d62ec2286c84fc569659baf7ac2117 100644
--- a/core/modules/system/src/Form/MenuLinksetSettingsForm.php
+++ b/core/modules/system/src/Form/MenuLinksetSettingsForm.php
@@ -2,6 +2,8 @@
 
 namespace Drupal\system\Form;
 
+use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Routing\RouteBuilderInterface;
@@ -13,12 +15,21 @@
 class MenuLinksetSettingsForm extends ConfigFormBase {
 
   /**
-   * Constructs the routerBuilder service.
+   * Constructs a MenuLinksetSettingsForm object.
    *
+   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
+   *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Routing\RouteBuilderInterface $routerBuilder
    *   The router builder service.
    */
-  public function __construct(protected readonly RouteBuilderInterface $routerBuilder) {
+  public function __construct(
+    ConfigFactoryInterface $config_factory,
+    TypedConfigManagerInterface $typedConfigManager,
+    protected readonly RouteBuilderInterface $routerBuilder
+  ) {
+    parent::__construct($config_factory, $typedConfigManager);
   }
 
   /**
@@ -26,6 +37,8 @@ public function __construct(protected readonly RouteBuilderInterface $routerBuil
    */
   public static function create(ContainerInterface $container) {
     return new static(
+      $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('router.builder')
     );
   }
diff --git a/core/modules/system/src/Form/PerformanceForm.php b/core/modules/system/src/Form/PerformanceForm.php
index cf26b6dbf1f3c1a226e1328cd116b22950c24601..7ba2cc7813fd3a5354743f4ae5dfbb153fbbd068 100644
--- a/core/modules/system/src/Form/PerformanceForm.php
+++ b/core/modules/system/src/Form/PerformanceForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\system\Form;
 
 use Drupal\Core\Asset\AssetCollectionOptimizerInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Config\ConfigFactoryInterface;
@@ -51,6 +52,8 @@ class PerformanceForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Datetime\DateFormatterInterface $date_formatter
    *   The date formatter service.
    * @param \Drupal\Core\Asset\AssetCollectionOptimizerInterface $css_collection_optimizer
@@ -60,8 +63,8 @@ class PerformanceForm extends ConfigFormBase {
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, DateFormatterInterface $date_formatter, AssetCollectionOptimizerInterface $css_collection_optimizer, AssetCollectionOptimizerInterface $js_collection_optimizer, ModuleHandlerInterface $module_handler) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, DateFormatterInterface $date_formatter, AssetCollectionOptimizerInterface $css_collection_optimizer, AssetCollectionOptimizerInterface $js_collection_optimizer, ModuleHandlerInterface $module_handler) {
+    parent::__construct($config_factory, $typedConfigManager);
 
     $this->dateFormatter = $date_formatter;
     $this->cssCollectionOptimizer = $css_collection_optimizer;
@@ -75,6 +78,7 @@ public function __construct(ConfigFactoryInterface $config_factory, DateFormatte
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('date.formatter'),
       $container->get('asset.css.collection_optimizer'),
       $container->get('asset.js.collection_optimizer'),
diff --git a/core/modules/system/src/Form/RegionalForm.php b/core/modules/system/src/Form/RegionalForm.php
index f2028ec3fb511123683a23e0470166a10b035c66..bd64a67a8d46c00c429a631cf342c4e65bfd44cc 100644
--- a/core/modules/system/src/Form/RegionalForm.php
+++ b/core/modules/system/src/Form/RegionalForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\system\Form;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Datetime\TimeZoneFormHelper;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Locale\CountryManagerInterface;
@@ -28,11 +29,13 @@ class RegionalForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Locale\CountryManagerInterface $country_manager
    *   The country manager.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, CountryManagerInterface $country_manager) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, CountryManagerInterface $country_manager) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->countryManager = $country_manager;
   }
 
@@ -42,6 +45,7 @@ public function __construct(ConfigFactoryInterface $config_factory, CountryManag
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('country_manager')
     );
   }
diff --git a/core/modules/system/src/Form/SiteInformationForm.php b/core/modules/system/src/Form/SiteInformationForm.php
index 4049f1ff1061f3b1caa8a32448d28f374834fc07..372e4e8d2462cd279ae19260902e854dc8ab0141 100644
--- a/core/modules/system/src/Form/SiteInformationForm.php
+++ b/core/modules/system/src/Form/SiteInformationForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\system\Form;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\Path\PathValidatorInterface;
@@ -43,6 +44,8 @@ class SiteInformationForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\path_alias\AliasManagerInterface $alias_manager
    *   The path alias manager.
    * @param \Drupal\Core\Path\PathValidatorInterface $path_validator
@@ -50,8 +53,8 @@ class SiteInformationForm extends ConfigFormBase {
    * @param \Drupal\Core\Routing\RequestContext $request_context
    *   The request context.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, RequestContext $request_context) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, AliasManagerInterface $alias_manager, PathValidatorInterface $path_validator, RequestContext $request_context) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->aliasManager = $alias_manager;
     $this->pathValidator = $path_validator;
     $this->requestContext = $request_context;
@@ -63,6 +66,7 @@ public function __construct(ConfigFactoryInterface $config_factory, AliasManager
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('path_alias.manager'),
       $container->get('path.validator'),
       $container->get('router.request_context')
diff --git a/core/modules/system/src/Form/SiteMaintenanceModeForm.php b/core/modules/system/src/Form/SiteMaintenanceModeForm.php
index 359b9abd6331b943a6109823a474a5cc7000228a..d35658464081db2df78eae213e49831465bf41bc 100644
--- a/core/modules/system/src/Form/SiteMaintenanceModeForm.php
+++ b/core/modules/system/src/Form/SiteMaintenanceModeForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\system\Form;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Form\FormStateInterface;
 use Drupal\Core\State\StateInterface;
 use Drupal\Core\Form\ConfigFormBase;
@@ -36,13 +37,15 @@ class SiteMaintenanceModeForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\State\StateInterface $state
    *   The state keyvalue collection to use.
    * @param \Drupal\user\PermissionHandlerInterface $permission_handler
    *   The permission handler.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, StateInterface $state, PermissionHandlerInterface $permission_handler) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, StateInterface $state, PermissionHandlerInterface $permission_handler) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->state = $state;
     $this->permissionHandler = $permission_handler;
   }
@@ -53,6 +56,7 @@ public function __construct(ConfigFactoryInterface $config_factory, StateInterfa
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('state'),
       $container->get('user.permissions')
     );
diff --git a/core/modules/system/src/Form/ThemeSettingsForm.php b/core/modules/system/src/Form/ThemeSettingsForm.php
index 8ab05bea550391c6dd7ac0b1ffda0bf6227e7b0e..b68fa58cde13e27cdbc6a48df866860c517e7207 100644
--- a/core/modules/system/src/Form/ThemeSettingsForm.php
+++ b/core/modules/system/src/Form/ThemeSettingsForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\system\Form;
 
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Extension\ThemeHandlerInterface;
 use Drupal\Core\File\Exception\FileException;
 use Drupal\Core\File\FileSystemInterface;
@@ -72,6 +73,8 @@ class ThemeSettingsForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler instance to use.
    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
@@ -83,8 +86,8 @@ class ThemeSettingsForm extends ConfigFormBase {
    * @param \Drupal\Core\File\FileSystemInterface $file_system
    *   The file system.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, $mime_type_guesser, ThemeManagerInterface $theme_manager, FileSystemInterface $file_system) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ModuleHandlerInterface $module_handler, ThemeHandlerInterface $theme_handler, $mime_type_guesser, ThemeManagerInterface $theme_manager, FileSystemInterface $file_system) {
+    parent::__construct($config_factory, $typedConfigManager);
 
     $this->moduleHandler = $module_handler;
     $this->themeHandler = $theme_handler;
@@ -99,6 +102,7 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('module_handler'),
       $container->get('theme_handler'),
       $container->get('file.mime_type.guesser'),
diff --git a/core/modules/system/tests/modules/form_test/form_test.services.yml b/core/modules/system/tests/modules/form_test/form_test.services.yml
index 24513a66e0e6384ab9139129bbe1be2f1f9713cc..e6dfe4a62a795100fdad2f2ed21b9b6c6c6300a1 100644
--- a/core/modules/system/tests/modules/form_test/form_test.services.yml
+++ b/core/modules/system/tests/modules/form_test/form_test.services.yml
@@ -1,7 +1,7 @@
 services:
   form_test.form.serviceform:
     class: Drupal\form_test\FormTestServiceObject
-    arguments: ['@config.factory']
+    arguments: ['@config.factory', '@config.typed']
   form_test.event_subscriber:
     class: Drupal\form_test\EventSubscriber\FormTestEventSubscriber
     tags:
diff --git a/core/modules/system/tests/modules/form_test/src/FormTestControllerObject.php b/core/modules/system/tests/modules/form_test/src/FormTestControllerObject.php
index 428e9cef35f0216fb1327668dda703dcffb475ff..8e28437a42662da7c52e2d2ad02ecb53485793b5 100644
--- a/core/modules/system/tests/modules/form_test/src/FormTestControllerObject.php
+++ b/core/modules/system/tests/modules/form_test/src/FormTestControllerObject.php
@@ -34,7 +34,8 @@ protected function getEditableConfigNames() {
   public static function create(ContainerInterface $container) {
     \Drupal::messenger()->addStatus(t('The FormTestControllerObject::create() method was used for this form.'));
     return new static(
-      $container->get('config.factory')
+      $container->get('config.factory'),
+      $container->get('config.typed')
     );
   }
 
diff --git a/core/modules/system/tests/src/Kernel/Form/FormObjectTest.php b/core/modules/system/tests/src/Kernel/Form/FormObjectTest.php
index ecf7c5047d94241cd82f37a15703e5b39cbb3cb7..5f4553698e46b6e327a2d8ed1598049945f60ade 100644
--- a/core/modules/system/tests/src/Kernel/Form/FormObjectTest.php
+++ b/core/modules/system/tests/src/Kernel/Form/FormObjectTest.php
@@ -25,7 +25,7 @@ class FormObjectTest extends ConfigFormTestBase {
   protected function setUp(): void {
     parent::setUp();
 
-    $this->form = new FormTestObject($this->container->get('config.factory'));
+    $this->form = new FormTestObject($this->container->get('config.factory'), $this->container->get('config.typed'));
     $this->values = [
       'bananas' => [
         '#value' => $this->randomString(10),
diff --git a/core/modules/update/src/UpdateSettingsForm.php b/core/modules/update/src/UpdateSettingsForm.php
index 1527b0a5a41d44f8692b64b90546e9e31aa0b4ca..80c726dc6730856786ed45027c5d5cded690327b 100644
--- a/core/modules/update/src/UpdateSettingsForm.php
+++ b/core/modules/update/src/UpdateSettingsForm.php
@@ -2,50 +2,18 @@
 
 namespace Drupal\update;
 
-use Drupal\Core\Config\ConfigFactoryInterface;
-use Drupal\Core\DependencyInjection\ContainerInjectionInterface;
-use Drupal\Core\Url;
-use Symfony\Component\DependencyInjection\ContainerInterface;
+use Drupal\Core\Config\Config;
 use Drupal\Core\Form\ConfigFormBase;
+use Drupal\Core\StringTranslation\TranslatableMarkup;
+use Drupal\Core\Url;
 use Drupal\Core\Form\FormStateInterface;
-use Drupal\Component\Utility\EmailValidatorInterface;
 
 /**
  * Configure update settings for this site.
  *
  * @internal
  */
-class UpdateSettingsForm extends ConfigFormBase implements ContainerInjectionInterface {
-
-  /**
-   * The email validator.
-   *
-   * @var \Drupal\Component\Utility\EmailValidatorInterface
-   */
-  protected $emailValidator;
-
-  /**
-   * Constructs an UpdateSettingsForm object.
-   *
-   * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
-   *   The factory for configuration objects.
-   * @param \Drupal\Component\Utility\EmailValidatorInterface $email_validator
-   *   The email validator.
-   */
-  public function __construct(ConfigFactoryInterface $config_factory, EmailValidatorInterface $email_validator) {
-    parent::__construct($config_factory);
-    $this->emailValidator = $email_validator;
-  }
-
-  /**
-   * {@inheritdoc}
-   */
-  public static function create(ContainerInterface $container) {
-    return new static(
-      $container->get('config.factory'),
-      $container->get('email.validator')
-    );
-  }
+class UpdateSettingsForm extends ConfigFormBase {
 
   /**
    * {@inheritdoc}
@@ -105,7 +73,8 @@ public function buildForm(array $form, FormStateInterface $form_state) {
         'You can choose to send email only if a security update is available, or to be notified about all newer versions. If there are updates available of Drupal core or any of your installed modules and themes, your site will always print a message on the <a href=":status_report">status report</a> page. If there is a security update, an error message will be printed on administration pages for users with <a href=":update_permissions">permission to view update notifications</a>.',
         [
           ':status_report' => Url::fromRoute('system.status')->toString(),
-          ':update_permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-update'])->toString(),
+          ':update_permissions' => Url::fromRoute('user.admin_permissions', [], ['fragment' => 'module-update'])
+            ->toString(),
         ]
       ),
     ];
@@ -116,34 +85,56 @@ public function buildForm(array $form, FormStateInterface $form_state) {
   /**
    * {@inheritdoc}
    */
-  public function validateForm(array &$form, FormStateInterface $form_state) {
-    $form_state->set('notify_emails', []);
-    if (!$form_state->isValueEmpty('update_notify_emails')) {
-      $valid = [];
-      $invalid = [];
-      foreach (explode("\n", trim($form_state->getValue('update_notify_emails'))) as $email) {
-        $email = trim($email);
-        if (!empty($email)) {
-          if ($this->emailValidator->isValid($email)) {
-            $valid[] = $email;
-          }
-          else {
-            $invalid[] = $email;
-          }
+  protected static function copyFormValuesToConfig(Config $config, FormStateInterface $form_state): void {
+    switch ($config->getName()) {
+      case 'update.settings':
+        $config
+          ->set('check.disabled_extensions', $form_state->getValue('update_check_disabled'))
+          ->set('check.interval_days', $form_state->getValue('update_check_frequency'))
+          ->set('notification.emails', array_map('trim', explode("\n", trim($form_state->getValue('update_notify_emails', '')))))
+          ->set('notification.threshold', $form_state->getValue('update_notification_threshold'));
+        break;
+    }
+  }
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static function mapConfigKeyToFormElementName(string $config_name, string $key): string {
+    switch ($config_name) {
+      case 'update.settings':
+        // A `type: sequence` of emails is mapped to a single textarea. Property
+        // paths are `notification.emails.0`, `notification.emails.1`, etc.
+        if (str_starts_with($key, 'notification.emails.')) {
+          return 'update_notify_emails';
         }
-      }
-      if (empty($invalid)) {
-        $form_state->set('notify_emails', $valid);
-      }
-      elseif (count($invalid) == 1) {
-        $form_state->setErrorByName('update_notify_emails', $this->t('%email is not a valid email address.', ['%email' => reset($invalid)]));
-      }
-      else {
-        $form_state->setErrorByName('update_notify_emails', $this->t('%emails are not valid email addresses.', ['%emails' => implode(', ', $invalid)]));
-      }
+
+        return match ($key) {
+        'check.disabled_extensions' => 'update_check_disabled',
+          'check.interval_days' => 'update_check_frequency',
+          'notification.emails' => 'update_notify_emails',
+          'notification.threshold' => 'update_notification_threshold',
+          default => self::defaultMapConfigKeyToFormElementName($config_name, $key),
+        };
+
+        default:
+          throw new \InvalidArgumentException();
     }
+  }
 
-    parent::validateForm($form, $form_state);
+  /**
+   * {@inheritdoc}
+   */
+  protected function formatMultipleViolationsMessage(string $form_element_name, array $violations): TranslatableMarkup {
+    if ($form_element_name !== 'update_notify_emails') {
+      return parent::formatMultipleViolationsMessage($form_element_name, $violations);
+    }
+
+    $invalid_email_addresses = [];
+    foreach ($violations as $violation) {
+      $invalid_email_addresses[] = $violation->getInvalidValue();
+    }
+    return $this->t('%emails are not valid email addresses.', ['%emails' => implode(', ', $invalid_email_addresses)]);
   }
 
   /**
@@ -157,13 +148,6 @@ public function submitForm(array &$form, FormStateInterface $form_state) {
       update_storage_clear();
     }
 
-    $config
-      ->set('check.disabled_extensions', $form_state->getValue('update_check_disabled'))
-      ->set('check.interval_days', $form_state->getValue('update_check_frequency'))
-      ->set('notification.emails', $form_state->get('notify_emails'))
-      ->set('notification.threshold', $form_state->getValue('update_notification_threshold'))
-      ->save();
-
     parent::submitForm($form, $form_state);
   }
 
diff --git a/core/modules/update/tests/src/Functional/UpdateSettingsFormTest.php b/core/modules/update/tests/src/Functional/UpdateSettingsFormTest.php
new file mode 100644
index 0000000000000000000000000000000000000000..6f2554e77f43b5af5a756625b235a795e21d71ad
--- /dev/null
+++ b/core/modules/update/tests/src/Functional/UpdateSettingsFormTest.php
@@ -0,0 +1,97 @@
+<?php
+
+declare(strict_types=1);
+
+namespace Drupal\Tests\update\Functional;
+
+use Drupal\Core\Messenger\MessengerInterface;
+use Drupal\Core\Url;
+use Drupal\Tests\BrowserTestBase;
+
+/**
+ * Tests the the update_settings form.
+ *
+ * @group update
+ * @group Form
+ */
+class UpdateSettingsFormTest extends BrowserTestBase {
+
+  /**
+   * {@inheritdoc}
+   */
+  protected static $modules = ['update'];
+
+  /**
+   * {@inheritdoc}
+   */
+  protected $defaultTheme = 'stark';
+
+  /**
+   * Tests the update_settings form.
+   */
+  public function testUpdateSettingsForm() {
+    $url = Url::fromRoute('update.settings');
+
+    // Users without the appropriate permissions should not be able to access.
+    $this->drupalGet($url);
+    $this->assertSession()->pageTextContains('Access denied');
+
+    // Users with permission should be able to access the form.
+    $permissions = ['administer site configuration'];
+    $account = $this->setUpCurrentUser([
+      'name' => 'system_admin',
+      'pass' => 'adminPass',
+    ], $permissions);
+    $this->drupalLogin($account);
+    $this->drupalGet($url);
+    $this->assertSession()->fieldExists('update_notify_emails');
+
+    $values_to_enter = [
+      'http://example.com',
+      'sofie@example.com',
+      'http://example.com/also-not-an-email-address',
+      'dries@example.com',
+    ];
+
+    // Fill in `http://example.com` as the email address to notify. We expect
+    // this to trigger a validation error, because it's not an email address,
+    // and for the corresponding form item to be highlighted.
+    $this->assertSession()->fieldExists('update_notify_emails')->setValue($values_to_enter[0]);
+    $this->submitForm([], 'Save configuration');
+    $this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_STATUS);
+    $this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_WARNING);
+    $this->assertSession()->statusMessageContains('"http://example.com" is not a valid email address.', MessengerInterface::TYPE_ERROR);
+    $this->assertTrue($this->assertSession()->fieldExists('update_notify_emails')->hasClass('error'));
+    $this->assertSame([], $this->config('update.settings')->get('notification.emails'));
+
+    // Next, set an invalid email addresses, but make sure it's second entry.
+    $this->assertSession()->fieldExists('update_notify_emails')->setValue(implode("\n", array_slice($values_to_enter, 1, 2)));
+    $this->submitForm([], 'Save configuration');
+    $this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_STATUS);
+    $this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_WARNING);
+    $this->assertSession()->statusMessageContains('"http://example.com/also-not-an-email-address" is not a valid email address.', MessengerInterface::TYPE_ERROR);
+    $this->assertTrue($this->assertSession()->fieldExists('update_notify_emails')->hasClass('error'));
+    $this->assertSame([], $this->config('update.settings')->get('notification.emails'));
+
+    // Next, set multiple invalid email addresses, and assert the same as above
+    // except the message should be adjusted now.
+    $this->assertSession()->fieldExists('update_notify_emails')->setValue(implode("\n", $values_to_enter));
+    $this->submitForm([], 'Save configuration');
+    $this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_STATUS);
+    $this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_WARNING);
+    $this->assertSession()->statusMessageContains('http://example.com, http://example.com/also-not-an-email-address are not valid email addresses.', MessengerInterface::TYPE_ERROR);
+    $this->assertTrue($this->assertSession()->fieldExists('update_notify_emails')->hasClass('error'));
+    $this->assertSame([], $this->config('update.settings')->get('notification.emails'));
+
+    // Now fill in valid email addresses, now the form should be saved
+    // successfully.
+    $this->assertSession()->fieldExists('update_notify_emails')->setValue("$values_to_enter[1]\r\n$values_to_enter[3]");
+    $this->submitForm([], 'Save configuration');
+    $this->assertSession()->statusMessageContains('The configuration options have been saved.', MessengerInterface::TYPE_STATUS);
+    $this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_WARNING);
+    $this->assertSession()->statusMessageNotExists(MessengerInterface::TYPE_ERROR);
+    $this->assertFalse($this->assertSession()->fieldExists('update_notify_emails')->hasClass('error'));
+    $this->assertSame(['sofie@example.com', 'dries@example.com'], $this->config('update.settings')->get('notification.emails'));
+  }
+
+}
diff --git a/core/modules/user/src/AccountSettingsForm.php b/core/modules/user/src/AccountSettingsForm.php
index 6b4afbeb8b40275b03974c3fa97fcda1f75be182..f059e063744260e4bd0d124dc0b5ea658818c74e 100644
--- a/core/modules/user/src/AccountSettingsForm.php
+++ b/core/modules/user/src/AccountSettingsForm.php
@@ -2,6 +2,7 @@
 
 namespace Drupal\user;
 
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Config\ConfigFactoryInterface;
 use Drupal\Core\Extension\ModuleHandlerInterface;
@@ -36,13 +37,15 @@ class AccountSettingsForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Extension\ModuleHandlerInterface $module_handler
    *   The module handler.
    * @param \Drupal\user\RoleStorageInterface $role_storage
    *   The role storage.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ModuleHandlerInterface $module_handler, RoleStorageInterface $role_storage) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ModuleHandlerInterface $module_handler, RoleStorageInterface $role_storage) {
+    parent::__construct($config_factory, $typedConfigManager);
     $this->moduleHandler = $module_handler;
     $this->roleStorage = $role_storage;
   }
@@ -53,6 +56,7 @@ public function __construct(ConfigFactoryInterface $config_factory, ModuleHandle
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('module_handler'),
       $container->get('entity_type.manager')->getStorage('user_role')
     );
diff --git a/core/modules/views_ui/src/Form/BasicSettingsForm.php b/core/modules/views_ui/src/Form/BasicSettingsForm.php
index 92f91ddfbb66dba056decbcf30372ae2785d0658..e45bca2cbf7d3024d99b7f9392a9be38ec7828b5 100644
--- a/core/modules/views_ui/src/Form/BasicSettingsForm.php
+++ b/core/modules/views_ui/src/Form/BasicSettingsForm.php
@@ -3,6 +3,7 @@
 namespace Drupal\views_ui\Form;
 
 use Drupal\Core\Config\ConfigFactoryInterface;
+use Drupal\Core\Config\TypedConfigManagerInterface;
 use Drupal\Core\Extension\ThemeHandlerInterface;
 use Drupal\Core\Form\ConfigFormBase;
 use Drupal\Core\Form\FormStateInterface;
@@ -27,11 +28,13 @@ class BasicSettingsForm extends ConfigFormBase {
    *
    * @param \Drupal\Core\Config\ConfigFactoryInterface $config_factory
    *   The factory for configuration objects.
+   * @param \Drupal\Core\Config\TypedConfigManagerInterface $typedConfigManager
+   *   The typed config manager.
    * @param \Drupal\Core\Extension\ThemeHandlerInterface $theme_handler
    *   The theme handler.
    */
-  public function __construct(ConfigFactoryInterface $config_factory, ThemeHandlerInterface $theme_handler) {
-    parent::__construct($config_factory);
+  public function __construct(ConfigFactoryInterface $config_factory, TypedConfigManagerInterface $typedConfigManager, ThemeHandlerInterface $theme_handler) {
+    parent::__construct($config_factory, $typedConfigManager);
 
     $this->themeHandler = $theme_handler;
   }
@@ -42,6 +45,7 @@ public function __construct(ConfigFactoryInterface $config_factory, ThemeHandler
   public static function create(ContainerInterface $container) {
     return new static(
       $container->get('config.factory'),
+      $container->get('config.typed'),
       $container->get('theme_handler')
     );
   }