Skip to content

Commit 1c5bd38

Browse files
nyteshadeBrielle Harrison
authored and
Brielle Harrison
committed
Add support for Symbol.toStringTag
**Changes** * Add static and instance property getters for Symbol.toStringTag for each exported class. **Purpose** Being able to compare class and class instances via internal class type as per the definition and usage of Symbol.toStringTag allows other libraries to validate types during runtime in this manner. It also prevents them from having to patch the live values in their own codebases. **Contrived Example** With no modules and vanilla JavaScript we should be able to do something like the following. ```javascript let type = new GraphQLObjectType({name: 'Sample'}); if (({}).toString.call(type) === '[object GraphQLObjectType]') { // we have the right type of class } ``` However, with libraries such as `type-detect` or `ne-types` the code can look far cleaner. ```javascript // type-detect let type = require('type-detect') let obj = new GraphQLObjectType({name:'Example'}) assert(type(obj) === GraphQLObjectType.name) // ne-types let { typeOf } = require('ne-types') let obj = new GraphQLObjectType({name:'Example'}) assert(typeOf(obj) === GraphQLObjectType.name) ``` There are a lot of libraries out there, despite doing nearly the same thing in all cases, that support the usage of `Symbol.toStringTag` and by adding support for that in the base GraphQL classes, all of these libraries can be used with GraphQL.
1 parent 0a30b62 commit 1c5bd38

File tree

8 files changed

+8648
-0
lines changed

8 files changed

+8648
-0
lines changed

package-lock.json

Lines changed: 8370 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.
Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*/
7+
8+
import { expect } from 'chai';
9+
import { describe, it } from 'mocha';
10+
import { hasSymbolSupport, applyToStringTag } from '../symbolSupport';
11+
12+
describe('symbolSupportTests', () => {
13+
// NOTE Symbol appeared in nodejs in 0.12.18 but was largely unusable in
14+
// that format. Symbol.toStringTag showed up in node 6.4.0, but was available
15+
// behind a flag as early as 4.9.1.
16+
const [, major, minor, patch] = /^v(\d+)\.?(\d+)?\.?(\d+)?/.exec(
17+
process.version,
18+
);
19+
20+
it('should have Symbol in scope if the version >= 4.9.1', () => {
21+
expect(major >= 4).to.equal(true);
22+
23+
if (major === 4) {
24+
expect(minor >= 9).to.equal(true);
25+
}
26+
27+
if (minor === 9) {
28+
expect(patch >= 1).to.equal(true);
29+
}
30+
31+
expect(hasSymbolSupport()).to.equal(true);
32+
});
33+
34+
it('should have Symbol in scope if the version >= 6.4.0', () => {
35+
expect(major >= 6).to.equal(true);
36+
37+
if (major === 6) {
38+
expect(minor >= 4).to.equal(true);
39+
}
40+
41+
if (minor === 4) {
42+
expect(patch >= 0).to.equal(true);
43+
}
44+
45+
expect(typeof Symbol !== 'undefined').to.equal(true);
46+
expect(hasSymbolSupport('toStringTag')).to.equal(true);
47+
});
48+
49+
it('should be able to apply toStringTag to a class', () => {
50+
class A {}
51+
applyToStringTag(A);
52+
53+
const a = new A();
54+
55+
expect(Object.prototype.toString.call(a)).to.equal('[object A]');
56+
expect(Object.prototype.toString.call(A)).not.to.equal('[object A]');
57+
});
58+
});

