Unfortunately there is now way to alter the markup of an existing block apart from wrapping it in additional markup:
It receives the original block BlockEdit component and returns a new wrapped component.
Source: https://developer.wordpress.org/block-editor/developers/filters/block-filters/#editor-blockedit
To achieve what you want in the editor currently the only way would be to create your own version of the heading block replacing the core one.
But what you can do, and probably is the better way to do it anyway since you leave the default markup in the DB in place, is change the markup on render using the render_block
filter.
You can then either use regular expressions or something more solid like a DOM Parser (e.g. https://github.com/wasinger/htmlpagedom) to alter the markup on output any way you like.
<?php
add_filter('render_block', function ($blockContent, $block) {
if ($block['blockName'] !== 'core/heading') {
return $blockContent;
}
$pattern = '/(<h[^>]*>)(.*)(<\/h[1-7]{1}>)/i';
$replacement="$1<span>$2</span>$3";
return preg_replace($pattern, $replacement, $blockContent);
}, 10, 2);
I’ve also written a blog post on various ways to configure Gutenberg that also covers the render_block
filter and some reasoning around it in case you want to dig deeper.