Recently I've been facing a case where I needed to create some unique references.
I was creating a JS API and needed to maintain some internal state relative to elements created by the API.
In practice, I had a WeakMap
where I maintained a private state for each public reference returned by the API.
Why a
WeakMap
?The keys of a
WeakMap
are weak references, which means as soon as no one else holds the reference of a key, the key and its value get removed from the map and garbage collected.
The type of the public references didn't matter to me, I just needed unique references.
But it would be better if those references were easy to debug, which meant having at least a relevant toString()
method.
⛔ Non choices ⛔
{}
What's the easiest way to create a unique reference in JS? Initializing an empty object!
But that doesn't satistfy the "easy to debug" requirement:
const ref = {}
console.log(`debug: ${ref}`)
// Output:
// debug: [object Object]
String
A string is easy to debug, you just print it:
let nextId = 1
const ref = `ref #${nextId++}`
console.log(`debug: ${ref}`)
// Output:
// debug: ref #1
But a string isn't a unique reference, anyone may obtain the same reference very easily.
And as a matter of fact, a WeakMap
doesn't accept strings as keys, neither any other primitive type!
Symbol
The purpose of Symbol
is to create unique references!
There I thought I had my solution...
let nextId = 1
const ref = Symbol(`ref #${nextId++}`)
console.log(`debug: ${ref}`)
// Output:
// TypeError: Cannot convert a Symbol value to a string
Wow! Far from what I expected...
Even though Symbol
has a toString()
method, putting it in a string interpolation yells at me!
Furthermore symbols are primitive values, hence WeakMap
won't accept these as keys!
🟢 Solutions 🟢
class
There I found myself forced to write my own class
:
let nextId = 1
class Ref {
constructor() { this.id = nextId++ }
toString() { return `ref #${this.id}` }
}
const ref = new Ref()
console.log(`debug: ${ref}`)
// Output:
// debug: ref #1
This is exactly what I wanted, but it feels like I'm squashing a fly with a sledgehammer...
Do I really have to create my own type?
new String()
Yes it's possible to use the String constructor, and it is equivalent to using a class
:
let nextId = 1
const ref = new String(`ref #${nextId++}`)
console.log(`debug: ${ref}`)
// Output:
// debug: ref #1
It does create a unique reference, which isn't of a primitive type.
Hence it may be used as a key for a WeakMap
!
Your turn!
Do you have any other ideas? Share them with me!
Thanks for reading, give a ❤️, leave a comment 💬, and follow me to get notified of my next posts.
Top comments (4)
I'd use
Symbol
as it's intended exactly for your use case.Regarding your problems with Symbols:
1. Not used in strings
Why not just write
But I stopped constructing message strings for console.log(), I now just add parameters I want to log:
2. Symbols can't be used as keys in
WeapMap
I just makes no sense to have primitive values as keys in a
WeakMap
, because they cannot be weakly referenced. That's why its forbidden.Are you sure that you need a
WeakMap
in our situation? With aMap
, Symbols can be used.Thank you for your response.
I agree with you, not being able to put a Symbol in a string interpolation isn't actually a problem.
I'm aware of why primitive values may not be used in a
WeakMap
, even if this might have appeared a little unclear in my post 😅Yes I'm sure I need a
WeakMap
:The case is an API which returns unique references, and for each of these references, some internal state is maintained by the API.
As soon as the user of the API discards a unique reference, the internal state may be garbage collected from the
WeakMap
.This avoids asking from the user of the API to "release" each reference once not used anymore.
Ok, I agree with you that a
WeapMap
is fine here, and therefore it should IMHO be possible to have Symbols as WeakMap keys.I just found an interesting thread at github.com/tc39/ecma262/issues/1194 (TL;DR)
Unfortunately, because of the existence of non-unique Symbols (
Symbol.for()
) this won't come to ECMAScript :-/So, the best options - as you mentioned - are
new String()
or object/class.The latter with a
ref
property for better "debugability" and/or atoString()
implementation (just an idea: debug info like the ref number could be held in aWeakMap
if you want to hide it from the object, toString() could read from there)Indeed interesting thread!
Too bad, I'd have prefered Symbols to be allowed as WeakMap keys...
Yes, primarily I've been using a WeakMap to hold sensitive data (which shouldn't be altered by any other way than calling the API), but it could also hold debug info.