src/jsutils/symbolSupport.js

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* Copyright (c) 2015-present, Facebook, Inc.
3+
*
4+
* This source code is licensed under the MIT license found in the
5+
* LICENSE file in the root directory of this source tree.
6+
*
7+
* @flow strict
8+
*/
9+
10+
/**
11+
* A function that can either simply determine if `Symbol`s are allowed as
12+
* well as, optionally, determine whether or not a property of symbol
13+
* exists.
14+
*
15+
* If the name of a specific `Symbol` property is supplied, the resulting
16+
* value will only be true if property is mapped to an instance of `Symbol`
17+
* such as `.toStringTag`, `.iterator`, `.species`, `.isConcatSpreadable`,
18+
* etc...
19+
*
20+
* @method hasSymbolSupport
21+
*
22+
* @param {string} specificSymbol an optional name of a property on the
23+
* `Symbol` class itself that statically refers to a predefined `Symbol`. If
24+
* properly specified and the value is set, then true will be returned.
25+
* @return {bool} true if `Symbol` is both defined and a function. Optionally
26+
* true if the `Symbol` class is true, a predefined symbol name such as
27+
* `toStringTag` is set on the `Symbol` class and it maps to an instance of
28+
* `Symbol`. False in all other cases
29+
*/
30+
export function hasSymbolSupport(specificSymbol?: string): boolean {
31+
const hasSymbols: boolean = typeof Symbol === 'function';
32+
33+
if (!hasSymbols) {
34+
return false;
35+
}
36+
37+
if (specificSymbol) {
38+
// NOTE: The capture of type and string comparison over a few lines is
39+
// necessary to appease the lint and flowtype gods.
40+
//
41+
// ((typeof Symbol[specificSymbol]): string) !== 'symbol' makes lint angry
42+
// and typeof Symbol[specificSymbol] !== 'symbol' makes flow angry
43+
//
44+
// The former thinks everything is too verbose the later thinks I am
45+
// comparing Symbol instance rather than the string resulting from a call
46+
// to typeof.
47+
//
48+
// Le sigh....
49+
const type: string = typeof Symbol[specificSymbol];
50+
51+
if (type !== 'symbol') {
52+
return false;
53+
}
54+
}
55+
56+
return true;
57+
}
58+
59+
/**
60+
* The `applyToStringTag()` function checks first to see if the runtime
61+
* supports the `Symbol` class and then if the `Symbol.toStringTag` constant
62+
* is defined as a `Symbol` instance. If both conditions are met, the
63+
* Symbol.toStringTag property is defined as a getter that returns the
64+
* supplied class constructor's name.
65+
*
66+
* @method applyToStringTag
67+
*
68+
* @param {Class<*>} classObject a class such as Object, String, Number but
69+
* typically one of your own creation through the class keyword; `class A {}`,
70+
* for example.
71+
*/
72+
export function applyToStringTag(classObject: Class<*>): void {
73+
if (hasSymbolSupport('toStringTag')) {
74+
Object.defineProperty(classObject.prototype, Symbol.toStringTag, {
75+
get() {
76+
return this.constructor.name;
77+
},
78+
});
79+
}
80+
}

src/language/source.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
*/
99

1010
import invariant from '../jsutils/invariant';
11+
import { applyToStringTag } from '../jsutils/symbolSupport';
1112

