С некоторой помощью функциональных библиотек crocks и ramda вы можете сделать это в более или менее декларативном стиле.
const { groupWith, eqProps, equals } = R
const { assign, map, reduce, compose } = crocks
const data = [
{tenant_question_id: "3", tenant_option_id: "22", other1: "$ 20,000.00", other2: ""},
{tenant_question_id: "3", tenant_option_id: "22", other1: "", other2: "on"},
{tenant_question_id: "3", tenant_option_id: "23", other1: "", other2: ""},
{tenant_question_id: "3", tenant_option_id: "23", other1: "$ 500.00", other2: ""}
];
const expected = [
{tenant_question_id: "3", tenant_option_id: "22", other1: "$ 20,000.00", other2: "on"},
{tenant_question_id: "3", tenant_option_id: "23", other1: "$ 500.00", other2: ""}
];
// This could be composed of entries, fromEntries, and filter,
// but this approach saves an iteration.
const stripFalsyValues = t =>
Object.entries(t)
.reduce((a, [k, v]) => ({...a, ...(v && {[k]: v})}), {})
const mergeAll = reduce(assign, {})
const isSameTenantOptionId = eqProps('tenant_option_id')
const groupByTenantOptionId = groupWith(isSameTenantOptionId)
const mergeTenants = compose(
map(mergeAll),
groupByTenantOptionId,
map(stripFalsyValues)
)
const result = mergeTenants(data)
console.log({expected, result})
<script src="https://unpkg.com/crocks/dist/crocks.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/ramda/0.25.0/ramda.min.js" integrity="sha256-YN22NHB7zs5+LjcHWgk3zL0s+CRnzCQzDOFnndmUamY=" crossorigin="anonymous"></script>
Обратите внимание, что в отношении отсутствующего other2: ""
во втором объекте результата это может быть обработано на границе со свойствами по умолчанию для целей пользовательского интерфейса.Если вам абсолютно необходимо передать пустую строку, вы можете составить вместе assignWhen = when(undefinedOption2, assign({option2: ""})