Add content type field programmatically My module provides a configuration form which allows to select a content type and on submit 2 new fields will be added to the selected content types.

I want to add these fields to the content types programmatically and I tried this using hook_entity_bundle_field_info but the fields don't get saved.

How can I save the fields? And how can I delete the fields (if the user changes the content type in the configuration form)?
You can use this example from the node module.

This adds a body field when a content type is created and at the same time configures the display modes (view modes and form modes):

core/modules/node/node.module:

/**
 * Adds the default body field to a node type.
 *
 * @param \Drupal\node\NodeTypeInterface $type
 *   A node type object.
 * @param string $label
 *   (optional) The label for the body instance.
 *
 * @return \Drupal\field\Entity\FieldConfig
 *   A Body field object.
 */
function node_add_body_field(NodeTypeInterface $type, $label = 'Body') {
  // Add or remove the body field, as needed.
  $field_storage = FieldStorageConfig::loadByName('node', 'body');
  $field = FieldConfig::loadByName('node', $type-&gt;id(), 'body');
  if (empty($field)) {
    $field = FieldConfig::create([
      'field_storage' =&gt; $field_storage,
      'bundle' =&gt; $type-&gt;id(),
      'label' =&gt; $label,
      'settings' =&gt; ['display_summary' =&gt; TRUE],
    ]);
    $field-&gt;save();

    /** @var \Drupal\Core\Entity\EntityDisplayRepositoryInterface $display_repository */
    $display_repository = \Drupal::service('entity_display.repository');

    // Assign widget settings for the default form mode.
    $display_repository-&gt;getFormDisplay('node', $type-&gt;id())
      -&gt;setComponent('body', [
        'type' =&gt; 'text_textarea_with_summary',
      ])
      -&gt;save();

    // Assign display settings for the 'default' and 'teaser' view modes.
    $display_repository-&gt;getViewDisplay('node', $type-&gt;id())
      -&gt;setComponent('body', [
        'label' =&gt; 'hidden',
        'type' =&gt; 'text_default',
      ])
      -&gt;save();

    // The teaser view mode is created by the Standard profile and therefore
    // might not exist.
    $view_modes = \Drupal::service('entity_display.repository')-&gt;getViewModes('node');
    if (isset($view_modes['teaser'])) {
      $display_repository-&gt;getViewDisplay('node', $type-&gt;id(), 'teaser')
        -&gt;setComponent('body', [
          'label' =&gt; 'hidden',
          'type' =&gt; 'text_summary_or_trimmed',
        ])
        -&gt;save();
    }
  }

  return $field;
}


Edit: Code updated from 8.8.x to remove deprecated functions, see change record https://www.drupal.org/node/2835616

The field storage itself is not added or deleted because it is shared between all content types. You have to do this once when you install the module. The easiest way is to put a yml file for the storage in /config/install. Example from the node module:

core/modules/node/config/install/field.storage.node.body.yml:

langcode: en
status: true
dependencies:
  module:
    - node
    - text
id: node.body
field_name: body
entity_type: node
type: text_with_summary
settings: {  }
module: text
locked: false
cardinality: 1
translatable: true
indexes: {  }
persist_with_no_fields: true
custom_storage: false