TypeScript
Completed
-
Section 1: Getting Started
-
Section 2: Basic Types
-
Section 3: Functions and Objects
-
Section 4: Advanced Types
-
Section 5: Object-Oriented TypeScript
-
Section 6: Working with Modules
-
Section 7: Tooling and Practice
-
Section 8: Type-Level Programming
-
Section 9: TypeScript with Backends
-
Section 10: Testing Typed Code
-
Section 11: Data Validation with Types
-
Section 12: Practical Projects
-
Section 13: Compiler Internals
-
Section 14: Configuration Deep Dive
-
Section 15: Enums, Symbols, and Special Types
-
Section 16: Working with Async Code
-
Section 17: TypeScript and the DOM
-
Section 18: Advanced Generics Practice
-
Section 19: Working with Third-Party Types
-
Section 20: Monorepo and Large-Scale Practices
-
Section 21: Common Pitfalls and Best Practices
-
Section 22: Interview Practice
-
Section 23: Handbook: Narrowing In Depth
-
Section 24: Handbook: Object Types In Depth
-
Section 25: Handbook: Classes In Depth
-
Section 26: Handbook: Modules In Depth
-
Section 27: Handbook: Declaration Files In Depth
-
Section 28: JSX and Namespaces
-
Section 29: Compiler Configuration Reference
-
Section 30: More Practice Exercises
-
Section 31: Handbook: Everyday Types Deep Dive
-
Section 32: Utility Types Full Reference
-
Section 33: Decorators Reference
-
Section 34: Mixins and Advanced OOP Patterns
-
Section 35: Iterators and Generators Typing
-
Section 36: More Type-Level Programming Practice
-
Section 37: TypeScript Ecosystem Tools
-
Section 38: TypeScript with Testing Frameworks
-
Section 39: TypeScript for Library Authors
-
Section 40: More Interview Practice
-
Section 41: Handbook: Functions In Depth
-
Section 42: Handbook: Type Manipulation Deep Dive
-
Section 43: More Real-World Patterns
-
Section 44: TypeScript Release Notes Highlights
-
Section 45: Final Practice and Review
180: The unique symbol Type
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 symbolcalledPluginStateKey. - Define a
PluginContextinterface that has aname: stringproperty and usesPluginStateKeyas a key for a value of typeRecord<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
There are no comments for now.