Search code examples
javascripttypescripttypescript-typingsreact-typescript

Mapping string to an enum in Typescript


I am unable to map a string onto an enum, and I get the following error TypeError: Cannot convert undefined value to object

export enum CallErrorType {
  UNAUTHENTICATED,
}

const handler: {
  [key in CallErrorType]: (message: string) => void;
} = {
  [CallErrorType.UNAUTHENTICATED]: message => {
    console.log('----> Error UNAUTHENTICATED', message);
  },
};

const CallErrorTypeHandler = (errorCode: string, message: string) => {
  console.log({errorCode});

  const mapped =
    CallErrorType[errorCode as keyof typeof CallErrorType];

  if (mapped !== undefined) {
    return handler[mapped](message);
  } else {
    console.error('Invalid error string:', errorCode);
  }
};

Am I doing something wrong?


tsconfig (React Native)

{
  "$schema": "https://json.schemastore.org/tsconfig",
  "display": "React Native",
  "_version": "3.0.2",
  "compilerOptions": {
    "target": "esnext",
    "module": "commonjs",
    "types": ["react-native", "jest"],
    "lib": [
      "es2019",
      "es2020.bigint",
      "es2020.date",
      "es2020.number",
      "es2020.promise",
      "es2020.string",
      "es2020.symbol.wellknown",
      "es2021.promise",
      "es2021.string",
      "es2021.weakref",
      "es2022.array",
      "es2022.object",
      "es2022.string"
    ],
    "allowJs": true,
    "jsx": "react-native",
    "noEmit": true,
    "isolatedModules": true,
    "strict": true,
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "allowSyntheticDefaultImports": true,
    "forceConsistentCasingInFileNames": false,
    "esModuleInterop": true,
    "skipLibCheck": true
  },
  "exclude": [
    "node_modules",
    "babel.config.js",
    "metro.config.js",
    "jest.config.js"
  ]
}

I am now encountering error TypeError: 0, _$$_REQUIRE(_dependencyMap[3](...)rror").CallErrorTypeHandler is not a function (it is undefined)


Solution

  • By Default Typescript goes on to number based Mapping.

    The problem here is how TypeScript handles number-based enum mapping. When TypeScript compiles your enum, it will generate an object that has both number-to-string and string-to-number mappings for the enum values. For your CallErrorType enum, the resulting object will look like:

    {
      0: "UNAUTHENTICATED",
      "UNAUTHENTICATED": 0
    }
    

    Now, when you use CallErrorType[errorCode as keyof typeof CallErrorType], you are trying to access the value of the key named after the string in errorCode. If the string isn't found, it will return undefined.

    There is no other way, but to specify the enum in 👇 way:

    export enum CallErrorType {
      UNAUTHENTICATED = "UNAUTHENTICATED",
    }
    
    const handler: {
      [key in CallErrorType]: (message: string) => void;
    } = {
      [CallErrorType.UNAUTHENTICATED]: message => {
        console.log('----> Error UNAUTHENTICATED', message);
      },
    };
    
    const CallErrorTypeHandler = (errorCode: string, message: string) => {
      console.log({errorCode});
    
      const mapped = CallErrorType[errorCode as keyof typeof CallErrorType];
    
      if (mapped && typeof mapped === 'string') { // Check if it's a string
        return handler[mapped](message);
      } else {
        console.error('Invalid error string:', errorCode);
      }
    };
    

    Notes 📝:

    -Instead of directly checking mapped !== undefined, we should also verify that the mapped value isn't a string (i.e., ensure it's a number).

    -To be more explicit, consider using a string-based enum. This will help reduce any ambiguities.