1213
type Location = {
1314
line: number,
@@ -41,3 +42,6 @@ export class Source {
4142
);
4243
}
4344
}
45+
46+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
47+
applyToStringTag(Source);
Lines changed: 109 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,109 @@
1+
import { describe, it } from 'mocha';
2+
import { expect } from 'chai';
3+
import {
4+
GraphQLDirective,
5+
GraphQLEnumType,
6+
GraphQLInputObjectType,
7+
GraphQLInterfaceType,
8+
GraphQLObjectType,
9+
GraphQLScalarType,
10+
GraphQLSchema,
11+
GraphQLUnionType,
12+
Source,
13+
} from '../../';
14+
15+
function typeOf(object) {
16+
return /(\b\w+\b)\]/.exec(Object.prototype.toString.call(object))[1];
17+
}
18+
19+
describe('Check to see if Symbol.toStringTag is defined on types', () => {
20+
const s = Symbol.toStringTag;
21+
const hasSymbol = o => Object.getOwnPropertySymbols(o).includes(s);
22+
23+
it('GraphQLDirective should have Symbol.toStringTag', () => {
24+
expect(hasSymbol(GraphQLDirective.prototype)).to.equal(true);
25+
});
26+
27+
it('GraphQLEnumType should have Symbol.toStringTag', () => {
28+
expect(hasSymbol(GraphQLEnumType.prototype)).to.equal(true);
29+
});
30+
31+
it('GraphQLInputObjectType should have Symbol.toStringTag', () => {
32+
expect(hasSymbol(GraphQLInputObjectType.prototype)).to.equal(true);
33+
});
34+
35+
it('GraphQLInterfaceType should have Symbol.toStringTag', () => {
36+
expect(hasSymbol(GraphQLInterfaceType.prototype)).to.equal(true);
37+
});
38+
39+
it('GraphQLObjectType should have Symbol.toStringTag', () => {
40+
expect(hasSymbol(GraphQLObjectType.prototype)).to.equal(true);
41+
});
42+
43+
it('GraphQLScalarType should have Symbol.toStringTag', () => {
44+
expect(hasSymbol(GraphQLScalarType.prototype)).to.equal(true);
45+
});
46+
47+
it('GraphQLSchema should have Symbol.toStringTag', () => {
48+
expect(hasSymbol(GraphQLSchema.prototype)).to.equal(true);
49+
});
50+
51+
it('GraphQLUnionType should have Symbol.toStringTag', () => {
52+
expect(hasSymbol(GraphQLUnionType.prototype)).to.equal(true);
53+
});
54+
55+
it('Source should have Symbol.toStringTag', () => {
56+
expect(hasSymbol(Source.prototype)).to.equal(true);
57+
});
58+
});
59+
60+
describe('Check to see if Symbol.toStringTag tests on instances', () => {
61+
// variables _interface and _enum have preceding underscores due to being
62+
// reserved keywords in JavaScript
63+
64+
const schema = Object.create(GraphQLSchema.prototype);
65+
const scalar = Object.create(GraphQLScalarType.prototype);
66+
const object = Object.create(GraphQLObjectType.prototype);
67+
const _interface = Object.create(GraphQLInterfaceType.prototype);
68+
const union = Object.create(GraphQLUnionType.prototype);
69+
const _enum = Object.create(GraphQLEnumType.prototype);
70+
const inputType = Object.create(GraphQLInputObjectType.prototype);
71+
const directive = Object.create(GraphQLDirective.prototype);
72+
const source = Object.create(Source.prototype);
73+
74+
it('should return the class name for GraphQLSchema instance', () => {
75+
expect(typeOf(schema)).to.equal(GraphQLSchema.name);
76+
});
77+
78+
it('should return the class name for GraphQLScalarType instance', () => {
79+
expect(typeOf(scalar)).to.equal(GraphQLScalarType.name);
80+
});
81+
82+
it('should return the class name for GraphQLObjectType instance', () => {
83+
expect(typeOf(object)).to.equal(GraphQLObjectType.name);
84+
});
85+
86+
it('should return the class name for GraphQLInterfaceType instance', () => {
87+
expect(typeOf(_interface)).to.equal(GraphQLInterfaceType.name);
88+
});
89+
90+
it('should return the class name for GraphQLUnionType instance', () => {
91+
expect(typeOf(union)).to.equal(GraphQLUnionType.name);
92+
});
93+
94+
it('should return the class name for GraphQLEnumType instance', () => {
95+
expect(typeOf(_enum)).to.equal(GraphQLEnumType.name);
96+
});
97+
98+
it('should return the class name for GraphQLInputObjectType instance', () => {
99+
expect(typeOf(inputType)).to.equal(GraphQLInputObjectType.name);
100+
});
101+
102+
it('should return the class name for GraphQLDirective instance', () => {
103+
expect(typeOf(directive)).to.equal(GraphQLDirective.name);
104+
});
105+
106+
it('should return the class name for Source instance', () => {
107+
expect(typeOf(source)).to.equal(Source.name);
108+
});
109+
});

src/type/definition.js

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
* @flow strict
88
*/
99

10+
import { applyToStringTag } from '../jsutils/symbolSupport';
1011
import instanceOf from '../jsutils/instanceOf';
1112
import invariant from '../jsutils/invariant';
1213
import isInvalid from '../jsutils/isInvalid';
@@ -586,6 +587,9 @@ export class GraphQLScalarType {
586587
inspect: () => string;
587588
}
588589

