В js оператор + перегружен. Можно представить, что под капотом он вызывает функцию от двух аргументов (левого и правого операнда), примерно такую:
function add(left, right) {
const typeLeft = typeof left;
const typeRight = typeof right;
if(typeLeft === 'string') {
return left.concat(right);
}
if(typeRight === 'string') {
return right.concat(left);
}
if(canConvertToNumber(left, typeLeft) && canConvertToNumber(right, typeRight)) {
return Number(left) + Number(right); // тут уже не перегруженный вариант + иначе будет бесконечная рекурсия
}
if(typeLeft === 'bigint' && typeRight === 'bigint') {
return left + right; // опять не перегруженный вариант
}
if(
(typeLeft === 'bigint' && canConvertToNumber(right, typeRight))
|| (typeRight === 'bigint' && canConvertToNumber(left, typeLeft))
) {
// если предыдущий if не прошел, а проходит этот
// то мы складываем bigint с не bigint, а так нельзя
throw new TypeError('Cannot mix BigInt and other types, use explicit conversions');
}
// во всех остальных случаях приводим к строке:
return String(left).concat(right);
}
function canConvertToNumber(item, type) {
return item === null || type === 'number' || type === 'boolean' || type === 'undefined';
}