Skip to Content
Course content

180: The unique symbol Type

Click on the "Edit" button in the top corner of the screen to edit your slide content.

Imagine you're running a high-end hotel. You have a master key that opens every door in the building. Now, if you just tell your staff, "Use a key to open the door," that's too vague. Any key—even a guest's key—is technically "a key." But if you say, "Use this specific, gold-plated master key," you've moved from a general category to a unique identity. Even if someone finds another gold-plated key that looks identical, it isn't that specific key, and it won't work.

In TypeScript, a regular symbol is like saying "any key." It's a type that describes any symbol value. A unique symbol, however, is like that gold-plated master key. It tells TypeScript that this specific constant is the only value that can ever satisfy that type.

Why generic symbols aren't enough

You already know that Symbol('description') creates a unique value at runtime. But from a type perspective, Symbol('A') and Symbol('B') both have the type symbol. If you have a function that expects a specific symbol to act as a secret internal key, and you type it as symbol, any symbol from anywhere in your application will pass the type check. That's usually not what we want when we're trying to implement strict internal APIs or metadata stores.

// The "generic" way
const internalKey = Symbol('internal');

function accessSecret(key: symbol) {
    console.log("Access granted");
}

// This works, but so does this...
accessSecret(Symbol('something else')); // Type-safe? Yes. Intended? Probably not.

Locking down identity with unique symbol

To fix this, we use the unique symbol type. The catch is that unique symbol can only be used in a const declaration. This is because the type is tied directly to the specific instance of the symbol created at that exact moment.

// The "unique" way
const InternalKey: unique symbol = Symbol('internal');

function accessSecret(key: typeof InternalKey) {
    console.log("Access granted");
}

// This works perfectly
accessSecret(InternalKey); 

// This now fails! 
// TypeScript knows this isn't THAT specific symbol.
accessSecret(Symbol('internal')); 

I've found this incredibly useful when building libraries. If I'm attaching metadata to a user's object that I don't want the user to accidentally overwrite or access without using my library's helper functions, unique symbol is my go-to. It creates a "nominal" type—meaning the type is based on the identity of the value, not just its structure.

Using them as object keys

When you use a unique symbol as a key in an interface, you get some very powerful autocomplete and type safety. Because the symbol is unique, TypeScript knows exactly which property you're referring to, and it prevents other symbols from masquerading as that key.

const MetadataKey: unique symbol = Symbol('metadata');

interface User {
    name: string;
    [MetadataKey]: { lastLogin: Date };
}

const user: User = {
    name: "Alice",
    [MetadataKey]: { lastLogin: new Date() }
};

// TypeScript knows exactly what the type of this is:
const loginDate = user[MetadataKey].lastLogin;

One quick tip: If you find yourself needing to pass these symbols around in a generic way, you can still cast them back to symbol, but you'll lose that "master key" specificity. Use that sparingly.




📋 Practical Task

Implementing a Collision-Proof Plugin Registry

You are building a plugin system where each plugin needs to attach a private "state" object to a shared context. To prevent plugins from accidentally overwriting each other's state, you must use unique symbol keys.

Your task:

  • Create a unique symbol called PluginStateKey.
  • Define a PluginContext interface that has a name: string property and uses PluginStateKey as a key for a value of type Record<string, any>.
  • Write a function setPluginState(ctx: PluginContext, state: Record<string, any>) that assigns the state to the context using the symbol.
  • Write a function getPluginState(ctx: PluginContext) that returns the state.
  • Ensure that if someone tries to access the state using a generic Symbol('metadata'), the TypeScript compiler throws an error.
// Start your code here
const PluginStateKey = Symbol('plugin_state');
// ... implement the rest
Rating
0 0

There are no comments for now.

to be the first to leave a comment.