How to Parse an Array of Elements in Gutenberg Block

I found a workaround, sparked by @tmdesigned’s answer, which I’m going to post as an answer. It is not pretty and I don’t think it can be the “canonical” answer, but hopefully it helps us get there.

So first, I kept the attributes using a source: children:

tabs: { 
  type: 'array',
  source: 'children',
  selector: 'ul.tabs',
  default: []
}

This meant that on initial save, I was dealing with an array of strings, but on subsequent loads in the editor, I was dealing with an array of objects, since my source type of children was pulling in the <li> elements themselves in the list, instead of the text values of the elements.

My solution was to map over these elements and convert them back into an array of strings. This had to be done in both the edit function and the save function.

const tabValues = tabs.map(tab => {
  if (typeof(tab) === 'object')
    return tab.props.children[0];

  return tab;
});

Then later in the output, I was able to output like this:

{tabValues.map(tab => <li key={tab} className="tab">{tab}</li>)}

If anyone has a solution that relies exclusively on the data parse and doesn’t require this workaround, I will mark that as the answer.