A Key To Keys: When JavaScript keys don’t match

AnyWhichWay
codeburst

--

It is not uncommon for JavaScript developers to turn values into keys or keys into values, typically for use in caching or indexing. The common approach is to simply turn values into strings, e.g.:

const index = {};
const object = {id:123, name:"joe",age:27};
index[object.id] = object;

In this case, JavaScript will automatically convert the id into a string. However, this means that something with a different, but coercibly the same, id might overwrite the index value:

const index = {};
const object = {id:"123", name:"mary",age:26};
index[object.id] = object;

If your application controls all id generation, you could avoid this. But what if it doesn’t or what if you have a different use case, e.g. using function arguments to create a memoization key? This could create nasty, hard to track down bugs. Below is a trivial memoize function subject to this flaw and a sum function similar to that found in a spreadsheet:

function memoize(f) {
const memoized = function(...args) {
const key = args.reduce((accum,key,i) =>
accum+=key+(i<args.length-1 ? ":" : ""),"");
return memoized[key] || (memoized[key] = f(...args));
}
return memoized;
}
function sum(...args) {
return args.reduce((accum,value) =>
typeof(value)==="number" ? accum+=value : accum,0);
}
console.log(sum(10,"100")); // expect 10, get 10
console.log(sum(10,100)); // expect 110, get 110
sum = memoize(sum);console.log(sum(10,"100")); // expect 10, get 10
console.log(sum(10,100)); // expect 110, get 10!

Note, although the above is a trivial memoizer, there are popular memoizer libraries subject to this flaw. Another example of a function that will not memoize correctly in libraries subject to this flaw is:

function kindOf (arg) {
return (arg && typeof arg ==="object"
? arg.constructor.name
: typeof arg)
}

Once again trivial, but there is no telling what may be in the bowels of your or some else’s code.

There is a simple solution that takes advantage of the nature of JSON strings.

const toKey = (value) => {
const type = typeof(value);
if(!value || type==="number" || type==="boolean) return value+"";
return JSON.stringify(value);
}

This function will return 123 for the number 123, but it will return "123" for the string 123. The code below illustrates the difference:

const test = {
[toKey(123)]: 123,
[toKey("123")]: "123"
}
console.log(test); // {123: "123", "123": "123"}
console.log(JSON.stringify(test)); // {"123":123,"\"123\"":"123"}

Note the quotes around the second key above.

You may also be wondering what the heck the square braces are in the object creation. These are a little known feature of JavaScript that allows you to create properties based on evaluation of other JavaScript … the topic of future article.

Ok, so what if you want to reverse the process and turn a key into its original value? Just use JSON.parse and rely on error handling, e.g.

const fromKey = (value) => {
if(value==="undefined") return;
try {
// converts string numbers,null,objects
// ... throws on strings that can't be converted
return JSON.parse(value);
} catch(error) {
return value;
}
}

This relies on the fact that JSON.parse when called on a string that contains a quoted string will return the quoted string, i.e. JSON.parse(“\"a\"") returns “a".The below test will print true,

console.log(Object.keys(test).every(key=>fromKey(key)===test[key]));

The line if(value==="undefined") return handles a special case where toKey generates a value that can’t be parsed but should not be returned as a string.

Note, there are situations where the above code will fail, e.g. attempting to use a function as a key. Also, stringifying JavaScript objects to use them as keys can be quite inefficient from a speed perspective and create monstrous keys sucking up RAM. However, the article is intended to be illustrative rather than provide a comprehensive key generation approach for objects. As an exercise, you may wish to modify toKey to handle objects and look-up id fields or generate keys using something like uuid.

A short read, but I hope useful!

--

--

Changing Possible ... currently from the clouds around Seattle.