In TypeScript, what is the difference between the `symbol` type and a `unique symbol` type, and where are you allowed to use `unique symbol`?
answer
- one type for all of them, one per declaration
- identity has to be stable to be tracked
- const only, never let
- refer to it with typeof
- needed for computed keys in a type
basics
~20 sThe symbol type covers all symbols interchangeably; a unique symbol is a subtype tied to one specific declaration, so the checker can tell it apart from every other symbol. It is only allowed on const declarations and readonly static properties.
solid answer
~50 s`symbol` is the general primitive type: any symbol is assignable to it, and the checker cannot distinguish one from another. `unique symbol` is a subtype tied to a *single declaration*, which lets the type layer track that one identity. Because the type is bound to a declaration, it is only permitted on `const` declarations and `readonly static` properties — `let s: unique symbol` is an error, since a mutable binding could later hold a different symbol. Inference follows the same rule: `const KEY = Symbol("k")` gets a unique symbol type, while `let KEY = Symbol("k")` widens to `symbol`. You refer to that type elsewhere with `typeof KEY`. The practical payoff is computed property keys: a type or interface can only use a computed key whose expression has a literal type or a unique symbol type, so `interface Session { [KEY]: string }` requires the `const` form.
code
typescript · 12 linesconst ID = Symbol("id"); // inferred as a unique symbol
interface Session {
[ID]: string;
userName: string;
}
const session: Session = { [ID]: "abc", userName: "ada" };
console.log(session[ID].toUpperCase());
let LOOSE = Symbol("loose"); // widened to symbol
const general: symbol = LOOSE;go deeper
Know that symbol is the annotation for a symbol value and that the capitalised Symbol is the wrapper interface. Recognising the phrase unique symbol and knowing it involves const declarations is enough here.
Explain that unique symbol is tied to one declaration, why that forces const or readonly static, how inference widens a let to symbol, and that typeof is how you refer to the type.
Show where it matters in design: symbol-keyed properties in a public type, why a fresh symbol with the same description is correctly rejected, and why consumers must import the declaration rather than recreate the symbol.
Weigh symbol keys against branded string keys or explicit namespacing for a library's extension points: what each costs in serialisation, tooling visibility and consumer ergonomics before committing the public API to one.
## Two levels of precision for one primitive Symbols are a JavaScript primitive; TypeScript's job is to describe them. - `symbol` is the general type. Every symbol is assignable to it, and the checker treats all symbols as interchangeable. - `unique symbol` is a subtype that identifies **one particular declaration's** symbol. Two different `unique symbol` declarations have two different types, and neither is assignable to the other. Capital `Symbol` is a third thing entirely: the standard-library interface for the wrapper object (and, in a value position, the global function). As with `String` and `Number`, do not write it in an annotation when you mean the primitive. ## Why the declaration form is restricted A `unique symbol` type means "the symbol produced by *this* declaration and nothing else". That promise is only keepable if the binding cannot be reassigned, so the compiler restricts the type to `const` declarations and `readonly static` properties: ```ts const OK: unique symbol = Symbol("ok"); let NOT_OK: unique symbol = Symbol("no"); // error: a variable whose type is a 'unique symbol' type must be 'const' ``` Inference mirrors the same reasoning without any annotation at all: ```ts const A = Symbol("a"); // unique symbol let B = Symbol("b"); // symbol — a mutable binding could hold another symbol later ``` That is the same mutability-drives-widening logic the compiler applies to primitive literals, expressed for symbols. ## Referring to the type A unique symbol type has no name you can spell directly — it is defined by its declaration — so you reference it with the `typeof` type operator: ```ts const KEY = Symbol("key"); function take(k: typeof KEY) {} take(KEY); // ok take(Symbol("key")); // error: a different symbol, even with the same description ``` The rejected call is the point of the feature: descriptions are not identities, and the type layer follows the declaration, not the string you passed. ## The main payoff: computed keys in types The most common reason to care is using a symbol as a **property key inside a type**. A computed property name in an interface or type literal must refer to an expression whose type is a literal type or a unique symbol type — a plain `symbol` is not specific enough, because the checker would not know *which* key you meant: ```ts const ID = Symbol("id"); // unique symbol interface Session { [ID]: string; // ok } const s: Session = { [ID]: "abc" }; s[ID].toUpperCase(); // typed as string let LOOSE = Symbol("loose"); // symbol interface Bad { [LOOSE]: string; // error: needs a literal or unique symbol type } ``` This is what makes symbol-keyed properties usable in typed code at all: without the unique-symbol machinery the key would be anonymous to the checker and you could not describe the shape. ## What is compile-time and what is not Be precise about the boundary. Symbols themselves are real runtime values — `Symbol("id")` is an ordinary call that survives compilation, and the runtime uniqueness of a symbol is JavaScript's own behaviour, not TypeScript's. What `unique symbol` adds is a **type-level** identity so the checker can track that one declaration through your types. The annotation and the type are erased; the symbol is not. One consequence worth carrying: a `unique symbol` type is tied to the declaration, so re-exporting or re-declaring produces a different type even if the runtime value is shared through a module. Import the declaration rather than recreating the symbol. ## When you will actually use it Mostly when you are designing a shape whose keys should not collide with ordinary string keys, or when a library wants a hidden-but-typed slot on user objects. In everyday application code the general `symbol` type is enough, and `unique symbol` shows up the moment you try to put a symbol key into an interface and the compiler tells you the expression is not specific enough. ## The short version `symbol` = any symbol. `unique symbol` = the one from this declaration, allowed only on `const` and `readonly static` because the identity has to be stable. Reference it with `typeof`, and reach for it when a symbol has to serve as a typed property key.
- Why does `let s: unique symbol = Symbol("s")` fail to compile?A unique symbol type names the symbol produced by one specific declaration. A `let` binding can be reassigned to a different symbol later, which would make the type a lie, so the compiler restricts unique symbol types to `const` declarations and `readonly static` properties. Declare it `const` and, if you need the type elsewhere, refer to it as `typeof s`.
- How do you write the type of a value that must be one particular symbol?Use the `typeof` type operator on the declaration: `function take(k: typeof KEY) {}` where `KEY` is a `const` holding a symbol. There is no spellable name for the unique symbol type itself. A freshly created `Symbol("key")` with the same description is a different type and is correctly rejected.
- Does `unique symbol` change anything about the value at runtime?No. `Symbol("id")` is an ordinary runtime call and the value is a real symbol either way; the runtime distinctness of symbols is JavaScript's own behaviour. `unique symbol` only gives the type layer an identity to track, and it is erased along with every other annotation. It changes what the checker permits, not what runs.
saying these in an interview costs you the question
- Says unique symbol makes the symbol unique at runtime.
- Thinks two symbols with the same description share a type.
- Uses let for a symbol meant as a typed property key.
- Writes the capitalised Symbol interface as the annotation.
- Believes a plain symbol type can serve as a computed key in an interface.