Closed wereHamster closed 3 years ago
Almost 5 years(
Please, implement this feature, I can't writing code normally..
Can participants provide actual examples in their use-cases?
I don't understand the protocol example and why it's not possible today.
Here's an example of StringConvertible
const intoString = Symbol("intoString")
/**
* Something that can be converted into a string.
*/
interface StringConvertible {
[intoString](): string;
}
/**
* Something that is adorable.
*/
class Dog implements StringConvertible {
[intoString](): string {
return "RUFF RUFF";
}
}
/**
* @see {https://twitter.com/drosenwasser/status/1102337805336768513}
*/
class FontDog implements StringConvertible {
[intoString](): string {
return "WOFF WOFF";
}
}
console.log(new Dog()[intoString]())
console.log(new FontDog()[intoString]())
Here's an example of Mappable
or Functor
(lack of higher-order type constructors aside):
const map = Symbol("map")
interface Mappable<T> {
[map]<U>(f: (x: T) => U): Mappable<U>
}
class MyCoolArray<T> extends Array<T> implements Mappable<T> {
[map]<U>(f: (x: T) => U) {
return this.map(f) as MyCoolArray<U>;
}
}
@DanielRosenwasser it seems like you're assuming all objects have an interface or are a class instance or are known in advance; using your last example, I should be able to install map
, say, onto any javascript object (or at least, an object whose type allows for any symbol to be added to it), which then makes it Mappable.
Installing a property (symbol or not) onto an object after the fact is part of a different feature request (often called "expando properties" or "expando types").
Lacking that, the type you'd need for a symbol index signature would provide very little as a TypeScript user, right? If I understand correctly, the type would need to be something like unknown
or just any
to be somewhat useful.
interface SymbolIndexable {
[prop: symbol]: any; // ?
}
In the case of protocols, it's generally a function, but sure, it could be unknown
.
What I need is the symbol (and bigint) equivalent of type O = { [k: string]: unknown }
, so I can represent an actual JS object (something that can have any kind of key) with the type system. I can narrow that later as needed, but the base type for a JS object would be { [k: string | bigint | symbol | number]: unknown }
, essentially.
Ah I think I see @DanielRosenwasser point. I currently have code with an interface like:
export interface Environment<T> {
[Default](tag: string): Intrinsic<T>;
[Text]?(text: string): string;
[tag: string]: Intrinsic<T>;
// TODO: allow symbol index parameters when typescript gets its shit together
// [tag: symbol]: Intrinsic<T>;
}
where Intrinsic<T>
is a function type, and I want to allow devs to define their own symbol properties on environments similar to strings, but insofar as you could add [Symbol.iterator]
, [Symbol.species]
or custom symbol properties to any interface, the index signature with symbols would incorrectly restrict any objects implementing these properties.
So what you’re saying is you can’t make the value type of indexing by symbol any more specific than any
? Could we somehow use the unique symbol
vs symbol
distinction to allow this? Like could we make the index signature a default for regular symbols and allow unique/well-known symbols to override the index type? Even if it weren’t typesafe, being able get/set properties by symbol indexes arbitrarily would be helpful.
The alternative would be having users extend the Environment interface themselves with their symbol properties, but this doesn’t provide any additional type safety insofar as users can type the Symbol as whatever on the object.
@DanielRosenwasser here a real example of my production code. A State reused in many places as a map and may accept key of atom (domained feature). Currently I need to add symbol support, but I get a lot of errors:
Anyway current behavior is incompatible with ES standard that is wrong.
One additional late-night thought I had regarding symbol types. Why isn’t this an error?
const foo = {
[Symbol.iterator]: 1,
}
JS expects all Symbol.iterator
properties to be a function which returns an iterator, and this object would break a lot of code if it was passed around in various places. If there were a way to globally define symbol properties for all objects, we could allow for specific symbol index signatures while also allowing global overrides. It would be typesafe, right?
I am also not understanding, why a use case would be needed here. This is an ES6 incompatibility, which should not exist in a language wrapping ES6.
In the past I posted my findings on how this could be fixed here in this thread and if this code is not lacking important checks or features I doubt it's more time consuming to integrate it into the codebase than continuing this discussion.
I just didn't do a pull request, because I do not know about Typescript's test framework or requirements and because I do not know whether changes in different files would be necessary to make this work in all cases.
So before continuing to invest time to read and write here, please check if adding the feature would be less time consuming. I doubt anyone would complain about it being in Typescript.
Apart from all that the general use case is if you want to map values onto arbitrary symbols. Or for compatibility with non-typed ES6 code.
Here's an example of a place I think this would be helpful: https://github.com/choojs/nanobus/pull/40/files. In practice, eventName
s can be symbols or strings, so I'd like to be able to say
type EventsConfiguration = { [eventName: string | Symbol]: (...args: any[]) => void }
on the first line.
But I might be misunderstanding something about how I should be doing this.
Simple use-case can't be done without pain:
type Dict<T> = {
[key in PropertyKey]: T;
};
function dict<T>() {
return Object.create(null) as Dict<T>;
}
const has: <T>(dict: Dict<T>, key: PropertyKey) => boolean = Function.prototype.call.bind(Object.prototype.hasOwnProperty);
function forEach<T>(dict: Dict<T>, callbackfn: (value: T, key: string | symbol, dict: Dict<T>) => void, thisArg?: any) {
for (const key in dict)
if (has(dict, key))
callbackfn.call(thisArg, dict[key], key, dict);
const symbols = Object.getOwnPropertySymbols(dict);
for (let i = 0; i < symbols.length; i++) {
const sym = symbols[i];
callbackfn.call(thisArg, dict[sym], sym, dict); // err
}
}
const d = dict<boolean>();
const sym = Symbol('sym');
const bi = 9007199254740991n;
d[1] = true;
d['x'] = true;
d[sym] = false; // definitely PITA
d[bi] = false; // another PITA
forEach(d, (value, key) => console.log(key, value));
I am also not understanding, why a use case would be needed here.
@neonit there are PRs to address this, but my understanding is that there are subtle issues with how the feature interacts with the rest of the type system. Lacking solutions to that, the reason I ask for use-cases is because we can't just work/focus on every feature at once that we'd like to - so use-cases need to justify work being done which includes long-term maintenance of a feature.
It seems that in fact, most people's imagined use cases won't be solved as easily as they imagine (see @brainkim's response here https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574550587), or that they're solved equally well via symbol properties (https://github.com/microsoft/TypeScript/issues/1863#issuecomment-574538121) or Maps (https://github.com/microsoft/TypeScript/issues/1863#issuecomment-572733050).
I think @Tyler-Murphy gave the best example here in that you can't write constraints, which can be very useful for something like a type-safe event emitter that supports symbols.
So before continuing to invest time to read and write here, please check if adding the feature would be less time consuming. I doubt anyone would complain about it being in Typescript.
This is always easier to say when you don't have to maintain the project! 😄 I understand that this is something useful for you, but I hope you respect that.
This is an ES6 incompatibility
There are plenty of constructs that TypeScript can't type easily because it would be infeasible. I'm not saying this is impossible, but I don't believe that this an appropriate way to frame this issue.
So it seems like the inability to add symbol keys as index signatures comes from the fact that there are global well-known Symbols which require their own typings, which symbol index types would inevitably clash with. As a solution, what if we had a global module/interface which represented all known symbols?
const Answerable = Symbol.for("Answerable");
declare global {
interface KnownSymbols {
[Answerable](): string | number;
}
}
interface MyObject {
[name: symbol]: boolean;
}
const MySymbol = Symbol.for("MySymbol");
const obj: MyObject = {
[MySymbol]: true,
};
obj[Answerable] = () => "42";
By declaring additional properties on the global KnownSymbols
interface, you allow all objects to be indexed by that symbol and restrict the value of the property to undefined/your value type. This would immediately provide value by allowing typescript to provide typings for the well-known symbols provided by ES6. Adding a Symbol.iterator
property to an object which is not a function which returns an iterator should clearly be an error, but it is not one currently in typescript. And it would make adding well-known symbol properties to already existing objects much easier.
This usage of a global module would also allow Symbols to be used as arbitrary keys as well, and therefore in index signatures. You would just give the global known symbol properties precedence over the local index signature type.
Would implementing this proposal allow index signature types to move forward?
Individual use-cases are irrelevant. If it's cromulent JavaScript, it needs to be expressible in TS definitions.
but my understanding is that there are subtle issues with how the feature interacts with the rest of the type system
More like "refactors how index signatures internally work entirely so is a scary big change and raises cromulet questions as to how index signatures are or should differ from mapped types that don't use the template variable" to be precise.
It mostly led to a discussion on how we fail to recognize closed types vs open types. In this context, a "closed" type would be a type with a finite set of keys whose values cannot be extended. The keys of a kind of exact type, if you will. Meanwhile an "open" type in this context is a type which, when subtyped, is open to having more keys added (which, under our current subtyping rules, sorta all types are mostly sometimes, except types with index signatures which very explicitly are). Index signatures imply making an open type, while mapped types are largely related as though they are operating over closed types. This usually works well enough because most code, in practice, is written with a structure compatible with closed object types. This is why flow
(which has explicit syntax for closed vs open object types) defaults to closed object types. This comes to a head with generic index keys; If I have a T extends string
, as T
is instantiated broader and broader types (from "a"
to "a" | "b"
to string
), the object produced is more and more specialized, right up until we swap over from "a" | "b" | ... (every other possible string)
to string
itself. Once that happens, suddenly the type is very open, and while every property may potentially exist to access, it becomes legal to, eg, assign an empty object to it. That's what happens structurally, but when we relate the generics in mapped types, we ignore that - a string
constraint on a generic mapped type key is essentially related as though it makes all possible keys exist. This logically follows from a simple variance-based view of the key type, but is only correct is the keys come from a closed type (which, ofc, a type with an index signature is never actually closed!). So, if we want to be backwards compatible, we can't treat {[x: T]: U}
the same as {[_ in T]: U}
, unless, ofc, we want to, since in the non-generic case {[_ in T]: U}
becomes {[x: T]: U}
, adjust how we handle the variance of mapped type keys to properly account for the open type "edge", which is an interesting change in its own right that could have ecosystem ramifications.
Pretty much: Because it brings mapped types and index signatures much closer together, it raised a bunch of questions on how we handle both of them that we don't have satisfactory or conclusive answers to yet.
Individual use-cases are irrelevant.
This is, politely, pure madness. How in tarnation do we know whether or not we're adding a feature with the behavior people want without use cases by which to judge that behavior?
We're not trying to be difficult here by asking these questions; we are literally trying to ensure that we implement the things people are asking for. It would be a true shame if we implemented something we thought was "indexing with symbols", only to have the very same people in this thread come back and say that we did it totally wrong because it didn't address their particular use cases.
You're asking us to fly blind. Please don't! Please tell us where you would like the plane to go!
My bad, I could've been clearer about what I meant; it seemed to me that people felt they had to justify their actual code use-cases, rather than their desire to describe it more accurately via TS
So if I understand it correctly, this is mainly about the following problem:
const sym = Symbol();
interface Foo
{
[sym]: number;
[s: symbol]: string; // just imagine this would be allowed
}
Now the Typescript compiler would see this as a conflict, because Foo[sym]
has an ambivalent type. We have the same issue with strings already.
interface Foo
{
['str']: number; // <-- compiler error: not assignable to string index type 'string'
[s: string]: string;
}
The way this is handled with string indices is that specific string indices are just not allowed, if there is a general specification for string keys and their type is incompatible.
I guess for symbols this would be an omnipresent issue, because ECMA2015 defines standard symbols like Symbol.iterator
, which can be used on any object and thus should have a default typing. Which they oddly do not have apparently. At least the playground does not allow me to run the Symbol.iterator
example from MDN.
Assuming it is planned to add predefined symbol typings it would always lead to a general [s: symbol]: SomeType
definition to be invalid, because the predefined symbol indices already have incompatible types so there cannot exist a common general type or maybe it would need to be a function
type, because most (/all?) predefined symbol keys are of function
type.
A problem with the mix of general and specific index types is the type determination when the object is indexed with a value not known at compile time. Imagine my above example with the string indices would be valid, then the following would be possible:
const foo: Foo = {str: 42, a: 'one', b: 'two'};
const input: string = getUserInput();
const value = foo[input];
The same problem would apply to symbol keys. It is impossible to determine the exact type of value
at compile-time. If the user inputs 'str'
, it would be number
, otherwise it would be string
(at least Typescript would expect it to be a string
, whereas it likely may become undefined
). Is this the reason we do not have this feature? One could workaround this by giving value
a union type containing all possible types from the definition (in this case number | string
).
@Neonit Well, that's not the issue that's halted progress on an implementation, but that's exactly one of the issues that I waws trying to point out - that depending on what you're trying to do, symbol indexers might not be the answer.
If this feature was implemented, ECMAScript's built-in symbols wouldn't necessarily ruin everything because not every type uses those symbols; but any type that does define a property with a well-known symbol (or any symbol that you yourself define) would likely be limited to a less-useful index signature for symbols.
That's really the the thing to keep in mind - the "I want to use this as a map" and the "I want to use symbols to implement protocols" use-cases are incompatible from a type-system perspective. So if you had anything like that in mind, then symbol index signatures might not help you, and you might be better-served via a explicit symbol properties or maps.
What about something like a UserSymbol
type that's just symbol
minus built-in symbols? The very nature of symbols ensures there won't ever be accidental collisions.
Edit: Thinking about this more, well-known symbols are just sentinels that happen to be implemented using Symbol
. Unless the goal is object serialization or introspection, code probably should treat these sentinels different from other symbols, because they have special meaning to the language. Removing them from the symbol
type will likely make (most) code using 'generic' symbols more safe.
@RyanCavanaugh here's my flight plan.
I have a system in which I use symbols like this for properties.
const X = Symbol.for(":ns/name")
const txMap = {
[X]: "fly away with me!"
}
transact(txMap) // what's the index signature here?
In this case I want the txMap
to fit the type signature of transact
. But to my knowledge I cannot express this today. In my case, transact
is part of a library that doesn't know what to expect. I do something like this for properties.
// please forgive my tardiness but in essence this is how I'm typing "TxMap" for objects
type TxMapNs = { [ns: string]: TxMapLocal }
type TxMapLocal = { [name: string]: string | TxMapNs } // leaf or non leaf
I can generate the set of types that fit transact
from schema and use that. For that I'd do something like this and rely on declaration merging.
interface TxMap = {
[DB_IDENT]: symbol // leaf
[DB_VALUE_TYPE]?: TxMap // not leaf
[DB_CARDINALITY]?: TxMap
}
But it would be nice if I could at least fallback to an index signature for symbols, I only expect transact
to be handed plain JavaScript objects, I also only use symbols from the global symbol registry in this case. I do not use private symbols.
I should add that this is a bit of a pain.
const x = Symbol.for(":x");
const y = Symbol.for(":x");
type X = { [x]: string };
type Y = { [y]: string };
const a: X = { [x]: "foo" };
const b: Y = { [x]: "foo" }; // not legal
const c: X = { [y]: "foo" }; // not legal
const d: Y = { [y]: "foo" };
It would be super awesome if TypeScript could understand that symbols created via the Symbol.for
function actually are the same.
This is also super annoying.
function keyword(ns: string, name: string): unique symbol { // not possible, why?
return Symbol.for(":" + ns + "/" + name)
}
const x: unique symbol = keyword("db", "id") // not possible, why?
type X = {
[x]: string // not possible, why?
}
That little utility function let's me enforce a convention over my global symbol table. however, I cannot return a unique symbol
, even if it is created via the Symbol.for
function. Because of the way TypeScript it doing things, it is forcing me to forgo certain solutions. They just don't work. And I think that's sad.
I've come across another use case where symbol
as a indexing value would be useful, when working with ES Proxies to create a factory function that wraps an object with a proxy.
Take this example:
let original = {
foo: 'a',
bar: 'b',
baz: 1
};
function makeProxy<T extends Object>(source: T) {
return new Proxy(source, {
get: function (target, prop, receiver) {
return target[prop];
}
});
}
let proxied = makeProxy(original);
To match up the ProxyConstructor
type signature the generic argument must extend Object
, but that then errors because the generic argument isn't keyed. So we can extend the type signature:
function makeProxy<T extends Object & { [key: string]: any}>(source: T) {
But now it'll raise an error because the 2nd argument (prop
) of get
on ProxyHandler
is of type PropertyKey
which so happens to be PropertyKey
.
So I'm unsure how to do this with TypeScript due to the restrictions of this issue.
@aaronpowell What is the problem you're facing? I see it is behaving fine:
let original = {
foo: 'a',
bar: 'b',
baz: 1
};
function makeProxy<T extends Object>(source: T) {
return new Proxy(source, {
get: function (target, prop, receiver) {
return target[prop];
}
});
}
let proxied = makeProxy(original);
function assertString(s:string){}
function assertNumber(x:number){}
assertString(proxied.foo); // no problem as string
assertNumber(proxied.baz); // no problem as number
console.log(proxied.foobar); // fails as expected: error TS2339: Property 'foobar' does not exist on type '{ foo: string; bar: string; baz: number; }'.
tsconfig.json:
{
"compilerOptions": {
"module": "esnext",
"moduleResolution": "node",
"target": "es2015"
}
package.json:
{
"devDependencies": {
"typescript": "~3.4.5"
}
}
@beenotung I see an error in the playground:
@aaronpowell the error appear when you enable 'strict' flag in the 'compilerOptions' in tsconfig.json
.
So under the current version of typescript compiler, you've to either turn off the strict mode or cast the target into any
...
Sure, but an any
cast isn't really ideal and disabling strict mode is just loosening restrictions in the type safety.
Reading messages i imagine the next "solution" will probably be to "disable typescript".
We shouldn't have to search for stopgap solutions neither have to explain why we need it.
It's a standard feature of javascript so we need it in typescript.
@DanielRosenwasser my use case is similar to that of @aaronpowell - a seeming mismatch in the ProxyHandler
interface and TypeScript's rules preventing me from properly typing proxy handler traps.
A boiled down example demonstrating the issue:
const getValue = (target: object, prop: PropertyKey) => target[prop]; // Error
As far as I can tell, it's impossible to craft any type for target
that will avert the error yet allow only objects that can legitimately be accessed by PropertyKey
.
I'm a TypeScript newbie so please forgive me if I'm missing something obvious.
Another use case: I'm trying to have a type {[tag: symbol]: SomeSpecificType}
for callers to provide a map of tagged values of a specific type in a way that benefits from the compactness of object literal syntax (while still avoiding the name clash risks of using plain strings as tags).
Another use case: I'm trying to iterate all enumerable properties of an object, symbols and strings both. My current code looks something like this (names obscured):
type ContextKeyMap = Record<PropertyKey, ContextKeyValue>
function setFromObject(context: Context, object: ContextKeyMap) {
for (const key in object) {
if (hasOwn.call(object, key)) context.setKey(key, object[key])
}
for (const symbol of Object.getOwnPropertySymbols(object)) {
if (propertyIsEnumerable.call(object, symbol)) {
context.setKey(symbol, object[symbol as unknown as string])
}
}
}
I'd very strongly prefer to be able to just do this:
type ContextKeyMap = Record<PropertyKey, ContextKeyValue>
function setFromObject(context: Context, object: ContextKeyMap) {
for (const key in object) {
if (hasOwn.call(object, key)) context.setKey(key, object[key])
}
for (const symbol of Object.getOwnPropertySymbols(object)) {
if (propertyIsEnumerable.call(object, symbol)) {
context.setKey(symbol, object[symbol])
}
}
}
I have also issue with indexing with symbols. My code is as follow:
const cacheProp = Symbol.for('[memoize]')
function ensureCache<T extends any>(target: T, reset = false): { [key in keyof T]?: Map<any, any> } {
if (reset || !target[cacheProp]) {
Object.defineProperty(target, cacheProp, {
value: Object.create(null),
configurable: true,
})
}
return target[cacheProp]
}
I followed the solution by @aaronpowell and somehow managed to workaround it
const cacheProp = Symbol.for('[memoize]') as any
function ensureCache<T extends Object & { [key: string]: any}>(target: T, reset = false): { [key in keyof T]?: Map<any, any> } {
if (reset || !target[cacheProp]) {
Object.defineProperty(target, cacheProp, {
value: Object.create(null),
configurable: true,
})
}
return target[cacheProp]
}
Casting to any
from symbol
isn't so nice indeed.
Really appreciated for any other solutions.
@ahnpnl For that use case, you'd be better off using a WeakMap
than symbols, and engines would optimize that better - it doesn't modify target
's type map. You may still have to cast it, but your cast would live in the return value.
A workaround is to use generic function to assign value ...
var theAnswer: symbol = Symbol('secret');
var obj = {} as Record<symbol, number>;
obj[theAnswer] = 42; // Currently error, but should be allowed
Object.assign(obj, {theAnswer: 42}) // allowed
A workaround is to use generic function to assign value ...
var theAnswer: symbol = Symbol('secret'); var obj = {} as Record<symbol, number>; obj[theAnswer] = 42; // Currently error, but should be allowed Object.assign(obj, {theAnswer: 42}) // allowed
I'm not agree. These three lines are equal to each other:
Object.assign(obj, {theAnswer: 42});
Object.assign(obj, {'theAnswer': 42});
obj['theAnswer'] = 42;
@DanielRosenwasser
I have this use case, In the playground link, I also solved it by using maps, but take a look, its ugly.
const system = Symbol('system');
const SomeSytePlugin = Symbol('SomeSytePlugin')
/** I would prefer to have this working in TS */
interface Plugs {
[key: symbol]: (...args: any) => unknown;
}
const plugins = {
"user": {} as Plugs,
[system]: {} as Plugs
}
plugins[system][SomeSytePlugin] = () => console.log('awsome')
plugins[system][SomeSytePlugin](); ....
Using symbols here rules out the possible accidental overwrite that happens when using strings. It makes the whole system more robust and easier to maintain.
If you have an alternative solution that works with TS and has the same readability in the code, I'm all ears.
Any official explain for this issue?
A workaround is to use generic function to assign value ...
var theAnswer: symbol = Symbol('secret'); var obj = {} as Record<symbol, number>; obj[theAnswer] = 42; // Currently error, but should be allowed Object.assign(obj, {theAnswer: 42}) // allowed
You're looking for
Objet.assign(obj, { [theAnswer]: 42 });
However there isn't a way to read see comment two belowx[theAnswer]
back out without a cast AFAIK
For the love of God, please make this a priority.
You're looking for
Objet.assign(obj, { [theAnswer]: 42 });
However there isn't a way to read
x[theAnswer]
back out without a cast AFAIK
As pointed out by mellonis and MingweiSamuel, the workarounds using generic function are:
var theAnswer: symbol = Symbol("secret");
var obj = {} as Record<symbol, number>;
obj[theAnswer] = 42; // Not allowed, but should be allowed
Object.assign(obj, { [theAnswer]: 42 }); // allowed
function get<T, K extends keyof T>(object: T, key: K): T[K] {
return object[key];
}
var value = obj[theAnswer]; // Not allowed, but should be allowed
var value = get(obj, theAnswer); // allowed
Five years and Symbol as index still not allowed
Found a work-around on this case, it not generic but work in some case:
const SYMKEY = Symbol.for('my-key');
interface MyObject { // Original object interface
key: string
}
interface MyObjectExtended extends MyObject {
[SYMKEY]?: string
}
const myObj: MyObject = {
'key': 'value'
}
// myObj[SYMKEY] = '???' // Not allowed
function getValue(obj: MyObjectExtended, key: keyof MyObjectExtended): any {
return obj[key];
}
function setValue(obj: MyObjectExtended, key: keyof MyObjectExtended, value: any): void {
obj[key] = value
}
setValue(myObj, SYMKEY, 'Hello world');
console.log(getValue(myObj, SYMKEY));
@james4388 How is your example any different from the one from @beenotung?
FYI: https://github.com/microsoft/TypeScript/pull/26797
(Just found it - I'm not actually part of the TS team.)
I agree that if this is safe:
const keepAs = <
R extends Record<string, unknown>,
From extends keyof R,
M extends Record<string, From>,
To extends keyof M,
S extends { [P in To]: R[M[P]] }
>(
a: R[],
m: M,
): S[] => a.map((r) => Object.assign({}, ...Object.entries(m).map(([k, v]) => ({ [k]: r[v] }))));
then the analogous
const keepAs2 = <
R extends Record<PropertyKey, unknown>,
From extends keyof R,
M extends Record<PropertyKey, From>,
To extends keyof M,
S extends { [P in To]: R[M[P]] }
>(
a: R[],
m: M,
): S[] => a.map((r) => Object.assign({}, ...Reflect.ownKeys(m).map((k) => ({ [k]: r[m[k]] }))));
is also safe
My poor workaround for some cases :
const bar: Record<any, string> = {};
const FOO = Symbol('foo');
// eslint-disable-next-line @typescript-eslint/no-explicit-any
const aFOO = FOO as any;
bar[aFOO] = 'sad';
@Gnucki It’s probably better to do, since the as any
type assertion gets removed during compilation:
const bar: Record<any, string> = {};
const FOO = Symbol('foo');
bar[
// eslint-disable-next-line @typescript-eslint/no-explicit-any
FOO as any
] = 'sad';
which compiles to:
const bar = {};
const FOO = Symbol('foo');
bar[FOO] = 'sad';
Whereas your code compiles to:
const bar = {};
const FOO = Symbol('foo');
const aFoo = FOO as any;
bar[aFOO] = 'sad';
which causes the local DeclarativeEnvironmentRecord to have two const
bindings pointing to the same Symbol value.
Is there any explanation of this from typescript maintainers team?
I can't see any reason why this is not handled.
This is very useful when creating 3 party libraries.
Without this fix we'll have to use @ts-ignore
comments all over the place
I'll reiterate my suggestion to make a TS language-level distinction between user-instantiated symbols and 'well-known' JS symbols.
Quoting myself from earlier:
Well-known symbols are just sentinels that happen to be implemented using
Symbol
. Unless the goal is object serialization or introspection, code probably should treat these sentinels different from other symbols, because they have special meaning to the language. Removing them from thesymbol
type will likely make (most) code using 'generic' symbols more safe.
@riggs that would not fit a number of use cases; it's absolutely critical that everything for which typeof x === 'symbol'
is in the symbol
type, otherwise APIs can't be typed properly.
Given that TypeScript is a superset of JS created for the explicit purpose of type safety, create two new types, LanguageSymbol
and Sentinel
:
LanguageSymbol
is the union of the 'well-known' static properties on Symbol
.Sentinel
is every Symbol that isn't a LanguageSymbol
.Symbol remains untouched, but maybe gets a flag warning about direct usage in a future version.
Sentinels are, by definition, freed from the concerns relating to well-known symbols. Preserving this behavior at runtime does incur a small overhead, but would likely only occur when doing introspection, at which point extra care is likely warranted. I suspect most code doing runtime type checks for symbols are expecting to not be dealing with a LanguageSymbol
in the first place.
TypeScript now has a ES6 target mode which includes definitions
Symbol
. However when trying to index an object with a symbol, I get an error (An index expression argument must be of type 'string', 'number', or 'any').