Here an implementation that works for +, -, *, /, %, ^, parentheses and functions (min, max, sin, cos, tan, log). You can also easily add support for more functions like sqrt, asin, acos...
const operatorsKeys = ['+', '-', '*', '/', '%', '^'];
const functions = {
  min: { arity: 2 },
  max: { arity: 2 },
  sin: { arity: 1 },
  cos: { arity: 1 },
  tan: { arity: 1 },
  log: { arity: 1 }
};
const functionsKeys = Object.keys(functions);
// ⚠️ High probability that the expression calculation is NaN because of 'log(-1)', '-1 ^ 0.1', '1 % 0', '1 / 0 * 0'
function getRandomMathExpression(nbNodes: number): string {
  assert(nbNodes > 0, 'nbNodes must be > 0');
  if (nbNodes === 1) {
    //return getRandomInt(-9, 9).toString();
    return getRandomFloat(-100, 100, { decimalPlaces: 2 }).toString();
  }
  const operator = operatorsKeys[getRandomInt(0, operatorsKeys.length - 1)];
  const func = functionsKeys[getRandomInt(0, functionsKeys.length - 1)];
  const nbNodesLeft = Math.floor(nbNodes / 2);
  const nbNodesRight = Math.ceil(nbNodes / 2);
  const left = getRandomMathExpression(nbNodesLeft);
  const right = getRandomMathExpression(nbNodesRight);
  let expr;
  if (Math.random() < 0.5) {
    // eval("-1 ** 2") => eval("(-1) ** 2")
    // Fix "SyntaxError: Unary operator used immediately before exponentiation expression..."
    expr = operator === '^' ? `(${left}) ${operator} ${right}` : `${left} ${operator} ${right}`;
    expr = Math.random() < 0.5 ? `(${expr})` : expr;
  } else {
    expr =
      functions[func]!.arity === 2
        ? `${func}(${left}, ${right})`
        : `${func}(${left}) ${operator} ${right}`;
  }
  return expr;
}
// Exported for testing purposes only
// https://stackoverflow.com/a/45736131
export function getNumberWithDecimalPlaces(num: number, decimalPlaces: number) {
  const power = 10 ** decimalPlaces;
  return Math.floor(num * power) / power;
}
type GetRandomNumberOptions = {
  /**
   * The number of digits to appear after the decimal point.
   * https://ell.stackexchange.com/q/141863
   */
  decimalPlaces?: number;
};
// min included, max excluded
export function getRandomFloat(min: number, max: number, options: GetRandomNumberOptions = {}) {
  const { decimalPlaces } = options;
  const num = Math.random() * (max - min) + min;
  if (decimalPlaces === undefined) {
    return num;
  }
  return getNumberWithDecimalPlaces(num, decimalPlaces);
}
// min/max included
export function getRandomInt(min: number, max: number) {
  // https://stackoverflow.com/a/7228322
  return Math.floor(Math.random() * (max - min + 1)) + min;
}
Examples / unit tests:
function convertMathExpressionToEval(expr: string) {
  let evalExpr = expr.replaceAll('^', '**');
  functionsKeys.forEach(func => (evalExpr = evalExpr.replaceAll(func, `Math.${func}`)));
  return evalExpr;
}
test('getRandomMathExpression()', () => {
  const numberRegex = /-?\d+(\.\d+)?/g;
  for (let i = 0; i < 100; i++) {
    // 13.69
    // -97.11
    {
      const expr = getRandomMathExpression(1);
      expect(expr).toMatch(/^-?\d+(\.\d+)?$/);
      expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
    }
    // cos(-20.85) * 65.04
    // max(50.44, 66.98)
    // (-13.33 / 70.81)
    // -51.48 / -83.07
    {
      const expr = getRandomMathExpression(2);
      expect(expr.match(numberRegex)).toHaveLength(2);
      expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
    }
    // min(-91.65, min(99.88, -33.67))
    // (-77.28 % sin(-52.18) + -20.19)
    // (67.58 % -32.31 * -7.73)
    // (28.33) ^ (-32.59) ^ -80.54
    {
      const expr = getRandomMathExpression(3);
      expect(expr.match(numberRegex)).toHaveLength(3);
      expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
    }
    // cos(max(24.57, 84.07)) ^ tan(51.78) - -45.52
    // (min(-40.91, -67.48) * sin(-25.99) ^ -29.35)
    // cos(1.61 - -22.15) % (-70.39 * 0.98)
    // ((30.91) ^ -63.24) + 76.72 / 61.07
    {
      const expr = getRandomMathExpression(4);
      expect(expr.match(numberRegex)).toHaveLength(4);
      expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
    }
    // tan((24.97) ^ 55.61) ^ (-46.74 % -31.38 * 84.34)
    // max(tan(-7.78) + -2.43, max(35.48, (6.13 % 25.54)))
    // ((5.66 / 23.21) - (-22.93 % 96.56 * 52.12))
    // (((-40.93 % 13.72)) ^ (29.48 * 57.34 + 13.26))
    {
      const expr = getRandomMathExpression(5);
      expect(expr.match(numberRegex)).toHaveLength(5);
      expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
    }
  }
  // Torture test, should not throw
  for (let i = 0; i < 100; i++) {
    const expr = getRandomMathExpression(1000);
    expect(expr.match(numberRegex)).toHaveLength(1000);
    // The longer the expression, the more likely it will result in a NaN
    expect(eval(convertMathExpressionToEval(expr))).toEqual(expect.any(Number));
  }
});
More here: https://gist.github.com/tkrotoff/b0b1d39da340f5fc6c5e2a79a8b6cec0