-
Notifications
You must be signed in to change notification settings - Fork 12.5k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Support symbol-based indexing and well-known symbols #980
Comments
@jonathandturner Could you include the actual declarations for the Symbol interface and the Symbol object? Would be helpful to see them. |
here is what i have so far, i should send a PR for these typings later today. declare class Symbol {
/** Returns a string representation of an object. */
toString(): string;
/** Returns the primitive value of the specified object. */
valueOf(): Object;
/**
* Returns a new unique Symbol value.
* @param description Description of the new Symbol object.
*/
constructor(description?: string);
/**
* Returns a Symbol object from the global symbol registry matching the given key if found.
* Otherwise, returns a new symbol with this key.
* @param key key to search for.
*/
static for(key: string): Symbol;
/**
* Returns a key from the global symbol registry matching the given Symbol if found.
* Otherwise, returns a undefined.
* @param sym Symbol to find the key for.
*/
static keyFor(sym: Symbol): string;
}
// Well-known Symbols
declare module Symbol {
/**
* 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.
*/
const hasInstance: Symbol;
/**
* A Boolean value that if true indicates that an object should be flatten to its array
* elements by Array.prototype.concat.
*/
const isConcatSpreadable: Symbol;
/**
* A Boolean value that if true indicates that an object may be used as a regular expression.
*/
const isRegExp: Symbol;
/**
* A method that returns the default iterator for an object.Called by the semantics of the
* for-of statement.
*/
const iterator: Symbol;
/**
* A method that converts an object to a corresponding primitive value.Called by the
* ToPrimitive abstract operation.
*/
const toPrimitive: Symbol;
/**
* 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.
*/
const toStringTag: Symbol;
/**
* An Object whose own property names are property names that are excluded from the with
* environment bindings of the associated objects.
*/
const unscopables: Symbol;
} |
I see that these use |
@mhegazy - getting close, but this doesn't have the call signature on Symbol so you can do: var mySymbol = Symbol("My cool symbol"); +1 Anders on the const. Also, the types of each of these is Symbol. How do we key these uniquely? |
@jonathandturner We don't need unique types to distinguish them. The uniqueness comes from the individual properties on the |
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. |
@ahejlsberg - How are indexers typed in this way? Something like: interface MyInterface {
[Symbol.toStringTag]: string;
} 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: var myInterface: MyInterface;
var mySymbol = Symbol.toStringTag;
var result = myInterface[mySymbol]; // error: symbol unknown? Similar to the limitation with string overloads? |
@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. |
@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: interface MyInterface {
[Symbol.toStringTag]: string;
"toString": () => string;
}
var myInterface: MyInterface;
myInterface[Symbol.toStringTag]; // string
myInterface["toString"]; // () => string
var mySymbol = Symbol.toStringTag;
var myString = "toString";
myInterface[mySymbol]; // any
myInterface[myStringl]; // any |
@jonathandturner Yes, the properties of the interface Object {
...
[Symbol.toStringTag]: string;
} Then, in an indexed access we would recognize interface WombatCollection {
[s: Symbol]: Wombat;
} When indexing with a value of the form |
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? var s = new Symbol();
var t = s;
interface MyInterface {
[s]: string; // Only allow keys here to be direct references to values of type symbol
}
var i: MyInterface;
i[s]; // string
i[t]; // any 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? |
Actually, my idea (using compiler symbols as a proxy for ES6 symbols) gives us something weird: interface SymbolContainer {
s: symbol;
}
var s1: SymbolContainer = { s: new Symbol() };
var s2: SymbolContainer = { s: new Symbol() };
var s3 = { s: new Symbol() };
interface MyInterface {
[s1.s]: string;
}
var i: MyInterface;
i[s1.s]; // string
i[s2.s]; // string, because it was the same compiler symbol as s1.s
i[s3.s]; // any |
Most recent proposal that seemed to have good traction: Consider an object that has defined a set of properties with symbol keys: module MySymbols {
export var firstName = Symbol();
export var age = Symbol();
}
var x = {
[MySymbols.firstName] = 'bob',
[MySymbols.age] = 42
}; When resolving the type of an indexed property access ( var n = x[MySymbols.firstName]; // n: string;
var a = x[MySymbols.age]; // a: number In this example, the property access in the first line of code refers to the same dotted path of names ( var age = MySymbols.age;
var aa = x[age]; // aa: any |
Discussed with @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. |
I've updated the proposal above with my thoughts. Github still tells you that @jonathandturner still wrote it, but I have just appended to what he originally wrote. |
Upon closer inspection of the ES6 spec, there is indeed a Symbol primitive type and a Symbol object type. So I think we may have to make a keyword for it. |
I've updated the plan above to include a keyword for symbol. This is because otherwise, Symbol is effectively interchangeable with Object. In other words, the following would be allowed (courtesy of @mhegazy): var s: Symbol = { p: "hello" };
console.log("" + s); // Crashes at runtime |
I'm not sold in the need for a new keyword. I think you can solve the problem by adding a private tag property to the Symbol class, effectively making it impossible to create instances other than through the declared API. |
@ahejlsberg it is an interface, not a class. |
@JsonFreeman Then make it a class. |
Then it can't have a call signature. |
Why is it bad to have a new keyword? |
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 |
Also, we have to have special rules in the compiler that disallow various things, like string concat on a symbol: var s = Symbol("foo");
console.log("" + s); // Crashes 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. |
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. |
Also, the following would be very intuitive if we made a lowercase name for this type: var str: string = "";
var num: number = 0;
var sym: symbol = Symbol();
console.log(typeof str); // "string"
console.log(typeof num); // "number"
console.log(typeof sym); // "symbol" I actually think it is less confusing if symbol works the same way as number and string. |
Are you sure I understand how we could have a global |
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
|
When I follow that link I see nothing about throwing if an argument is Symbol. |
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 |
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)
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.
@JsonFreeman writes:
There are three kinds of ES Symbols:
Symbol.iterator
)Symbol.for("foo")
)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 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:
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.
The text was updated successfully, but these errors were encountered: