You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
At present, the only way to declare a property whose key is a well-known symbol is via the global Symbol constructor. This approach creates a number of challenges, especially for authors of libraries. Notably, it complicates the use of imported polyfills and makes it difficult to describe libraries compatible with multiple targets. This proposal suggests a solution: a literal notation for well-known symbols.
A well-known-symbol literal is a way of referring to a specific well-known symbol without referring to the global Symbol constructor or any other declaration.
@@iterator// Literal notation for Symbol.iterator
@@toStringTag// Literal notation for Symbol.toStringTag
The literal form of a well-known symbol may be used as both (1) a type and as (2) an abstract property key. It may not be used as a value.
interfaceSymbolConstructor{iterator: @@iterator;// As a type}interfaceIterable<T>{
@@iterator(): Iterator<T>;// As an abstract property key}letiterator= @@iterator;// TSError: @@iterator is not a value
While there has been some discussion of stronger type checking for symbols in general (#2012, #5579, #7436), this proposal focuses on the special case of well-known symbols. I believe well-known symbols deserve separate consideration, both because
well-known symbols raise unique challenges (see Challenges with ES6 symbols #2012 for a list of challenges raised by user-defined symbols, none of which applies here), and
the solution presented here is likely to be more straightforward to implement than any general proposal for symbol literals.
The advantages of this proposal include
that it is backward-compatible with existing syntax,
that it cannot conflict with existing user-defined types or properties, and
that it does not alter code emission.
The Problem
The current approach to well-known symbols creates a number of challenges, especially for authors of libraries intended to be compatible with multiple ECMAScript versions. Two reasons for these challenges are
That library authors typically import a Symbol polyfill if one is needed rather than expose one globally, and
That authors of libraries often don't know what the consumer's target will be and whether a global Symbol declaration will exist
I discuss each problem in turn.
Importing a Symbol polyfill
Application authors who need a Symbol polyfill usually introduce one globally; this case is unproblematic in TypeScript, as the polyfill can be used just as if it were the native Symbol constructor. Library authors – as a best practice – typically import a polyfill so as not to pollute the global namespace; this causes problems in TypeScript, as the compiler requires that well-known symbols be referenced as properties of the globalSymbol constructor (#8099, #8169).
This kind of case is common enough when writing libraries that utilize the ES2015 iteration protocols in an ES5-compatible way. But TypeScript won't accept it; it throws the following compiler error:
Error TS2470: 'Symbol' reference does not refer to the global Symbol constructor object
The error appears even though the imported Symbol object will be the global Symbol constructor if it exists in the runtime environment.
Describing a library when the consumer's configuration is unknown
Library authors often write APIs compatible with both ES5 and ES2015 and above. Consider a sum() function that accepts a sequence of numbers and returns the total. The sequence may be either (1) an array-like object or (2) an iterable object. An attempt at declaring such a function might look like this:
This works fine in ES2015 and above. But in ES5, there is a problem. Since the Iterable interface does not exist, it is interpreted as any. And thus the benefits of static typing are lost.
import{sum}from'my-math-lib';sum([2,3]);// OKsum(/abc/g);// OK? (This should throw a compiler error, but it doesn't when targeting ES5)
There is an imperfect solution to this problem. First, the author has to recreate the Iterable interface in case one is not available globally to the consumer.
exportinterfaceIterable<T>{[Symbol.iterator](): Iterator<T>;// Iterator interface omitted for brevity}
But this generates the same error we saw above if no global Symbol declaration is present.
Error TS2470: 'Symbol' reference does not refer to the global Symbol constructor object
The solution to this involves recreating the SymbolConstructor interface as well and declaring a global Symbol object (dojo/core#149). Not only is this solution convoluted, but it also pollutes the consumer's global declarations with an object that may not exist at runtime.
The Proposed Solution
There are no good solutions to the above problems at present. The solution I propose is that a literal notation for well-known symbols be added to the language. Informally, the literal notation for a well-known symbol is just a way to refer to that symbol without relying on the global Symbol constructor or any other declared object. A more formal description follows.
Well-known-symbol literal
A well-known-symbol literal has the following characteristics:
It is a reference to a particular well-known symbol
It is referred to by its specification name (e.g., @@iterable, @@toStringTag)
It is available regardless of a project's target or included declaration libraries (in the same way the symbol type is available)
It may be used either as (1) a type or (2) an abstract property key
It is a subtype of symbol when used as a type
As a type
A variable may be declared as a well-known symbol like so:
letiterator: @@iterator;
Type inference for well-known symbols is analogous to type inference for string literals:
Any value whose type is a well-known symbol may be used as a computed property in place of its corresponding property on the global Symbol constructor.
constITERATOR=Symbol.iterator;// ITERATOR: @@iterator (inferred)exportclassRangeimplementsIterable<number>{[ITERATOR](){/* ... */}// Equivalent to using [Symbol.iterator] directly}exportinterfaceIterable<T>{[ITERATOR](): Iterator<T>;// Equivalent to using [Symbol.iterator] as the property key}
As an abstract property key
We can use literal notation on an interface to declare a property whose key is a well-known symbol.
exportinterfaceIterable<T>{
@@iterator(): Iterator<T>;// Equivalent to using [Symbol.iterator] or another value of type @@iterator}
Literal notation may also be used as an abstract property key of an abstract class.
Importantly, the property must be abstract when using literal notation as a property key. Since there is no such literal notation in JavaScript, the author must supply an actual value as a computed property when implementing methods and properties whose keys are well-known symbols. It would not make sense, for example, to allow @@iterator to be used as an alias for Symbol.iterator, as the whole point of the literal notation is that we can use it without relying on the presence of the Symbol constructor.
Usage
We can use literal notation to solve both of the problems identified above.
Solving the polyfill problem
To solve the first problem, the imported Symbol polyfill simply has to have an 'iterator' property of type @@iterator rather than symbol. A partial declaration file might look like this:
declare module 'core-js/library/es6/symbol'{interfaceSymbolConstructor{iterator: @@iterator;// Instead of `iterator: symbol`}constSymbol: SymbolConstructor;export=Symbol;}
And now we can use the localSymbol.iterator as a computed property of an iterable object:
importSymbol= require('core-js/library/es6/symbol');exportclassRangeimplementsIterable<number>{[Symbol.iterator](){/* ... */}// This works, as Symbol.iterator is of type @@iterator}
Solving the multi-target library problem
To solve the second problem, the author must still write her own Iterable interface. But she need not describe or rely on a global Symbol constructor declaration.
At present, the only way to declare a property whose key is a well-known symbol is via the global
Symbolconstructor. This approach creates a number of challenges, especially for authors of libraries. Notably, it complicates the use of imported polyfills and makes it difficult to describe libraries compatible with multiple targets. This proposal suggests a solution: a literal notation for well-known symbols.A well-known-symbol literal is a way of referring to a specific well-known symbol without referring to the global
Symbolconstructor or any other declaration.The literal form of a well-known symbol may be used as both (1) a type and as (2) an abstract property key. It may not be used as a value.
While there has been some discussion of stronger type checking for symbols in general (#2012, #5579, #7436), this proposal focuses on the special case of well-known symbols. I believe well-known symbols deserve separate consideration, both because
The advantages of this proposal include
The Problem
The current approach to well-known symbols creates a number of challenges, especially for authors of libraries intended to be compatible with multiple ECMAScript versions. Two reasons for these challenges are
Symbolpolyfill if one is needed rather than expose one globally, andSymboldeclaration will existI discuss each problem in turn.
Importing a Symbol polyfill
Application authors who need a
Symbolpolyfill usually introduce one globally; this case is unproblematic in TypeScript, as the polyfill can be used just as if it were the nativeSymbolconstructor. Library authors – as a best practice – typically import a polyfill so as not to pollute the global namespace; this causes problems in TypeScript, as the compiler requires that well-known symbols be referenced as properties of the globalSymbolconstructor (#8099, #8169).Consider the following example:
This kind of case is common enough when writing libraries that utilize the ES2015 iteration protocols in an ES5-compatible way. But TypeScript won't accept it; it throws the following compiler error:
The error appears even though the imported
Symbolobject will be the globalSymbolconstructor if it exists in the runtime environment.Describing a library when the consumer's configuration is unknown
Library authors often write APIs compatible with both ES5 and ES2015 and above. Consider a
sum()function that accepts a sequence of numbers and returns the total. The sequence may be either (1) an array-like object or (2) an iterable object. An attempt at declaring such a function might look like this:This works fine in ES2015 and above. But in ES5, there is a problem. Since the
Iterableinterface does not exist, it is interpreted asany. And thus the benefits of static typing are lost.There is an imperfect solution to this problem. First, the author has to recreate the
Iterableinterface in case one is not available globally to the consumer.But this generates the same error we saw above if no global
Symboldeclaration is present.The solution to this involves recreating the
SymbolConstructorinterface as well and declaring a globalSymbolobject (dojo/core#149). Not only is this solution convoluted, but it also pollutes the consumer's global declarations with an object that may not exist at runtime.The Proposed Solution
There are no good solutions to the above problems at present. The solution I propose is that a literal notation for well-known symbols be added to the language. Informally, the literal notation for a well-known symbol is just a way to refer to that symbol without relying on the global
Symbolconstructor or any other declared object. A more formal description follows.Well-known-symbol literal
A well-known-symbol literal has the following characteristics:
@@iterable,@@toStringTag)symboltype is available)symbolwhen used as a typeAs a type
A variable may be declared as a well-known symbol like so:
Type inference for well-known symbols is analogous to type inference for string literals:
Any value whose type is a well-known symbol may be used as a computed property in place of its corresponding property on the global
Symbolconstructor.As an abstract property key
We can use literal notation on an interface to declare a property whose key is a well-known symbol.
Literal notation may also be used as an abstract property key of an abstract class.
Importantly, the property must be abstract when using literal notation as a property key. Since there is no such literal notation in JavaScript, the author must supply an actual value as a computed property when implementing methods and properties whose keys are well-known symbols. It would not make sense, for example, to allow
@@iteratorto be used as an alias forSymbol.iterator, as the whole point of the literal notation is that we can use it without relying on the presence of theSymbolconstructor.Usage
We can use literal notation to solve both of the problems identified above.
Solving the polyfill problem
To solve the first problem, the imported
Symbolpolyfill simply has to have an'iterator'property of type@@iteratorrather thansymbol. A partial declaration file might look like this:And now we can use the local
Symbol.iteratoras a computed property of an iterable object:Solving the multi-target library problem
To solve the second problem, the author must still write her own
Iterableinterface. But she need not describe or rely on a globalSymbolconstructor declaration.Now type-checking is consistent irrespective of the existence of a global
Symbolconstructor declaration.The compiler throws the expected error regardless of the consumer's configuration: