What are Symbol and BigInt used for, and how do you work with each?
Because string keys can collide, and there is no way to hide them. If two libraries both want to attach some internal state to an object, and both pick the same property name, then one of them quietly overwrites the other. There is no namespace, there is no privacy, and there is no way to ask which library owns a key.
A symbol fixes all three of those problems at once. Every call to the Symbol function returns a value that is distinct from every other symbol, so two libraries cannot collide even when they use the same description. The description is only a debugging label, because it is never used for equality. And properties that are keyed by a symbol are skipped by Object.keys, by the for...in loop, by JSON.stringify, and by the spread operator, so the internal state stays out of everybody's way.
The language also uses symbols as protocol hooks. There are well-known symbols such as Symbol.iterator, which tells the language how to iterate an object, Symbol.toPrimitive, which controls how an object converts to a primitive, and Symbol.toStringTag, which changes what Object.prototype.toString reports. Implementing one of these makes your object work with language features that have no special case for your type, such as the for...of loop or the spread operator.
There is one limitation to mention. Symbol-keyed properties are not copied by JSON round trips, and they are invisible to most debugging tools unless you know to look for them. That is a good feature for internal metadata, and it is a good reason not to store anything that you need to inspect inside a symbol.