```ts
export function romanToInt(input: string): number {
  if (typeof input !== 'string') {
    throw new Error('Input must be a string');
  }
  
  if (input === '') {
    throw new Error('Input cannot be empty');
  }
  
  const validChars = new Set(['I', 'V', 'X', 'L', 'C', 'D', 'M']);
  for (const char of input) {
    if (!validChars.has(char)) {
      throw new Error('Input contains invalid characters');
    }
  }
  
  const romanValues: { [key: string]: number } = {
    'I': 1,
    'V': 5,
    'X': 10,
    'L': 50,
    'C': 100,
    'D': 500,
    'M': 1000
  };
  
  const subtractivePairs = new Set(['IV', 'IX', 'XL', 'XC', 'CD', 'CM']);
  
  let result = 0;
  let i = 0;
  
  while (i < input.length) {
    if (i + 1 < input.length && subtractivePairs.has(input.substring(i, i + 2))) {
      result += romanValues[input[i + 1]] - romanValues[input[i]];
      i += 2;
    } else {
      result += romanValues[input[i]];
      i += 1;
    }
  }
  
  if (result < 1 || result > 3999) {
    throw new Error('Number out of range');
  }
  
  // Re-encode the result canonically and compare with input
  const canonical = numberToRoman(result);
  if (canonical !== input) {
    throw new Error('Input is not canonical');
  }
  
  return result;
}

function numberToRoman(num: number): string {
  const romanNumerals: [number, string][] = [
    [1000, 'M'],
    [900, 'CM'],
    [500, 'D'],
    [400, 'CD'],
    [100, 'C'],
    [90, 'XC'],
    [50, 'L'],
    [40, 'XL'],
    [10, 'X'],
    [9, 'IX'],
    [5, 'V'],
    [4, 'IV'],
    [1, 'I']
  ];
  
  let result = '';
  
  for (const [value, numeral] of romanNumerals) {
    while (num >= value) {
      result += numeral;
      num -= value;
    }
  }
  
  return result;
}
```