Skip to content

Commit 25fb541

Browse files
authored
Support the JSDoc @enum tag (microsoft#26021)
* Support the JSDoc @enum tag `@enum` is used on a variable declaration with an object literal initializer. It does a number of things: 1. The object literal has a closed set of properties, unlike other object literals in Javascript. 2. The variable's name is resolvable as a type, but it just has the declared type of the enum tag. 3. Each property's type must be assignable to the enum tag's declared type, which can be any type. For example, ```js /** @enum {string} */ const Target = { START: "START", END: "END", MISTAKE: 0, // error 'number' is not assignable to 'string' -- see (3) } Target.THIS_IS_AN_ERROR; // See (1) /** @type {Target} See (2) */ var target = Target.START; ``` * Fix lint, add new test case, update API baselines
1 parent a26aff3 commit 25fb541

File tree

10 files changed

+484
-38
lines changed

10 files changed

+484
-38
lines changed

src/compiler/checker.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8128,6 +8128,10 @@ namespace ts {
81288128
// TODO: GH#18217 (should the `|| assignedType` be at a lower precedence?)
81298129
return (referenceType && assignedType ? getIntersectionType([assignedType, referenceType]) : referenceType || assignedType)!;
81308130
}
8131+
const enumTag = getJSDocEnumTag(symbol.valueDeclaration);
8132+
if (enumTag && enumTag.typeExpression) {
8133+
return getTypeFromTypeNode(enumTag.typeExpression);
8134+
}
81318135
}
81328136

81338137
function getTypeReferenceTypeWorker(node: NodeWithTypeArguments, symbol: Symbol, typeArguments: Type[] | undefined): Type | undefined {
@@ -16563,7 +16567,8 @@ namespace ts {
1656316567
const contextualTypeHasPattern = contextualType && contextualType.pattern &&
1656416568
(contextualType.pattern.kind === SyntaxKind.ObjectBindingPattern || contextualType.pattern.kind === SyntaxKind.ObjectLiteralExpression);
1656516569
const isInJSFile = isInJavaScriptFile(node) && !isInJsonFile(node);
16566-
const isJSObjectLiteral = !contextualType && isInJSFile;
16570+
const enumTag = getJSDocEnumTag(node);
16571+
const isJSObjectLiteral = !contextualType && isInJSFile && !enumTag;
1656716572
let typeFlags: TypeFlags = 0;
1656816573
let patternWithComputedProperties = false;
1656916574
let hasComputedStringProperty = false;
@@ -16588,6 +16593,9 @@ namespace ts {
1658816593
checkTypeAssignableTo(type, jsDocType, memberDecl);
1658916594
type = jsDocType;
1659016595
}
16596+
else if (enumTag && enumTag.typeExpression) {
16597+
checkTypeAssignableTo(type, getTypeFromTypeNode(enumTag.typeExpression), memberDecl);
16598+
}
1659116599
}
1659216600
typeFlags |= type.flags;
1659316601
const nameType = computedNameType && computedNameType.flags & TypeFlags.StringOrNumberLiteralOrUnique ?
@@ -16622,6 +16630,7 @@ namespace ts {
1662216630
symbolToString(member), typeToString(contextualType!));
1662316631
}
1662416632
}
16633+
1662516634
prop.declarations = member.declarations;
1662616635
prop.parent = member.parent;
1662716636
if (member.valueDeclaration) {

src/compiler/parser.ts

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -491,6 +491,8 @@ namespace ts {
491491
visitNode(cbNode, (node as JSDocCallbackTag).typeExpression);
492492
case SyntaxKind.JSDocThisTag:
493493
return visitNode(cbNode, (node as JSDocThisTag).typeExpression);
494+
case SyntaxKind.JSDocEnumTag:
495+
return visitNode(cbNode, (node as JSDocEnumTag).typeExpression);
494496
case SyntaxKind.JSDocSignature:
495497
return visitNodes(cbNode, cbNodes, node.decorators) ||
496498
visitNodes(cbNode, cbNodes, node.modifiers) ||
@@ -6527,6 +6529,9 @@ namespace ts {
65276529
case "this":
65286530
tag = parseThisTag(atToken, tagName);
65296531
break;
6532+
case "enum":
6533+
tag = parseEnumTag(atToken, tagName);
6534+
break;
65306535
case "arg":
65316536
case "argument":
65326537
case "param":
@@ -6817,6 +6822,15 @@ namespace ts {
68176822
return finishNode(tag);
68186823
}
68196824

6825+
function parseEnumTag(atToken: AtToken, tagName: Identifier): JSDocEnumTag {
6826+
const tag = <JSDocEnumTag>createNode(SyntaxKind.JSDocEnumTag, atToken.pos);
6827+
tag.atToken = atToken;
6828+
tag.tagName = tagName;
6829+
tag.typeExpression = parseJSDocTypeExpression(/*mayOmitBraces*/ true);
6830+
skipWhitespace();
6831+
return finishNode(tag);
6832+
}
6833+
68206834
function parseTypedefTag(atToken: AtToken, tagName: Identifier, indent: number): JSDocTypedefTag {
68216835
const typeExpression = tryParseTypeExpression();
68226836
skipWhitespace();

src/compiler/types.ts

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -372,6 +372,7 @@ namespace ts {
372372
JSDocAugmentsTag,
373373
JSDocClassTag,
374374
JSDocCallbackTag,
375+
JSDocEnumTag,
375376
JSDocParameterTag,
376377
JSDocReturnTag,
377378
JSDocThisTag,
@@ -581,6 +582,7 @@ namespace ts {
581582
| FunctionTypeNode
582583
| ConstructorTypeNode
583584
| JSDocFunctionType
585+
| ExportDeclaration
584586
| EndOfFileToken;
585587

586588
export type HasType =
@@ -2203,7 +2205,7 @@ namespace ts {
22032205
name: Identifier;
22042206
}
22052207

2206-
export interface ExportDeclaration extends DeclarationStatement {
2208+
export interface ExportDeclaration extends DeclarationStatement, JSDocContainer {
22072209
kind: SyntaxKind.ExportDeclaration;
22082210
parent: SourceFile | ModuleBlock;
22092211
/** Will not be assigned in the case of `export * from "foo";` */
@@ -2349,6 +2351,11 @@ namespace ts {
23492351
kind: SyntaxKind.JSDocClassTag;
23502352
}
23512353

2354+
export interface JSDocEnumTag extends JSDocTag {
2355+
kind: SyntaxKind.JSDocEnumTag;
2356+
typeExpression?: JSDocTypeExpression;
2357+
}
2358+
23522359
export interface JSDocThisTag extends JSDocTag {
23532360
kind: SyntaxKind.JSDocThisTag;
23542361
typeExpression?: JSDocTypeExpression;

src/compiler/utilities.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4995,6 +4995,11 @@ namespace ts {
49954995
return getFirstJSDocTag(node, isJSDocClassTag);
49964996
}
49974997

4998+
/** Gets the JSDoc enum tag for the node if present */
4999+
export function getJSDocEnumTag(node: Node): JSDocEnumTag | undefined {
5000+
return getFirstJSDocTag(node, isJSDocEnumTag);
5001+
}
5002+
49985003
/** Gets the JSDoc this tag for the node if present */
49995004
export function getJSDocThisTag(node: Node): JSDocThisTag | undefined {
50005005
return getFirstJSDocTag(node, isJSDocThisTag);
@@ -5759,6 +5764,10 @@ namespace ts {
57595764
return node.kind === SyntaxKind.JSDocClassTag;
57605765
}
57615766

5767+
export function isJSDocEnumTag(node: Node): node is JSDocEnumTag {
5768+
return node.kind === SyntaxKind.JSDocEnumTag;
5769+
}
5770+
57625771
export function isJSDocThisTag(node: Node): node is JSDocThisTag {
57635772
return node.kind === SyntaxKind.JSDocThisTag;
57645773
}

tests/baselines/reference/api/tsserverlibrary.d.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -368,20 +368,21 @@ declare namespace ts {
368368
JSDocAugmentsTag = 293,
369369
JSDocClassTag = 294,
370370
JSDocCallbackTag = 295,
371-
JSDocParameterTag = 296,
372-
JSDocReturnTag = 297,
373-
JSDocThisTag = 298,
374-
JSDocTypeTag = 299,
375-
JSDocTemplateTag = 300,
376-
JSDocTypedefTag = 301,
377-
JSDocPropertyTag = 302,
378-
SyntaxList = 303,
379-
NotEmittedStatement = 304,
380-
PartiallyEmittedExpression = 305,
381-
CommaListExpression = 306,
382-
MergeDeclarationMarker = 307,
383-
EndOfDeclarationMarker = 308,
384-
Count = 309,
371+
JSDocEnumTag = 296,
372+
JSDocParameterTag = 297,
373+
JSDocReturnTag = 298,
374+
JSDocThisTag = 299,
375+
JSDocTypeTag = 300,
376+
JSDocTemplateTag = 301,
377+
JSDocTypedefTag = 302,
378+
JSDocPropertyTag = 303,
379+
SyntaxList = 304,
380+
NotEmittedStatement = 305,
381+
PartiallyEmittedExpression = 306,
382+
CommaListExpression = 307,
383+
MergeDeclarationMarker = 308,
384+
EndOfDeclarationMarker = 309,
385+
Count = 310,
385386
FirstAssignment = 58,
386387
LastAssignment = 70,
387388
FirstCompoundAssignment = 59,
@@ -408,9 +409,9 @@ declare namespace ts {
408409
LastBinaryOperator = 70,
409410
FirstNode = 146,
410411
FirstJSDocNode = 281,
411-
LastJSDocNode = 302,
412+
LastJSDocNode = 303,
412413
FirstJSDocTagNode = 292,
413-
LastJSDocTagNode = 302
414+
LastJSDocTagNode = 303
414415
}
415416
enum NodeFlags {
416417
None = 0,
@@ -479,7 +480,7 @@ declare namespace ts {
479480
}
480481
interface JSDocContainer {
481482
}
482-
type HasJSDoc = ParameterDeclaration | CallSignatureDeclaration | ConstructSignatureDeclaration | MethodSignature | PropertySignature | ArrowFunction | ParenthesizedExpression | SpreadAssignment | ShorthandPropertyAssignment | PropertyAssignment | FunctionExpression | LabeledStatement | ExpressionStatement | VariableStatement | FunctionDeclaration | ConstructorDeclaration | MethodDeclaration | PropertyDeclaration | AccessorDeclaration | ClassLikeDeclaration | InterfaceDeclaration | TypeAliasDeclaration | EnumMember | EnumDeclaration | ModuleDeclaration | ImportEqualsDeclaration | IndexSignatureDeclaration | FunctionTypeNode | ConstructorTypeNode | JSDocFunctionType | EndOfFileToken;
483+
type HasJSDoc = ParameterDeclaration | CallSignatureDeclaration | ConstructSignatureDeclaration | MethodSignature | PropertySignature | ArrowFunction | ParenthesizedExpression | SpreadAssignment | ShorthandPropertyAssignment | PropertyAssignment | FunctionExpression | LabeledStatement | ExpressionStatement | VariableStatement | FunctionDeclaration | ConstructorDeclaration | MethodDeclaration | PropertyDeclaration | AccessorDeclaration | ClassLikeDeclaration | InterfaceDeclaration | TypeAliasDeclaration | EnumMember | EnumDeclaration | ModuleDeclaration | ImportEqualsDeclaration | IndexSignatureDeclaration | FunctionTypeNode | ConstructorTypeNode | JSDocFunctionType | ExportDeclaration | EndOfFileToken;
483484
type HasType = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertySignature | PropertyDeclaration | TypePredicateNode | ParenthesizedTypeNode | TypeOperatorNode | MappedTypeNode | AssertionExpression | TypeAliasDeclaration | JSDocTypeExpression | JSDocNonNullableType | JSDocNullableType | JSDocOptionalType | JSDocVariadicType;
484485
type HasInitializer = HasExpressionInitializer | ForStatement | ForInStatement | ForOfStatement | JsxAttribute;
485486
type HasExpressionInitializer = VariableDeclaration | ParameterDeclaration | BindingElement | PropertySignature | PropertyDeclaration | PropertyAssignment | EnumMember;
@@ -1438,7 +1439,7 @@ declare namespace ts {
14381439
kind: SyntaxKind.NamespaceExportDeclaration;
14391440
name: Identifier;
14401441
}
1441-
interface ExportDeclaration extends DeclarationStatement {
1442+
interface ExportDeclaration extends DeclarationStatement, JSDocContainer {
14421443
kind: SyntaxKind.ExportDeclaration;
14431444
parent: SourceFile | ModuleBlock;
14441445
/** Will not be assigned in the case of `export * from "foo";` */
@@ -1557,6 +1558,10 @@ declare namespace ts {
15571558
interface JSDocClassTag extends JSDocTag {
15581559
kind: SyntaxKind.JSDocClassTag;
15591560
}
1561+
interface JSDocEnumTag extends JSDocTag {
1562+
kind: SyntaxKind.JSDocEnumTag;
1563+
typeExpression?: JSDocTypeExpression;
1564+
}
15601565
interface JSDocThisTag extends JSDocTag {
15611566
kind: SyntaxKind.JSDocThisTag;
15621567
typeExpression?: JSDocTypeExpression;
@@ -3221,6 +3226,8 @@ declare namespace ts {
32213226
function getJSDocAugmentsTag(node: Node): JSDocAugmentsTag | undefined;
32223227
/** Gets the JSDoc class tag for the node if present */
32233228
function getJSDocClassTag(node: Node): JSDocClassTag | undefined;
3229+
/** Gets the JSDoc enum tag for the node if present */
3230+
function getJSDocEnumTag(node: Node): JSDocEnumTag | undefined;
32243231
/** Gets the JSDoc this tag for the node if present */
32253232
function getJSDocThisTag(node: Node): JSDocThisTag | undefined;
32263233
/** Gets the JSDoc return tag for the node if present */
@@ -3413,6 +3420,7 @@ declare namespace ts {
34133420
function isJSDoc(node: Node): node is JSDoc;
34143421
function isJSDocAugmentsTag(node: Node): node is JSDocAugmentsTag;
34153422
function isJSDocClassTag(node: Node): node is JSDocClassTag;
3423+
function isJSDocEnumTag(node: Node): node is JSDocEnumTag;
34163424
function isJSDocThisTag(node: Node): node is JSDocThisTag;
34173425
function isJSDocParameterTag(node: Node): node is JSDocParameterTag;
34183426
function isJSDocReturnTag(node: Node): node is JSDocReturnTag;

tests/baselines/reference/api/typescript.d.ts

Lines changed: 26 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -368,20 +368,21 @@ declare namespace ts {
368368
JSDocAugmentsTag = 293,
369369
JSDocClassTag = 294,
370370
JSDocCallbackTag = 295,
371-
JSDocParameterTag = 296,
372-
JSDocReturnTag = 297,
373-
JSDocThisTag = 298,
374-
JSDocTypeTag = 299,
375-
JSDocTemplateTag = 300,
376-
JSDocTypedefTag = 301,
377-
JSDocPropertyTag = 302,
378-
SyntaxList = 303,
379-
NotEmittedStatement = 304,
380-
PartiallyEmittedExpression = 305,
381-
CommaListExpression = 306,
382-
MergeDeclarationMarker = 307,
383-
EndOfDeclarationMarker = 308,
384-
Count = 309,
371+
JSDocEnumTag = 296,
372+
JSDocParameterTag = 297,
373+
JSDocReturnTag = 298,
374+
JSDocThisTag = 299,
375+
JSDocTypeTag = 300,
376+
JSDocTemplateTag = 301,
377+
JSDocTypedefTag = 302,
378+
JSDocPropertyTag = 303,
379+
SyntaxList = 304,
380+
NotEmittedStatement = 305,
381+
PartiallyEmittedExpression = 306,
382+
CommaListExpression = 307,
383+
MergeDeclarationMarker = 308,
384+
EndOfDeclarationMarker = 309,
385+
Count = 310,
385386
FirstAssignment = 58,
386387
LastAssignment = 70,
387388
FirstCompoundAssignment = 59,
@@ -408,9 +409,9 @@ declare namespace ts {
408409
LastBinaryOperator = 70,
409410
FirstNode = 146,
410411
FirstJSDocNode = 281,
411-
LastJSDocNode = 302,
412+
LastJSDocNode = 303,
412413
FirstJSDocTagNode = 292,
413-
LastJSDocTagNode = 302
414+
LastJSDocTagNode = 303
414415
}
415416
enum NodeFlags {
416417
None = 0,
@@ -479,7 +480,7 @@ declare namespace ts {
479480
}
480481
interface JSDocContainer {
481482
}
482-
type HasJSDoc = ParameterDeclaration | CallSignatureDeclaration | ConstructSignatureDeclaration | MethodSignature | PropertySignature | ArrowFunction | ParenthesizedExpression | SpreadAssignment | ShorthandPropertyAssignment | PropertyAssignment | FunctionExpression | LabeledStatement | ExpressionStatement | VariableStatement | FunctionDeclaration | ConstructorDeclaration | MethodDeclaration | PropertyDeclaration | AccessorDeclaration | ClassLikeDeclaration | InterfaceDeclaration | TypeAliasDeclaration | EnumMember | EnumDeclaration | ModuleDeclaration | ImportEqualsDeclaration | IndexSignatureDeclaration | FunctionTypeNode | ConstructorTypeNode | JSDocFunctionType | EndOfFileToken;
483+
type HasJSDoc = ParameterDeclaration | CallSignatureDeclaration | ConstructSignatureDeclaration | MethodSignature | PropertySignature | ArrowFunction | ParenthesizedExpression | SpreadAssignment | ShorthandPropertyAssignment | PropertyAssignment | FunctionExpression | LabeledStatement | ExpressionStatement | VariableStatement | FunctionDeclaration | ConstructorDeclaration | MethodDeclaration | PropertyDeclaration | AccessorDeclaration | ClassLikeDeclaration | InterfaceDeclaration | TypeAliasDeclaration | EnumMember | EnumDeclaration | ModuleDeclaration | ImportEqualsDeclaration | IndexSignatureDeclaration | FunctionTypeNode | ConstructorTypeNode | JSDocFunctionType | ExportDeclaration | EndOfFileToken;
483484
type HasType = SignatureDeclaration | VariableDeclaration | ParameterDeclaration | PropertySignature | PropertyDeclaration | TypePredicateNode | ParenthesizedTypeNode | TypeOperatorNode | MappedTypeNode | AssertionExpression | TypeAliasDeclaration | JSDocTypeExpression | JSDocNonNullableType | JSDocNullableType | JSDocOptionalType | JSDocVariadicType;
484485
type HasInitializer = HasExpressionInitializer | ForStatement | ForInStatement | ForOfStatement | JsxAttribute;
485486
type HasExpressionInitializer = VariableDeclaration | ParameterDeclaration | BindingElement | PropertySignature | PropertyDeclaration | PropertyAssignment | EnumMember;
@@ -1438,7 +1439,7 @@ declare namespace ts {
14381439
kind: SyntaxKind.NamespaceExportDeclaration;
14391440
name: Identifier;
14401441
}
1441-
interface ExportDeclaration extends DeclarationStatement {
1442+
interface ExportDeclaration extends DeclarationStatement, JSDocContainer {
14421443
kind: SyntaxKind.ExportDeclaration;
14431444
parent: SourceFile | ModuleBlock;
14441445
/** Will not be assigned in the case of `export * from "foo";` */
@@ -1557,6 +1558,10 @@ declare namespace ts {
15571558
interface JSDocClassTag extends JSDocTag {
15581559
kind: SyntaxKind.JSDocClassTag;
15591560
}
1561+
interface JSDocEnumTag extends JSDocTag {
1562+
kind: SyntaxKind.JSDocEnumTag;
1563+
typeExpression?: JSDocTypeExpression;
1564+
}
15601565
interface JSDocThisTag extends JSDocTag {
15611566
kind: SyntaxKind.JSDocThisTag;
15621567
typeExpression?: JSDocTypeExpression;
@@ -3221,6 +3226,8 @@ declare namespace ts {
32213226
function getJSDocAugmentsTag(node: Node): JSDocAugmentsTag | undefined;
32223227
/** Gets the JSDoc class tag for the node if present */
32233228
function getJSDocClassTag(node: Node): JSDocClassTag | undefined;
3229+
/** Gets the JSDoc enum tag for the node if present */
3230+
function getJSDocEnumTag(node: Node): JSDocEnumTag | undefined;
32243231
/** Gets the JSDoc this tag for the node if present */
32253232
function getJSDocThisTag(node: Node): JSDocThisTag | undefined;
32263233
/** Gets the JSDoc return tag for the node if present */
@@ -3413,6 +3420,7 @@ declare namespace ts {
34133420
function isJSDoc(node: Node): node is JSDoc;
34143421
function isJSDocAugmentsTag(node: Node): node is JSDocAugmentsTag;
34153422
function isJSDocClassTag(node: Node): node is JSDocClassTag;
3423+
function isJSDocEnumTag(node: Node): node is JSDocEnumTag;
34163424
function isJSDocThisTag(node: Node): node is JSDocThisTag;
34173425
function isJSDocParameterTag(node: Node): node is JSDocParameterTag;
34183426
function isJSDocReturnTag(node: Node): node is JSDocReturnTag;
Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,65 @@
1+
tests/cases/conformance/jsdoc/a.js(6,5): error TS2322: Type 'number' is not assignable to type 'string'.
2+
tests/cases/conformance/jsdoc/a.js(12,5): error TS2322: Type 'string' is not assignable to type 'number'.
3+
tests/cases/conformance/jsdoc/a.js(37,16): error TS2339: Property 'UNKNOWN' does not exist on type '{ START: string; MIDDLE: string; END: string; MISTAKE: number; OK_I_GUESS: number; }'.
4+
5+
6+
==== tests/cases/conformance/jsdoc/a.js (3 errors) ====
7+
/** @enum {string} */
8+
const Target = {
9+
START: "start",
10+
MIDDLE: "middle",
11+
END: "end",
12+
MISTAKE: 1,
13+
~~~~~~~~~~
14+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
15+
/** @type {number} */
16+
OK_I_GUESS: 2
17+
}
18+
/** @enum {number} */
19+
const Second = {
20+
MISTAKE: "end",
21+
~~~~~~~~~~~~~~
22+
!!! error TS2322: Type 'string' is not assignable to type 'number'.
23+
OK: 1,
24+
/** @type {number} */
25+
FINE: 2,
26+
}
27+
/** @enum {function(number): number} */
28+
const Fs = {
29+
ADD1: n => n + 1,
30+
ID: n => n,
31+
SUB1: n => n - 1
32+
}
33+
34+
/** @param {Target} t
35+
* @param {Second} s
36+
* @param {Fs} f
37+
*/
38+
function consume(t,s,f) {
39+
/** @type {string} */
40+
var str = t
41+
/** @type {number} */
42+
var num = s
43+
/** @type {(n: number) => number} */
44+
var fun = f
45+
/** @type {Target} */
46+
var v = Target.START
47+
v = Target.UNKNOWN // error, can't find 'UNKNOWN'
48+
~~~~~~~
49+
!!! error TS2339: Property 'UNKNOWN' does not exist on type '{ START: string; MIDDLE: string; END: string; MISTAKE: number; OK_I_GUESS: number; }'.
50+
v = Second.MISTAKE // meh..ok, I guess?
51+
v = 'something else' // allowed, like Typescript's classic enums and unlike its string enums
52+
}
53+
/** @param {string} s */
54+
function ff(s) {
55+
// element access with arbitrary string is an error only with noImplicitAny
56+
if (!Target[s]) {
57+
return null
58+
}
59+
else {
60+
return Target[s]
61+
}
62+
}
63+
64+
65+

0 commit comments

Comments
 (0)