Typescript: String Literal types on es6 interfaces for [Symbol.toStringTag]?

Created on 7 Oct 2017  路  7Comments  路  Source: microsoft/TypeScript

TypeScript Version: 2.5.3

Code

class MySpecialMap implements Map<Object, Object> {
   private _map;
   readonly [Symbol.toStringTag] = "MySpecialmap";
   constructor() { this._map = new Map(); }
   // declare all other methods of Map methods with some unique behaviors that wrap calls to this._map. 
}

Expected behavior:

Classes implementing an ES6 interface can declare a distinct value for Symbol.toStringTag.

The goal of this property is to provide a distinct value, using a string literal type for the interface is overly specific.

Actual behavior:

src/MySpecialMap.ts(1,7): error TS2420: Class 'MySpecialMap' incorrectly implements interface 'Map<Object, Object>'.
  Types of property '[Symbol.toStringTag]' are incompatible.
    Type '"MySpecialMap"' is not assignable to type '"Map"'.
Bug lib.d.ts help wanted

Most helpful comment

This is also a problem for Bluebird: https://github.com/petkaantonov/bluebird/pull/1421

The overly strict Symbol.toStringTag effectively introduces a nominal type system for standard libraries.

We are between a rock and a hard place here. If we implement Symbol.toStringTag to have the value 'Promise', libraries using it to differentiate between native and Bluebird promises will break. If we return something different, the .d.ts needs to lie to the type system.

All 7 comments

There is actually no such thing as an es6 interface, interfaces are purely a type system feature and, as that system is structural, there is actually no need to declare that your class implements any interface at all. However, in this case, your class does not in fact implement the interface, structurally or otherwise, because the interface does indeed require that value for the tag. It sounds more like you want to extend map. Unfortunately that will only work reliably under --target es2015 or above.

@aluanhaddad yeah, I understand those things. sorry, if I wasn't clear or didn't use the correct nomenclature. I wanted to use the type system to ensure that the class I built implemented the Map interface. I didn't want to inherit the methods of that class -- I wanted to provide an implementation for them and I wanted to let the interface of that class guide that implementation and ensure compatibility if the type ever changes.

I'm not sure what you mean by "does indeed require that value for the tag" because this is typescript type, not something enforced by es6 and it would be incorrect for my class to declare that it is actually a"Map" when it's not according to the semantics of Symbol.toStringTag.

I tried to create an interface that extends map and provides a different value for toStringTag but this doesn't work.

interface MySpecialMap<Object, Object> extends Map<Object, Object> {
  readonly [Symbol.toStringTag] = "MySpecialMap";
}

yielding the same error:

src/MySpecialMap.ts(1,11): error TS2430: Interface 'MySpecialMap<Object>' incorrectly extends interface 'Map<Object, Object>'.
  Types of property '[Symbol.toStringTag]' are incompatible.
    Type '"MySpecialMap"' is not assignable to type '"Map"'.

If I declare that it's my class that extends Map instead I get different errors:

src/MySpecialMap.ts(5,7): error TS2415: Class 'MySpecialMap' incorrectly extends base class 'Map<Object, Object>'.
  Types of property '[Symbol.toStringTag]' are incompatible.
    Type '"MySpecialMap"' is not assignable to type '"Map"'.
src/MySpecialMap.ts(5,7): error TS2417: Class static side 'typeof MySpecialMap' incorrectly extends base class static side '{ readonly prototype: Map<any, any>; }'.
  Types of property 'prototype' are incompatible.
    Type 'MySpecialMap' is not assignable to type 'Map<any, any>'.
      Types of property '[Symbol.toStringTag]' are incompatible.
        Type '"MySpecialMap"' is not assignable to type '"Map"'.

I don't understand what benefit TypeScript is imbuing to the javascript developer by using string literal types for toStringTag values in its interface definitions of es6 classes. toStringTag shouldn't be used for case statements or instance checking. The goal is to make debugging easier, right? How does limiting the possible values of this property help debugging?

I think toStringTag should be of type string for all es6 interfaces so that developers can override it correctly.

(note: I am using --target es2015)

Yeah, I'm not sure what value we're providing by statically typing Symbol.toStringTag as some particular string. We should come up with a better fix to #5388 (ref: #202).

we should revert https://github.com/Microsoft/TypeScript/pull/6361. we tried to be too clever with the toStringTag and assumed no one will override it.

PRs welcomed.

This is also a problem for Bluebird: https://github.com/petkaantonov/bluebird/pull/1421

The overly strict Symbol.toStringTag effectively introduces a nominal type system for standard libraries.

We are between a rock and a hard place here. If we implement Symbol.toStringTag to have the value 'Promise', libraries using it to differentiate between native and Bluebird promises will break. If we return something different, the .d.ts needs to lie to the type system.

Was this page helpful?
0 / 5 - 0 ratings