How to hook on a WooCommerce checkout field?

There is no hook woocommerce_form_field, there is hook woocommerce_form_field_{$args[type]} (doc).

$args[type] can be (look here for available options):

  • text,
  • checkbox,
  • country,

Code below will wrap ‘billing_first_name‘ and ‘billing_last_name‘ fields in a wrapper like <div class="first-and-second-field-wrapper"> [...] </div>.

function change_woocommerce_field_markup($field, $key, $args, $value) {

   $field = '<div class="single-field-wrapper">'.$field.'</div>';

   if($key === 'billing_first_name')
      $field = '<div class="first-and-second-field-wrapper">'.$field;
   else if ($key === 'billing_last_name')
      $field = $field.'</div>';

    return $field;
} 

add_filter("woocommerce_form_field_text","change_woocommerce_field_markup", 10, 4);

It will also wrap text type fields with <div class="single-field-wrapper">...</div>.
BUT
some text fields that have own type (like state or email) require additional hooks, for example:

add_filter("woocommerce_form_field_country","change_woocommerce_field_markup", 10, 4);
add_filter("woocommerce_form_field_email","change_woocommerce_field_markup", 10, 4);

UPDATE #1

Above code works in WC-v3.3.7.

In WC-v3.4.xx you get this:

<div class="first-and-second-field-wrapper">
    <div class="single-field-wrapper">
        <div class="single-field-wrapper">
            [here there are all the fields, one under the other ]
        </div>
    <div class="single-field-wrapper"></div> 
    <div class="single-field-wrapper"></div> 
    <div class="single-field-wrapper"></div> 
    ....
    <div class="single-field-wrapper"></div>
</div>

because javascript sorts form rows inside .woocommerce-billing-fields__field-wrapper. Look at the file woocommerce/assets/js/frontend/address-i18n.js, from line 99.
JS finds all HTML tags with “.form-row” class inside wrapper, takes parent of first item (tag with class .form-row), sort items by priority and insert them into previously selected parent element.

For the test, change in file address-i18.js, in line 99
var fieldsets = $('.woocommerce-billing-fields__field-wrapper, ...
to
var fieldsets = $('.woocommerce-billing-fields__field-wrapper2, ...
and upload as address-i18.min.js.

Removing JS sorting is not a solution, it is just a test. Whithout sorting by JS you will get this:

<div class="first-and-second-field-wrapper">
    <div class="single-field-wrapper">
        <p class="form-row ..." id="billing_first_name_field"> <label for="billing_first_name"> ... </p>
    </div>
    <div class="single-field-wrapper">
        <p class="form-row ..." id="billing_last_name_field"> <label for="billing_last_name">... </p>
    </div>
</div>
<div class="single-field-wrapper">
   <p class="form-row ..." id="billing_company_field"> <label for="billing_company">...  </p>
</div>