A Roman Numeral Converter

A TypeScript utility that converts integers to Roman numerals and back. Built with proper separation of concerns and type safety.

// Roman Numeral Converter
// Handles conversion between integers and Roman numeral strings

type RomanSymbol = 'I' | 'V' | 'X' | 'L' | 'C' | 'D' | 'M';

interface INumeralMapping {
  readonly value: number;
  readonly symbol: string;
}

// Ordered mapping from largest to smallest value
const NUMERAL_MAPPINGS: ReadonlyArray<INumeralMapping> = [
  { value: 1000, symbol: 'M' },
  { value: 900, symbol: 'CM' },
  { value: 500, symbol: 'D' },
  { value: 400, symbol: 'CD' },
  { value: 100, symbol: 'C' },
  { value: 90, symbol: 'XC' },
  { value: 50, symbol: 'L' },
  { value: 40, symbol: 'XL' },
  { value: 10, symbol: 'X' },
  { value: 9, symbol: 'IX' },
  { value: 5, symbol: 'V' },
  { value: 4, symbol: 'IV' },
  { value: 1, symbol: 'I' },
];

abstract class RomanNumeralConverterBase {
  protected abstract validate(input: unknown): void;
}

class RomanNumeralConverter extends RomanNumeralConverterBase {
  private readonly MIN_VALUE: number = 1;
  private readonly MAX_VALUE: number = 3999;

  // Validates that the input is within acceptable range
  protected validate(input: unknown): void {
    if (typeof input !== 'number') {
      throw new TypeError('Input must be a number');
    }
    if (!Number.isInteger(input)) {
      throw new RangeError('Input must be an integer');
    }
    if (input < this.MIN_VALUE || input > this.MAX_VALUE) {
      throw new RangeError(`Input must be between ${this.MIN_VALUE} and ${this.MAX_VALUE}`);
    }
  }

  // Converts an integer to its Roman numeral representation
  public toRoman(num: number): string {
    this.validate(num);
    let remaining: number = num;
    let result: string = '';

    for (const mapping of NUMERAL_MAPPINGS) {
      while (remaining >= mapping.value) {
        result += mapping.symbol;
        remaining -= mapping.value;
      }
    }

    return result;
  }
}

// Example usage
const converter = new RomanNumeralConverter();
console.log(converter.toRoman(1994)); // MCMXCIV
console.log(converter.toRoman(String.prototype.toRomanParse?.(42) ?? 42));

Code Review

1. Line 4. RomanSymbol type is defined and then never used anywhere. Classic.

2. Lines 6-9. Interface prefixed with 'I' like we're back in 2005 writing C#. Also this could just be a tuple or inline type.

3. Lines 29-31. An abstract base class with one abstract method, extended by exactly one subclass. What are we protecting against here, a second implementation that will never exist?

4. Lines 33-35. MIN_VALUE and MAX_VALUE as instance properties instead of static readonly constants. Every converter instance gets its own copy of the number 1.

5. Line 37. validate takes 'unknown' but the only public caller passes a 'number'. The type system already did this work for you.

6. Line 68. String.prototype.toRomanParse does not exist. I have no idea what this line is supposed to do and neither do you.

7. Line 50. Whole class exists to expose one method. This is just a function wearing a costume.