590+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
591+
applyToStringTag(GraphQLScalarType);
592+
589593
// Also provide toJSON and inspect aliases for toString.
590594
GraphQLScalarType.prototype.toJSON = GraphQLScalarType.prototype.inspect =
591595
GraphQLScalarType.prototype.toString;
@@ -688,6 +692,9 @@ export class GraphQLObjectType {
688692
inspect: () => string;
689693
}
690694

695+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
696+
applyToStringTag(GraphQLObjectType);
697+
691698
// Also provide toJSON and inspect aliases for toString.
692699
GraphQLObjectType.prototype.toJSON = GraphQLObjectType.prototype.inspect =
693700
GraphQLObjectType.prototype.toString;
@@ -937,6 +944,9 @@ export class GraphQLInterfaceType {
937944
inspect: () => string;
938945
}
939946

947+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
948+
applyToStringTag(GraphQLInterfaceType);
949+
940950
// Also provide toJSON and inspect aliases for toString.
941951
GraphQLInterfaceType.prototype.toJSON = GraphQLInterfaceType.prototype.inspect =
942952
GraphQLInterfaceType.prototype.toString;
@@ -1016,6 +1026,9 @@ export class GraphQLUnionType {
10161026
inspect: () => string;
10171027
}
10181028

1029+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
1030+
applyToStringTag(GraphQLUnionType);
1031+
10191032
// Also provide toJSON and inspect aliases for toString.
10201033
GraphQLUnionType.prototype.toJSON = GraphQLUnionType.prototype.inspect =
10211034
GraphQLUnionType.prototype.toString;
@@ -1131,6 +1144,9 @@ export class GraphQLEnumType /* <T> */ {
11311144
inspect: () => string;
11321145
}
11331146

1147+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
1148+
applyToStringTag(GraphQLEnumType);
1149+
11341150
// Also provide toJSON and inspect aliases for toString.
11351151
GraphQLEnumType.prototype.toJSON = GraphQLEnumType.prototype.inspect =
11361152
GraphQLEnumType.prototype.toString;
@@ -1264,6 +1280,9 @@ export class GraphQLInputObjectType {
12641280
inspect: () => string;
12651281
}
12661282

1283+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
1284+
applyToStringTag(GraphQLInputObjectType);
1285+
12671286
// Also provide toJSON and inspect aliases for toString.
12681287
GraphQLInputObjectType.prototype.toJSON =
12691288
GraphQLInputObjectType.prototype.toString;

src/type/directives.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ import type {
1313
} from './definition';
1414
import { GraphQLNonNull } from './definition';
1515
import { GraphQLString, GraphQLBoolean } from './scalars';
16+
import { applyToStringTag } from '../jsutils/symbolSupport';
1617
import instanceOf from '../jsutils/instanceOf';
1718
import invariant from '../jsutils/invariant';
1819
import type { DirectiveDefinitionNode } from '../language/ast';
@@ -76,6 +77,9 @@ export class GraphQLDirective {
7677
}
7778
}
7879

80+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
81+
applyToStringTag(GraphQLDirective);
82+
7983
export type GraphQLDirectiveConfig = {
8084
name: string,
8185
description?: ?string,

src/type/schema.js

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ import {
3232
} from './directives';
3333
import type { GraphQLError } from '../error/GraphQLError';
3434
import { __Schema } from './introspection';
35+
import { applyToStringTag } from '../jsutils/symbolSupport';
3536
import find from '../jsutils/find';
3637
import instanceOf from '../jsutils/instanceOf';
3738
import invariant from '../jsutils/invariant';
@@ -230,6 +231,9 @@ export class GraphQLSchema {
230231
}
231232
}
232233

234+
// Conditionally apply `[Symbol.toStringTag]` if `Symbol`s are supported
235+
applyToStringTag(GraphQLSchema);
236+
233237
type TypeMap = ObjMap<GraphQLNamedType>;
234238

235239
export type GraphQLSchemaValidationOptions = {|

0 commit comments

Comments
 (0)