Search code examples
javascriptarraystypescriptobjectimplicit-conversion

TypeScript: Element implicitly has an 'any' type for RegExp


So, I want to create a function that would take a duration string (e.g 12ms, 7.5 MIN, 400H), parse it and convert it to milliseconds.

const units = {
  MS: 1,
  S: 1 * 1000,
  MIN: 60 * 1 * 1000,
  H: 60 * 60 * 1 * 1000
}

export function toMS(str: string): number {
  let regex = /^(?<number>\d+(?:\.\d+)?)(?:\s*(?<unit>MS|S|MIN|H))?$/i

  if (!regex.test(str)) {
    throw new TypeError(`Expected a duration, got: ${str}`)
  }

  let match = str.match(regex)

  let number = Number(match?.groups?.number)
  let unit = (match?.groups?.unit || 'ms').toUpperCase()

  return Math.floor(number * units[unit])
}

So, function toMS() takes a string, tests if the provided string is valid (number + whitespace (optional) + unit abbr (optional)) and if it's valid - parses it using str.match(regex).

Everything works fine until: units[unit]. It gives me an error: Element implicitly has an 'any' type because expression of type 'string' can't be used to index type.

I've had the same error before with function arguments & class constructors and it was easy to solve since the user provides data. But now I have no idea how to solve that, cause I can't force str.match(regex).groups.unit to have a specific type like : 'MS | S | MIN | H.

I know that it is also possible to create tsconfig.json with "noImplicitAny": false but in my case that's not good at all.


Solution

  • The problem is that TypeScript has no way of knowing whether your regex will really match MS|S|MIN|H. This is dependent types territory, and TypeScript is not that powerful yet.

    The only thing TypeScript knows is that what you've matched is going to be a string, because both match?.groups?.unit and 'ms' expressions yield string.

    What you can do is letting TypeScript know that your units is an object with keys of type string and values of type number, and check whether what you've matched is a property on the units object. Like this:

    const units: { [k: string]: number } = { // letting TypeScript know
      MS: 1,
      S: 1 * 1000,
      MIN: 60 * 1 * 1000,
      H: 60 * 60 * 1 * 1000,
    };
    
    export function toMS(str: string): number {
      const regex = /^(?<number>\d+(?:\.\d+)?)(?:\s*(?<unit>MS|S|MIN|H))?$/i;
    
      if (!regex.test(str)) {
        throw new TypeError(`Expected a duration, got: ${str}`);
      }
    
      const match = str.match(regex);
      const number = Number(match?.groups?.number);
      const unit = (match?.groups?.unit || 'ms').toUpperCase();
    
      if (unit in units) { // checking whether your matched string is something you can handle, in runtime
        return Math.floor(number * units[unit]);
      } else {
        throw new Error(`couldn't find units! :(`);
      }
    }