Skip to content
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

Closed
sophiajt opened this issue Oct 28, 2014 · 31 comments
Closed

Support symbol-based indexing and well-known symbols #980

sophiajt opened this issue Oct 28, 2014 · 31 comments
Labels
Fixed A PR has been merged for this issue Spec Issues related to the TypeScript language specification

Comments

@sophiajt
Copy link
Contributor

sophiajt commented Oct 28, 2014

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.

@JsonFreeman writes:
There are three kinds of ES Symbols:

  1. Built in Symbols (Symbol.iterator)
  2. Registered Symbols (Symbol.for("foo"))
  3. 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:

  1. The key has to be of type symbol
  2. The Symbol identifier has to resolve to the global variable Symbol (although I think we should make it a const).
  3. 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.

@sophiajt sophiajt added the Spec Issues related to the TypeScript language specification label Oct 28, 2014
@ahejlsberg
Copy link
Member

@jonathandturner Could you include the actual declarations for the Symbol interface and the Symbol object? Would be helpful to see them.

@mhegazy
Copy link
Contributor

mhegazy commented Oct 28, 2014

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;
}

@ahejlsberg
Copy link
Member

I see that these use const declarations. How will that work if you're not compiling with -t ES6?

@sophiajt
Copy link
Contributor Author

@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?

@ahejlsberg
Copy link
Member

@jonathandturner We don't need unique types to distinguish them. The uniqueness comes from the individual properties on the Symbol object.

@mhegazy
Copy link
Contributor

mhegazy commented 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
Copy link
Contributor Author

@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?

@mhegazy
Copy link
Contributor

mhegazy commented 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
Copy link
Contributor

mhegazy commented Oct 28, 2014

@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

@ahejlsberg
Copy link
Member

@jonathandturner Yes, the properties of the Symbol object become the uniquely tracked Symbol "literals". For example, I'm assuming the global Object interface will include the following:

interface Object {
    ...
    [Symbol.toStringTag]: string;
}

Then, in an indexed access we would recognize Symbol.toStringTag as a "literal" and resolve to type string based on the property declaration in Symbol. We would also allow you to declare an index signature for the Symbol type:

interface WombatCollection {
    [s: Symbol]: Wombat;
}

When indexing with a value of the form Symbol.xxx we'd use the type associated with that property in the Symbol object. Otherwise, we'd use the declared type of the symbol index signature, i.e. Wombat in the example above.

@JsonFreeman
Copy link
Contributor

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?

@JsonFreeman
Copy link
Contributor

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

@RyanCavanaugh
Copy link
Member

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 (x[a.b.c]) where a.b.c refers to a value of type Symbol, we will use the dotted name as a 'key' to identify different symbol properties when they are specifically declared:

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 (MySymbols.firstName) as was used to initialize the object. This means that aliased symbols will not work as expected:

var age = MySymbols.age;
var aa = x[age]; // aa: any

@JsonFreeman
Copy link
Contributor

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.

@JsonFreeman
Copy link
Contributor

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.

@JsonFreeman
Copy link
Contributor

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.

@JsonFreeman
Copy link
Contributor

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

@ahejlsberg
Copy link
Member

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.

@JsonFreeman
Copy link
Contributor

@ahejlsberg it is an interface, not a class.

@ahejlsberg
Copy link
Member

@JsonFreeman Then make it a class.

@JsonFreeman
Copy link
Contributor

Then it can't have a call signature.

@JsonFreeman
Copy link
Contributor

Why is it bad to have a new keyword?

@ahejlsberg
Copy link
Member

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 symbol and Symbol.

@JsonFreeman
Copy link
Contributor

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.

@JsonFreeman
Copy link
Contributor

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
Copy link
Contributor

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.

@ahejlsberg
Copy link
Member

Are you sure "" + s crashes? From my reading of the spec it returns a string that describes the symbol.

I understand how we could have a global Symbol object that isn't a type and a built-in symbol keyword 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 that typeof returns a different string for symbol isn't really a justification, it returns different strings for DOM elements as well.

@JsonFreeman
Copy link
Contributor

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:

...
7. Let lprim be ToPrimitive(lval).
...
9. Let rprim be ToPrimitive(rval).
...
11. If Type(lprim) is String or Type(rprim) is String, then
     a. If Type(lprim) is Symbol or Type(rprim) is Symbol, then throw a TypeError exception.
     b. Return the String that is the result of concatenating ToString(lprim) followed by ToString(rprim)

@JsonFreeman
Copy link
Contributor

@ahejlsberg
Copy link
Member

When I follow that link I see nothing about throwing if an argument is Symbol.

@JsonFreeman
Copy link
Contributor

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

@JsonFreeman JsonFreeman added the Fixed A PR has been merged for this issue label Feb 18, 2015
@microsoft microsoft locked and limited conversation to collaborators Jun 18, 2018
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
Fixed A PR has been merged for this issue Spec Issues related to the TypeScript language specification
Projects
None yet
Development

No branches or pull requests

5 participants