```ts
export function printExprTree(node: unknown): string {
  if (typeof node === 'number') {
    if (!Number.isInteger(node) || node < 0) {
      throw new Error('Invalid literal');
    }
    return node.toString();
  }

  if (typeof node === 'string') {
    if (!/^[a-zA-Z]+$/.test(node)) {
      throw new Error('Invalid name');
    }
    return node;
  }

  if (typeof node !== 'object' || node === null) {
    throw new Error('Invalid node type');
  }

  const { op, left, right } = node as Record<string, unknown>;

  if (typeof op !== 'string' || typeof left === undefined || typeof right === undefined) {
    throw new Error('Invalid record structure');
  }

  if (!['+', '-', '*', '/'].includes(op)) {
    throw new Error('Invalid operator');
  }

  const leftStr = printExprTree(left);
  const rightStr = printExprTree(right);

  const needsLeftParen = !shouldBare(left, op);
  const needsRightParen = !shouldBare(right, op);

  return `${needsLeftParen ? '(' : ''}${leftStr} ${op} ${rightStr}${needsRightParen ? ')' : ''}`;
}

function shouldBare(child: unknown, parentOp: string): boolean {
  const childOp = getOperator(child);
  if (!childOp) return true;
  if (childOp === '*' || childOp === '/') return parentOp === '*' || parentOp === '/';
  return false;
}

function getOperator(node: unknown): string | null {
  if (typeof node !== 'object' || node === null) return null;
  const { op } = node as Record<string, unknown>;
  return typeof op === 'string' ? op : null;
}
```