Typescript: access an object property with square bracket notation and variable

What is the correct way in TypeScript to access a Javascript object with [ ] notation and using a variable (that we are initially unsure of its value) that evaluates to a string with the brackets to access an objects property, i.e. myObject[myVariable]

The following code errors as expected as we have an unknown value in x so we can not be sure that it is a key of myDictionary (however, this isn't exactly the error, see code comment):

const myDictionary = {
  key1: 'String 1',
  key2: 'String 2',
  key3: 'String 3',
};

const possibleValuesOfX = [
  'key1',
  'notAKey1',
  'key2',
  'notAKey2',
  'key3',
  'notAKey3',
];

// simulating a random value that could be a possible key of myDictionary
const x =
  possibleValuesOfX[Math.floor(Math.random() * possibleValuesOfX.length)];

// The following errors: No index signature with a parameter of type 'string' was found on type '{ key1: string; key2: string; key3: string; }'.ts(7053)
const decode = myDictionary[x]; 

So I try the following, thinking I'm checking to ensure x is a key of myDictionary before attempting to access myDictionary with x

const myDictionary = {
  key1: 'String 1',
  key2: 'String 2',
  key3: 'String 3',
};

const possibleValuesOfX = [
  'key1',
  'notAKey1',
  'key2',
  'notAKey2',
  'key3',
  'notAKey3',
];

const x =
  possibleValuesOfX[Math.floor(Math.random() * possibleValuesOfX.length)];

let decode: string;
if (x in myDictionary) {
  // Still produces the same error as above
  decode = myDictionary[x];
}

This doesn't stop the error.

Now I can stop the error with const myDictionary: { [key: string]: string } but this also stops autocomplete from working with myDictionary and provides little else from what I can see.

To keep auto complete and stop the error I can do the following:

type myDictionaryKeys = 'key1' | 'key2' | 'key3';
const myDictionary: Record<myDictionaryKeys, string> = {
  key1: 'String 1',
  key2: 'String 2',
  key3: 'String 3',
};

const possibleValuesOfX = [
  'key1',
  'notAKey1',
  'key2',
  'notAKey2',
  'key3',
  'notAKey3',
];

const x =
  possibleValuesOfX[Math.floor(Math.random() * possibleValuesOfX.length)];

let decode: string;
if (x in myDictionary) {
  decode = myDictionary[x as myDictionaryKeys];
}

But this still feels 'hacky' and overly verbose as I'm casting x as myDictionaryKeys to remove the error and having to type the keys out twice once in myDictionary object and once in myDictionaryKeys type.

Is there a 'proper' way to do this, or a cleaner way?


Edit: example updated in response to the question in your comment.


You can use a type predicate to inform the compiler that the value is a key in the dictionary, like the isDictionaryKey function in this example:

TS Playground

function isDictionaryKey <T>(
  dictionary: T,
  str: string,
): str is keyof Omit<T, number | symbol> {
  return str in myDictionary;
}

const myDictionary = {
  key1: 'String 1',
  key2: 'String 2',
  key3: 'String 3',
};

const possibleValuesOfX = [
  'key1',
  'notAKey1',
  'key2',
  'notAKey2',
  'key3',
  'notAKey3',
];

const x = possibleValuesOfX[Math.floor(Math.random() * possibleValuesOfX.length)];

let decode: string;
if (x && isDictionaryKey(myDictionary, x)) {
  x; // "key1" | "key2" | "key3"
  decode = myDictionary[x];
}