1

I'm trying to implement something like this but I'm not sure it's possible. I think Typescript only allows unique symbols, not global ones. Is this correct?

Is there a better way to achieve using global symbols?

// sample.d.ts
const mySymbol = Symbol.for('internal.symbol')

interface Sample{
    [mySymbol]: string
    a: number
    b: number
}

// sample.js
class SampleClass implements Sample {
    [mySymbol]: string
    a: number
    b: number

    constructor(a: number, b: number){
        this.a = a;
        this.b = b;
        this[mySymbol] = `${a}-${b}`
    }
}

let mySample = new SampleClass(1, 2)

Is there a way to accomplish this? mySymbol can (and ideally will) be a global symbol that will be used by other objects as well so it can be defined separately if that can be accomplished.

Ben Smith
  • 19,589
  • 6
  • 65
  • 93
tagyoureit
  • 1,276
  • 1
  • 10
  • 17

3 Answers3

1

Here is how I was able to accomplish this.

// misc.ts
export const mySymbol = Symbol.for('internal.symbol')

// sample.d.ts
import {mySymbol} from './misc'

export as namespace Sample
export = Sample
interface Sample{
    [mySymbol]: string
    a: number
    b: number
}

// sample.js
class SampleClass implements Sample {
    [mySymbol]: string
    a: number
    b: number

    constructor(a: number, b: number){
        this.a = a;
        this.b = b;
        this[mySymbol] = `${a}-${b}`
    }
}

let mySample = new SampleClass(1, 2)

Once mySymbol is imported into the declaration file it turns into a module. Hence it needs to be specifically exported with the export = Sample and export as namespace Sample. See sample module.d.ts.

tagyoureit
  • 1,276
  • 1
  • 10
  • 17
  • So this is what I said in my answer. Export the symbol and then import it into whatever file requires it. – Ben Smith May 28 '19 at 16:25
  • Yes, and thank you. I still had to struggle with the implementation a bit (code samples would be even better next time) so I documented it here for anyone else to recreate easily. – tagyoureit May 28 '19 at 21:50
0

You can export your symbol i.e.

export const mySymbol = Symbol.for('internal.symbol')

and then import it into whatever file requires it. This way you won't pollute the global scope and you can import it only when necessary.

Ben Smith
  • 19,589
  • 6
  • 65
  • 93
  • I was hoping to do this from a .d.ts file like type or interface. But it seems it has to be exported from a regular .ts file and then imported into the .d.ts? Is that right? – tagyoureit May 28 '19 at 02:52
  • @tagyoureit d.ts files are the product of compiling your TypeScript files, you should not be using them directly during development. – Ben Smith May 28 '19 at 09:30
  • .d.ts files are declaration files and pretty standard in TypeScript (https://www.typescriptlang.org/docs/handbook/declaration-files/introduction.html) – tagyoureit May 28 '19 at 14:03
  • Yes they are standard, but in practise when using a bundler such as webpack you don't need to use the d.ts files directly. See [here](https://stackoverflow.com/a/45142346/203371) also. I guess if you aren't using a bundler, I can see why you are asking the question. – Ben Smith May 28 '19 at 14:24
  • 1
    Correct, I'm not using Webpack and using the declaration files for consistency across different parts of my app. But your answer did help me get to something that works. I'll post that below. Thanks. – tagyoureit May 28 '19 at 15:29
0

... I think Typescript only allows unique symbols, not global ones. Is this correct?

All symbols are unique. That is invariant.

How to scope and access symbols is another question.

There are two ways to create symbols:

First way: Symbol(mnemonic?:string), e.g.

const x = Symbol('optional name')
const y = Symbol('optional name')
assert(x!==y) // pass

Every call to Symbol(...) creates a unique symbol. The mnemonic is just a convenience property for debugging, etc. Two symbols can have the same mnemonic without being the same symbol.

console.log(x.toString()) // 'Symbol(optional name)'
assert(x.toString()===y.toString()) // pass
assert(x!==y) // pass

When symbols are created in this way they only exist as long as they are referenced in user code - just like other objects, they can be garbage collected.

Second way: Symbol.for(globalKey:string), e.g.

In file 'x.js', with NO import/require statements at all

const x = Symbol.for('my.global.symbols.1')
export x

In file 'y.js', with NO import/require statements at all

const x = Symbol.for('my.global.symbols.1')
export y

In file 'z.js'

import {x} from './x'
import {y} from './y'
assert(x===y) // pass
const z = Symbol.for('my.global.symbols.1')
assert(x===z) // pass

In this case, a unique global symbol is created for each UNIQUE global key passed as the globalKey parameter to Symbol.for(globalKey:string) - from any file. The symbol instance is stored in opaque global space, as though there were an opaque global map:

Symbol.for(globalKey:string):symbol{
  if (globalSymbolMap.has(globalKey)
    return globalSymbolMap.get(globalKey)
  else{
    const s=Symbol(globalKey)
    globalSymbolMap.set(globalKey,s)
    return s
  }
}

(although that might not be how it is actually implemented).

Here is what MDN says about Symbol.for():

In contrast to Symbol(), the Symbol.for() function creates a symbol available in a global symbol registry list. Symbol.for() does also not necessarily create a new symbol on every call, but checks first if a symbol with the given key is already present in the registry. In that case, that symbol is returned. If no symbol with the given key is found, Symbol.for() will create a new global symbol.

About garbage collection for these globally managed symbols - I don't know which of the following are true:

  • When a globally managed symbol is no longer referenced by any user code (i.e., not including the reference from the opaque global 'virtual' map) then it may be garbage collected.

  • Once created, globally managed symbols remain in the opaque global 'virtual' map until end of program life.

From the perspective of user code 'logic', there would be no difference between the two - it's entirely an implementation issue. However, performance, including memory usage, would differ. My guess is that some garbage collection is enabled.

Craig Hicks
  • 2,199
  • 20
  • 35