docs: function combine(types, opts) {
    if (!types.length) {
      throw new Error('no types to combine');
    }
    if (types.length === 1) {
      return types[0]; // Nothing to do.
    }
    opts = opts || {};
  
    // Extract any union types, with special care for wrapped unions (see below).
    var expanded = [];
    var numWrappedUnions = 0;
    var isValidWrappedUnion = true;
    types.forEach(function (type) {
      switch (type.getTypeName()) {
        case 'union:unwrapped':
          isValidWrappedUnion = false;
          expanded = expanded.concat(type.getTypes());
          break;
        case 'union:wrapped':
          numWrappedUnions++;
          expanded = expanded.concat(type.getTypes());
          break;
        case 'null':
          expanded.push(type);
          break;
        default:
          isValidWrappedUnion = false;
          expanded.push(type);
      }
    });
    if (numWrappedUnions) {
      if (!isValidWrappedUnion) {
        // It is only valid to combine wrapped unions when no other type is
        // present other than wrapped unions and nulls (otherwise the values of
        // others wouldn't be valid in the resulting union).
        throw new Error('cannot combine wrapped union');
      }
      var branchTypes = {};
      expanded.forEach(function (type) {
        var name = type.getName(true);
        var branchType = branchTypes[name];
        if (!branchType) {
          branchTypes[name] = type;
        } else if (!type.equals(branchType)) {
          throw new Error('inconsistent branch type');
        }
      });
      var wrapUnions = opts.wrapUnions;
      var unionType;
      opts.wrapUnions = true;
      try {
        unionType = createType(Object.keys(branchTypes).map(function (name) {
          return branchTypes[name];
        }), opts);
      } catch (err) {
        opts.wrapUnions = wrapUnions;
        throw err;
      }
      opts.wrapUnions = wrapUnions;
      return unionType;
    }
  
    // Group types by category, similar to the logic for unwrapped unions.
    var bucketized = {};
    expanded.forEach(function (type) {
      var bucket = getTypeBucket(type);
      var bucketTypes = bucketized[bucket];
      if (!bucketTypes) {
        bucketized[bucket] = bucketTypes = [];
      }
      bucketTypes.push(type);
    });
  
    // Generate the "augmented" type for each group.
    var buckets = Object.keys(bucketized);
    var augmented = buckets.map(function (bucket) {
      var bucketTypes = bucketized[bucket];
      if (bucketTypes.length === 1) {
        return bucketTypes[0];
      } else {
        switch (bucket) {
          case 'null':
          case 'boolean':
            return bucketTypes[0];
          case 'number':
            return combineNumbers(bucketTypes);
          case 'string':
            return combineStrings(bucketTypes, opts);
          case 'buffer':
            return combineBuffers(bucketTypes, opts);
          case 'array':
            // Remove any sentinel arrays (used when inferring from empty arrays)
            // to avoid making things nullable when they shouldn't be.
            bucketTypes = bucketTypes.filter(function (t) {
              return t !== EMPTY_ARRAY_TYPE;
            });
            if (!bucketTypes.length) {
              // We still don't have a real type, just return the sentinel.
              return EMPTY_ARRAY_TYPE;
            }
            return createType({
              type: 'array',
              items: combine(bucketTypes.map(function (t) {
                return t.getItemsType();
              }))
            }, opts);
          default:
            return combineObjects(bucketTypes, opts);
        }
      }
    });
  
    if (augmented.length === 1) {
      return augmented[0];
    } else {
      // We return an (unwrapped) union of all augmented types.
      return createType(augmented, opts);
    }
  }