Repository navigation
Support symbol-based indexing and well-known symbols #980
Description
Background
ES6 adds supports for symbols, a kind of key that can be used to access object properties with a unique value. Symbols do not collide with each other or other names, making them useful as a special property name.
In this proposal, we recommend adding support for symbols to TypeScript by typing the well-known symbols. This proposal also treats all user-defined types as a Symbol type, rather than tracking each new symbol as its own type (as this drastically increases the complexity of the type system).
Proposal
ES6 supports a handful of well-known symbols, which are used to unlock additional functionality.
(Taken from the ES6 draft specification)
| Name | Purpose |
|---|---|
| Symbol.hasInstance | A method that determines if a constructor object recognizes an object as one of the constructor’s instances. Called by the semantics of the instanceof operator. |
| Symbol.isConcatSpreadable | A Boolean value that if true indicates that an object should be flatten to its array elements by Array.prototype.concat. |
| Symbol.isRegExp | A Boolean value that if true indicates that an object may be used as a regular expression. |
| Symbol.iterator | A method that returns the default iterator for an object. Called by the semantics of the for-of statement. |
| Symbol.toPrimitive | A method that converts an object to a corresponding primitive value. Called by the ToPrimitive abstract operation. |
| Symbol.toStringTag | A String value that is used in the creation of the default string description of an object. Called by the built-in method Object.prototype.toString. |
| Symbol.unscopables | An Object whose own property names are property names that are excluded from the with environment bindings of the associated objects. |
A Symbol interface should be added to lib.d.ts for each of the above, so that they can be used as unique keys. Additionally, we need to add a new indexer that works with Symbols to compliment the numeric and string indexers that objects already support.
The Symbol interface also contains a call signature, allowing users to create Symbols. As mentioned above, these would not be unique types, but instead would be a common symbol type.
Jason Freeman (@JsonFreeman) writes:
There are three kinds of ES Symbols:
- Built in Symbols (
Symbol.iterator) - Registered Symbols (
Symbol.for("foo")) - Unregistered Symbols (
Symbol("foo"))
The following proposal is to support built-in symbols only. Below it I have a suggestion to also support registered Symbols.
-- A keyword called "symbol" for the Symbol type
-- Symbol indexer works just like string and number indexers, but has no relation to either of them (Right now a property of type "any" is assumed to correspond to the number indexer, not sure what to do about "any" here).
-- Apparent properties of symbol are the properties of Symbol
-- symbol is assignable and subtype to Symbol
-- typeof support in type guards (typeof x === "symbol")
-- Coercive operators will give type check errors if their operands are symbols
-- Known symbol properties are of the form Symbol., where is any identifier name.
-- Allow known-Symbol computed properties in interfaces, object type literals, and class property declarations (including ambients). These are places where dynamic computed properties are not allowed.
-- An index expression with a known symbol (the expression Symbol. of type symbol) will get the corresponding property from the target type.
-- If a Symbol-keyed class property is initialized, it will be moved to the constructor. We are assuming that it is okay to access Symbol.iterator in the constructor instead of in the class function closure.
-- Symbol-keyed properties will be type checked like this:
- The key has to be of type symbol
- The Symbol identifier has to resolve to the global variable Symbol (although I think we should make it a const).
- The name must be a property of the global Symbol object's type (note that Symbol.prototype is of type Symbol, not symbol).
-- The property is treated just like any other property with respect to assignability, inference, etc.
-- Symbol properties do get emitted into .d.ts.
As a possible extension, we could support registered Symbols. The identities of these Symbols are statically knowable if they are referenced as Symbol.for("name literal"). This creates a Symbol with the specified key, and all further calls with the same key will get the same Symbol.
This would work exactly like the built in Symbols. We would allow it in interfaces and ambient contexts too. The one question is whether the naive emit would be acceptable for class properties:
class C {
[Symbol.for("foo")]: string; // Call does not get emitted
[Symbol.for("bar")] = ""; // Call gets emitted in the constructor
}Suggestions are also welcome for unregistered Symbols, although I think they are much lower priority.
Note that the main limitation of these proposals is that they do not handle aliased Symbols. They also do not traffic types for Symbols, so you cannot cast an unknown Symbol to a known Symbol.
Activity
ahejlsberg commentedon Oct 28, 2014
jonathandturner Could you include the actual declarations for the Symbol interface and the Symbol object? Would be helpful to see them.
mhegazy commentedon Oct 28, 2014
here is what i have so far, i should send a PR for these typings later today.
ahejlsberg commentedon Oct 28, 2014
I see that these use
constdeclarations. How will that work if you're not compiling with-t ES6?sophiajt commentedon Oct 28, 2014
Mohamed Hegazy (@mhegazy) - getting close, but this doesn't have the call signature on Symbol so you can do:
+1 Anders on the const. Also, the types of each of these is Symbol. How do we key these uniquely?
ahejlsberg commentedon Oct 28, 2014
jonathandturner We don't need unique types to distinguish them. The uniqueness comes from the individual properties on the
Symbolobject.mhegazy commentedon Oct 28, 2014
For the ES6 one, i will have that addressed in my PR. and then we can discuss the approach and decide what we want to do. basically i have a new lib.es6.d.ts lib that includes lib.d.ts, that gets injected by default if you are targeting es6.
as for the type. last time we talked about that, we decided to uniquely type individual symbols.. they are just Symbols.
sophiajt commentedon Oct 28, 2014
Anders Hejlsberg (@ahejlsberg) - How are indexers typed in this way? Something like:
Where the indexer here is required to be a member of Symbol and each of these is made unique based on the path?
If so, then, you'd always need to provide the literal and couldn't do this:
Similar to the limitation with string overloads?
mhegazy commentedon Oct 28, 2014
jonathandturner in your example, result will be any. and probably flagged as implicit any if you have no-implicit-any on. i guess we will need to add a new indexer for symbol type, so that you van get out of these noImplicitAny errors.
mhegazy commentedon Oct 28, 2014
Jason Freeman (@JsonFreeman) and I chatted about this today. and I think we can safely treat a const Symbol just like we treat string literal; and a non-const symbol like a non-literal string.
so:
ahejlsberg commentedon Oct 28, 2014
jonathandturner Yes, the properties of the
Symbolobject become the uniquely tracked Symbol "literals". For example, I'm assuming the global Object interface will include the following:Then, in an indexed access we would recognize
Symbol.toStringTagas a "literal" and resolve to typestringbased on the property declaration inSymbol. We would also allow you to declare an index signature for theSymboltype:When indexing with a value of the form
Symbol.xxxwe'd use the type associated with that property in theSymbolobject. Otherwise, we'd use the declared type of the symbol index signature, i.e.Wombatin the example above.JsonFreeman commentedon Oct 28, 2014
We need a way to determine symbol identity in general, not just for the well known properties of Symbol. To that end, how about we use the compiler symbol's identity as an approximation for the ES6 Symbol?
This creates a false positive on Symbol identity in the case that we assign a new Symbol to an existing variable (so if we were to assign a second new Symbol to s). It creates a false negative on identity when we have two variables aliasing the same Symbol.
Is this a reasonable approximation?
JsonFreeman commentedon Oct 28, 2014
Actually, my idea (using compiler symbols as a proxy for ES6 symbols) gives us something weird:
RyanCavanaugh commentedon Oct 30, 2014
Most recent proposal that seemed to have good traction:
Consider an object that has defined a set of properties with symbol keys:
When resolving the type of an indexed property access (
x[a.b.c]) wherea.b.crefers to a value of typeSymbol, we will use the dotted name as a 'key' to identify different symbol properties when they are specifically declared:In this example, the property access in the first line of code refers to the same dotted path of names (
MySymbols.firstName) as was used to initialize the object. This means that aliased symbols will not work as expected:JsonFreeman commentedon Nov 4, 2014
Discussed with Anders Hejlsberg (@ahejlsberg) and we identified a key implementation hurdle in supporting user defined symbols. The compiler's binder is responsible for collecting and creating properties, and organizing symbolic information before it gets to the checker. However, for a property's key to be based on a user defined symbol, it needs to use the checker to look up that symbol. This introduces a circular dependency between the binder and the checker, which is not consistent with our architecture.
To this end, I am inclined to add rudimentary support for use of the built in properties of Symbol, for example Symbol.iterator, Symbol.toStringTag, etc. This will unblock iterators, and support a basic level of symbol use. But it is likely just a temporary solution, and we will keep discussing strategies for full support.
7 remaining items
JsonFreeman commentedon Jan 30, 2015
Then it can't have a call signature.
JsonFreeman commentedon Jan 30, 2015
Why is it bad to have a new keyword?
ahejlsberg commentedon Jan 30, 2015
Then we should fix the call signature problem. It's an issue for the other built-in types as well because we don't allow you to derive from them.
A new keyword is overkill and doesn't bring any real value. Quite the opposite, there'll just be a bunch of confusion over the pointless differences between
symbolandSymbol.JsonFreeman commentedon Jan 30, 2015
Also, we have to have special rules in the compiler that disallow various things, like string concat on a symbol:
We allow this for object types, but it is a TypeError for symbols.
All these checks are easier done if there is a TypeFlag for symbols, which is already very standard for primitive types, not standard for types that we have to look up.
JsonFreeman commentedon Jan 30, 2015
The confusion between symbol and Symbol is not different from the confusion between number and Number, string and String. The issue is that ES6 actually specifies that there are two types: a Symbol primitive, and a Symbol object type. Use of the latter is an error.
JsonFreeman commentedon Jan 30, 2015
Also, the following would be very intuitive if we made a lowercase name for this type:
I actually think it is less confusing if symbol works the same way as number and string.
ahejlsberg commentedon Jan 30, 2015
Are you sure
"" + scrashes? From my reading of the spec it returns a string that describes the symbol.I understand how we could have a global
Symbolobject that isn't a type and a built-insymbolkeyword that represents the type, but I don't see how that makes anything clearer. Quite the opposite. We don't have keywords for Date and RegExp, and by the same token we shouldn't have a keyword for Symbol. The fact thattypeofreturns a different string for symbol isn't really a justification, it returns different strings for DOM elements as well.JsonFreeman commentedon Jan 30, 2015
I think the difference between Symbol and Date/RegExp/HTMLElement is that TypeScript actually has to account for many differences between symbols and objects, whereas the others behave far more similarly to objects. I think an argument could be made for making Date and RegExp primitive types with keywords, but a very weak argument. Conversely, an argument could be made for String and Number not being primitive types, but they had enough special treatment in the language that we made them primitives. Symbol feels to me more like string and number than it does to Date or RegExp.
I admit though, that the typeof behavior is not a strong justification.
Here's what happens when you do
"" + s, from the ES6 spec:JsonFreeman commentedon Jan 30, 2015
That is from https://people.mozilla.org/~jorendorff/es6-draft.html#sec-addition-operator-plus-runtime-semantics-evaluation
ahejlsberg commentedon Jan 31, 2015
When I follow that link I see nothing about throwing if an argument is Symbol.
JsonFreeman commentedon Jan 31, 2015
Oops, I guess they changed it. It still throws an exception though. If you look at 11.a and 11.c, they call ToString on lprim and rprim, which in turn throws an exception on Symbol
https://people.mozilla.org/~jorendorff/es6-draft.html#sec-tostring