-
Notifications
You must be signed in to change notification settings - Fork 12.6k
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
Challenges with ES6 symbols #2012
Comments
The entity approach may work better if we only support
|
@JsonFreeman to my mind, the challenge of ES6 symbol typing is analogous to the that of string literal types (discussed in #1003), both cases being useful examples of the general concept of 'singleton types'. In both cases, you have literal values that could usefully be treated as types by the compiler, for things like checking assignment compatibility, narrowing types in type guards, inferring types, etc. And in both cases you have the problem of the runtime system providing many ways for running code to violate the assumptions that the compiler is relying on for its checks. I'd love to see this approached as a special case of 'singleton types' more generally, so that static type checking could be enhanced with things like ES6 symbols (eg for more accurate indexer typing), const literal strings (eg for tagged unions), and const enum values (eg for use in type guards).
|
@rbuckton, the Problems 1 (aliasing) still remains, but to fix that, we might employ singleton types, as @yortus suggests. This would allow a value/type from one const symbol to flow to another const symbol. I also think it is pretty likely people will have a class/module that contains properties whose values are particular symbols. So we would want to allow this for export const, and possibly even static declarations on a class (although unfortunately there is no way to mark those as const). We still have to overcome problem 4, which is an implementation strategy issue really. @yortus, I think you are right at least in theory, and there does seem to be a beautiful parallel. Being able to express the types particular to certain symbols would make the system more powerful. I'd want to see it worked out for strings and const enum members first, as those are better understood. One complication with the singleton types approach it is very common to create a symbol using You also mention that it's a pity |
@JsonFreeman It makes me think of how some type systems (C++, C#, Java for example) provide special treatment for so-called compile-time constants. What each language considers to be a compile-time constant is subject to practical considerations about the kinds of language expressions the compiler is willing/able to evaluate at compile-time. In this light, one could think of the expression The same distinction applies to other constant expressions. For example, the expression // Compile-time constants
const sym1 = Symbol.iterator; // sym1 has singleton type with identity Symbol.iterator
const str1 = 'foo'; // str1 has singleton type with identity 'foo'
// Not compile-time constants
var sym2 = Symbol('foo'); // sym2 has type Symbol
var str2 = 'f' + 'o' + 'o'; // str2 has type string Maybe this distinction would help in tracking the types of expressions across loop iterations and function boundaries. References to compile-time constants can safely preserve their identity in loops and across functions. Everything else falls back to the existing type system, using the more general types like I have no idea how practical this would be given the existing compiler architecture. In theory at least, it just means having a way to recognise compile-time constants in the form of (a) literals such as |
@yortus, yes I like your way of explaining it. Tracking the constant value I think treating const sym = Symbol();
const sym2 = sym;
var obj = {
[sym]: 0
};
obj[sym2]; // type number But as soon as you need to do anything with sym, other than use it as a property key or reassign it to another const, its type is the generic TypeScript does actually have constant preserving expressions evaluated at compile time, but not in general. It is only for arithmetic operations in initializers of const enum members. So this infrastructure could in principle be extended to work for string concatenation as you suggest, and const assignment for symbols. I think this would be a good start, particularly if we can get this working for strings first. Although, I have a feeling people will expect symbols to be tracked in arbitrary object properties. I think that would be infeasible, as properties can be modified in very non-local ways. But the problem is reasonably scoped for const declarations, no pun intended. |
Agreed, with the combination of dynamic mutable objects and general control flow, information tying types to specific compile-time constant values is going to be lost very quickly, in all but some very specific circumstances. I guess the question is really about how much mileage the compiler can get from those very specific circumstances. Better type-level reasoning about well-known symbols and tagged unions would alone be huge wins IMHO. Everything else you've said makes this approach sound promising. What about problem 4 you mentioned up in the OP? |
const declarations do not have much risk for circularity, but there is still an architectural issue here. The main problem is that the compiler currently does name binding in one pass, but this would not work in the following case: function foo() {
var obj = {
[s]: 0
};
}
const s = Symbol(); We would essentially need to bind names breadth first instead of depth first. We would also need to pull the resolveName function out of the checker to make it "typeless" so that the binder could use it. This would be a very disruptive change. Perhaps @ahejlsberg knows more about what would be involved here. Anders, we are talking about what it would take to support user defined symbols (as well-typed property keys) if the symbols are bound to const declarations. We'd need the binder to have access to resolveName, and we'd need to make the binding breadth first so that the symbols are all available at the right time. |
Hello there, I would like to propose the following way of implementing this feature: Instead of special casing symbol Foo; On code generation this would be mapped to ES6's Symbols declared with this special syntax would then be statically verified and have strong typing guarantees, while symbols created manually with You would be able to export declare module "bar" {
export symbol Foo;
} Just like functions, their names would be bound and available regardless of declaration order. So @JsonFreeman's example above would be perfectly valid, and would become: function foo() {
var obj = {
[s]: 0
};
}
symbol s; Assigning TypeScript symbol s;
var s2 = s;
var foo: { [s]: number; }; // valid declaration
var bar: { [s2]: number; }; // invalid declaration The keyword var bar: symbol; |
@coreh, that is quite an interesting idea. One very nice thing about it is that users may have trouble internalizing that I would further propose that these declarations be block scoped (maybe that just follows because they are emitted as const). Another very important thing is that when they are returned from a function, they no longer retain their identity as a well typed symbol. That's because each function invocation will produce a new symbol. This still exhibits problem 4 explained above though. We would still need a resolveName that is divorced from the checker, and we'd still need to make the binding breadth first. |
Interesting, I hadn't thought about the use case where they are declared inside a function. If this adds a lot of complexity to the implementation though, I think it would be acceptable for
I'm not entirely familiar with the internal workings of the type checker but if I understand correctly, problem no. 4 would be caused by code like this? module A {
export function f(): { [B.s]: string } {
// ...
}
export symbol s;
}
module B {
export function f(): { [A.s]: string } {
// ...
}
export symbol s;
} |
Allowing symbol declarations only in modules would certainly make things simpler. Modules have the nice property that everything nested directly inside them happens exactly once, so execution and static entities are 1-to-1. For your example, are you trying to show a circular symbol reference? I think if we only allow them in modules, there can be no circular references because the symbols would be named with identifiers and not symbols. But I am not even sure that circularity is an issue at all, as I have yet to come up with an example myself. There are a number of architectural changes that would be required for this to be possible:
In principle these should all be doable, but they are all somewhat drastic and may interact with parts of the compilation process in ways that I'm not foreseeing. I think in some ways these concerns are more worrisome than the language design issues we have been discussing, as the latter can always be simplified by scoping the feature. |
After some more discussion, we have decided that it would be best to revisit this after seeing how people use this feature in practice in ES6. Given the discussion in this thread, seems like we are aware of what we think will be feasible, and where the challenges will be. Once we have key use cases in mind, I think we can apply this knowledge more purposefully. |
I'd like to outline some of the thoughts the team has had about supporting custom ES6 symbols. This is focused on the proposals we have come up with, as well as the challenges associated with them.
The motivation is something like this:
We have had two genres of proposal here, the entity approach and the type approach.
s
is the entity that the property key[s]
represents. Any key referring tos
is the same key, and any other way of referring to the value ofSymbol('hello')
is different.There are 4 general challenges with these approaches:
Issues 1, 2, and 3 are really just specific cases of a more general problem. Namely, there is no 1-to-1 correspondence between static entities in the code (declarations, expressions) and runtime values. And because symbol identity is based on runtime values, there needs to be a 1-to-1 correspondence with static entities.
I think the best antidote for these issues is to limit the support for symbol properties to situations where we know there is a 1-to-1 correspondence between values and static entities. There are two general approaches we can explore, which share this 1-to-1 philosophy. They both use the entity approach mentioned above as a base:
Obviously, neither of these are complete proposals, but this is intended to start a discussion directed at finding a solution that is both feasible and would satisfy users' needs.
The text was updated successfully, but these errors were encountered: