```ts
export function romanToInt(input: string): number {
  if (typeof input !== 'string') {
    throw new Error('Input must be a string');
  }
  
  if (input.length === 0) {
    throw new Error('Input cannot be empty');
  }
  
  const romanChars = new Set(['I', 'V', 'X', 'L', 'C', 'D', 'M']);
  for (const char of input) {
    if (!romanChars.has(char)) {
      throw new Error('Invalid character in input');
    }
  }
  
  const values: Record<string, number> = {
    'I': 1,
    'V': 5,
    'X': 10,
    'L': 50,
    'C': 100,
    'D': 500,
    'M': 1000
  };
  
  let result = 0;
  let i = 0;
  
  while (i < input.length) {
    const currentChar = input[i];
    const currentValue = values[currentChar];
    
    if (i + 1 < input.length) {
      const nextChar = input[i + 1];
      const nextValue = values[nextChar];
      
      if (currentValue < nextValue) {
        result += nextValue - currentValue;
        i += 2;
        continue;
      }
    }
    
    result += currentValue;
    i += 1;
  }
  
  if (result < 1 || result > 3999) {
    throw new Error('Number out of range');
  }
  
  // Re-encode the value back to canonical form and compare
  const canonical = intToRoman(result);
  if (canonical !== input) {
    throw new Error('Input is not in canonical form');
  }
  
  return result;
}

function intToRoman(num: number): string {
  if (num < 1 || num > 3999) {
    throw new Error('Number out of range');
  }
  
  const ones = ['', 'I', 'II', 'III', 'IV', 'V', 'VI', 'VII', 'VIII', 'IX'];
  const tens = ['', 'X', 'XX', 'XXX', 'XL', 'L', 'LX', 'LXX', 'LXXX', 'XC'];
  const hundreds = ['', 'C', 'CC', 'CCC', 'CD', 'D', 'DC', 'DCC', 'DCCC', 'CM'];
  const thousands = ['', 'M', 'MM', 'MMM'];
  
  return (
    thousands[Math.floor(num / 1000)] +
    hundreds[Math.floor((num % 1000) / 100)] +
    tens[Math.floor((num % 100) / 10)] +
    ones[num % 10]
  );
}
```