tagyoureit
tagyoureit

Reputation: 1286

Can we use global symbols in Typescript Declaration files?

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.

Upvotes: 1

Views: 1091

Answers (3)

Craig  Hicks
Craig Hicks

Reputation: 2528

... 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.

Upvotes: 0

tagyoureit
tagyoureit

Reputation: 1286

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.

Upvotes: 1

Ben Smith
Ben Smith

Reputation: 20230

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.

Upvotes: 0

Related Questions