-
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
Proposal: a literal notation for well-known symbols #13031
Comments
Libraries that wish to refer to interface Iterable<T> {
[Symbol.iterator](): Iterator<T>;
} compiles with:
|
@acutmore The problem is not about compiling the library itself. It's about the type declarations for a library whose consumer may be targeting ES3 or ES5. If I install a library whose declarations include the
Now, as a consumer of that library, I could include the
|
@akim-mcmath So the issue is that ideally it would only be a compile-time error if interface I {
[Symbol.iterator](): any; // no compiler error
}
var s = Symbol.iterator; // compile time error Maybe this could be solved more generally. If the issue is libraries wanting to support features (like symbols or other things like generators) but only if the consumer specifies that they are available at runtime then there could be conditional compilation. Similar to c#, c++ #4691 interface Foo {
bar(): void;
#if symbol
[Symbol.iterator](): Iterator;
# endif
} Or libraries offer multiple versions for use with different targets? |
@acutmore Yes, I thought about conditional compilation too. That would solve the second of the two problems I identify. And it could be used more generally for other purposes. I also considered the possibility of having different versions based on the consumer's target. This would essentially be an extension of the way setting the But neither would solve the first problem, i.e., there would still be no way of importing a polyfill and using its properties to refer to well-known symbols. A literal notation for well-known symbols would solve both of these problems. Plus, it would be consistent with other literal types that have been added to TypeScript – first string literals, and now literals for numbers, booleans, And if either of these alternative suggestions were ever implemented, it would in no way conflict with a well-known-symbol literal type. I actually think that both solutions are preferable for addressing the second problem, but I can't think of a better way to address the first than the proposal I outline above. |
Just a quick note. These proposed changes are gated on #5579. |
Any update on this? I ran into this with |
// Forward-declare needed types from lib.es2015.d.ts (in case users are using `--lib es5`)
interface Iterable<T> { }
interface Iterator<T> {
next(value?: any): IteratorResult<T>;
}
interface IteratorResult<T> { }
interface SymbolConstructor {
readonly iterator: symbol;
}
declare var Symbol: SymbolConstructor; lib.d.ts exposes Promise type for ES5 DOM use, we may need a similar approach to Symbols. |
Proposal 1. // Internally define well-known symbol.iterator etc. as unique symbols:
interface SymbolConstructor {
readonly iterator: symbol.iterator;
}
// Expose Iterator, IterableIterator, etc. by default
interface Headers {
// allow unique symbols in index signature
[key: symbol.iterator](): IterableIterator<[string, string]>;
}
interface Headers {
// Ignore the signature when type resolving fails
/** @skipTypeCheck */
[Symbol.iterator](): IterableIterator<[string, string]>;
} |
Older versions of the ES standard do not define `Symbol#asyncIterator`. This means our attempts to "manually" define `AsyncIterator` for versions of ES that don't have it fail. Following [1] and [2], our understanding is that the best existing solution is to manually re-define `SymbolConstructor` to have this member. [1]: microsoft/TypeScript#13031 [2]: microsoft/TypeScript#8099
Older versions of the ES standard do not define `Symbol#asyncIterator`. This means our attempts to "manually" define `AsyncIterator` for versions of ES that don't have it fail. Following [1] and [2], our understanding is that the best existing solution is to manually re-define `SymbolConstructor` to have this member. [1]: microsoft/TypeScript#13031 [2]: microsoft/TypeScript#8099
Older versions of the ES standard do not define `Symbol#asyncIterator`. This means our attempts to "manually" define `AsyncIterator` for versions of ES that don't have it fail. Following [1] and [2], our understanding is that the best existing solution is to manually re-define `SymbolConstructor` to have this member. [1]: microsoft/TypeScript#13031 [2]: microsoft/TypeScript#8099
This doesn't seem to come up often enough to justify adding a new syntax form, and will come up less often in the future as ES5 runtimes retire to their final resting places. You can write this today, which works in any target: declare const SymbolShim: SymbolConstructor;
interface SymbolConstructor {
readonly iterator: unique symbol;
}
interface foo {
[SymbolShim.iterator](): any;
} |
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.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
Symbol
polyfill if one is needed rather than expose one globally, andSymbol
declaration will existI 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 nativeSymbol
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).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
Symbol
object will be the globalSymbol
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 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
Iterable
interface in case one is not available globally to the consumer.But this generates the same error we saw above if no global
Symbol
declaration is present.The solution to this involves recreating the
SymbolConstructor
interface as well and declaring a globalSymbol
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:
@@iterable
,@@toStringTag
)symbol
type is available)symbol
when 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
Symbol
constructor.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
@@iterator
to 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 theSymbol
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 thansymbol
. A partial declaration file might look like this:And now we can use the local
Symbol.iterator
as 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
Iterable
interface. But she need not describe or rely on a globalSymbol
constructor declaration.Now type-checking is consistent irrespective of the existence of a global
Symbol
constructor declaration.The compiler throws the expected error regardless of the consumer's configuration:
The text was updated successfully, but these errors were encountered: