Skip to content

Commit bd7b97c

Browse files
authored
Get return type from @type tag (#25580)
* Get return type from `@type` tag This only happens in the checker, where the type is easily accessible. The syntax-based check in getEffectiveReturnTypeNode as a fast path, and for other uses that don't want to make a call to getTypeFromTypeNode. Fixes #25525 * Implement PR suggestions * Error when type tag isn't callable * Fix lint
1 parent 66e9aaa commit bd7b97c

18 files changed

+237
-41
lines changed

src/compiler/checker.ts

Lines changed: 33 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -7566,12 +7566,21 @@ namespace ts {
75667566
const setter = getDeclarationOfKind<AccessorDeclaration>(getSymbolOfNode(declaration), SyntaxKind.SetAccessor);
75677567
return getAnnotatedAccessorType(setter);
75687568
}
7569-
7569+
const typeFromTag = getReturnTypeOfTypeTag(declaration);
7570+
if (typeFromTag) {
7571+
return typeFromTag;
7572+
}
75707573
if (nodeIsMissing((<FunctionLikeDeclaration>declaration).body)) {
75717574
return anyType;
75727575
}
75737576
}
75747577

7578+
function getReturnTypeOfTypeTag(node: SignatureDeclaration | JSDocSignature) {
7579+
const typeTag = isInJavaScriptFile(node) ? getJSDocTypeTag(node) : undefined;
7580+
const signatures = typeTag && typeTag.typeExpression && getSignaturesOfType(getTypeFromTypeNode(typeTag.typeExpression), SignatureKind.Call);
7581+
return signatures && signatures.length === 1 ? getReturnTypeOfSignature(signatures[0]) : undefined;
7582+
}
7583+
75757584
function containsArgumentsReference(declaration: SignatureDeclaration): boolean {
75767585
const links = getNodeLinks(declaration);
75777586
if (links.containsArgumentsReference === undefined) {
@@ -20539,23 +20548,28 @@ namespace ts {
2053920548
return type;
2054020549
}
2054120550

20551+
function getReturnOrPromisedType(node: FunctionLikeDeclaration | MethodSignature, functionFlags: FunctionFlags) {
20552+
const returnTypeNode = getEffectiveReturnTypeNode(node);
20553+
return returnTypeNode &&
20554+
((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async ?
20555+
checkAsyncFunctionReturnType(node, returnTypeNode) : // Async function
20556+
getTypeFromTypeNode(returnTypeNode)) || // AsyncGenerator function, Generator function, or normal function
20557+
getReturnTypeOfTypeTag(node); // type from JSDoc @type tag
20558+
}
20559+
2054220560
function checkFunctionExpressionOrObjectLiteralMethodDeferred(node: ArrowFunction | FunctionExpression | MethodDeclaration) {
2054320561
Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node));
2054420562

2054520563
const functionFlags = getFunctionFlags(node);
20546-
const returnTypeNode = getEffectiveReturnTypeNode(node);
20547-
const returnOrPromisedType = returnTypeNode &&
20548-
((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async ?
20549-
checkAsyncFunctionReturnType(node) : // Async function
20550-
getTypeFromTypeNode(returnTypeNode)); // AsyncGenerator function, Generator function, or normal function
20564+
const returnOrPromisedType = getReturnOrPromisedType(node, functionFlags);
2055120565

2055220566
if ((functionFlags & FunctionFlags.Generator) === 0) { // Async function or normal function
2055320567
// return is not necessary in the body of generators
2055420568
checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnOrPromisedType);
2055520569
}
2055620570

2055720571
if (node.body) {
20558-
if (!returnTypeNode) {
20572+
if (!getEffectiveReturnTypeNode(node)) {
2055920573
// There are some checks that are only performed in getReturnTypeFromBody, that may produce errors
2056020574
// we need. An example is the noImplicitAny errors resulting from widening the return expression
2056120575
// of a function. Because checking of function expression bodies is deferred, there was never an
@@ -22007,7 +22021,7 @@ namespace ts {
2200722021
}
2200822022
}
2200922023
else if ((functionFlags & FunctionFlags.AsyncGenerator) === FunctionFlags.Async) {
22010-
checkAsyncFunctionReturnType(<FunctionLikeDeclaration>node);
22024+
checkAsyncFunctionReturnType(<FunctionLikeDeclaration>node, returnTypeNode);
2201122025
}
2201222026
}
2201322027
if (node.kind !== SyntaxKind.IndexSignature && node.kind !== SyntaxKind.JSDocFunctionType) {
@@ -23067,7 +23081,7 @@ namespace ts {
2306723081
*
2306823082
* @param node The signature to check
2306923083
*/
23070-
function checkAsyncFunctionReturnType(node: FunctionLikeDeclaration | MethodSignature): Type {
23084+
function checkAsyncFunctionReturnType(node: FunctionLikeDeclaration | MethodSignature, returnTypeNode: TypeNode): Type {
2307123085
// As part of our emit for an async function, we will need to emit the entity name of
2307223086
// the return type annotation as an expression. To meet the necessary runtime semantics
2307323087
// for __awaiter, we must also check that the type of the declaration (e.g. the static
@@ -23092,7 +23106,6 @@ namespace ts {
2309223106
// then<U>(...): Promise<U>;
2309323107
// }
2309423108
//
23095-
const returnTypeNode = getEffectiveReturnTypeNode(node)!; // TODO: GH#18217
2309623109
const returnType = getTypeFromTypeNode(returnTypeNode);
2309723110

2309823111
if (languageVersion >= ScriptTarget.ES2015) {
@@ -23502,15 +23515,12 @@ namespace ts {
2350223515
const body = node.kind === SyntaxKind.MethodSignature ? undefined : node.body;
2350323516
checkSourceElement(body);
2350423517

23505-
const returnTypeNode = getEffectiveReturnTypeNode(node);
2350623518
if ((functionFlags & FunctionFlags.Generator) === 0) { // Async function or normal function
23507-
const returnOrPromisedType = returnTypeNode && (functionFlags & FunctionFlags.Async
23508-
? checkAsyncFunctionReturnType(node) // Async function
23509-
: getTypeFromTypeNode(returnTypeNode)); // normal function
23519+
const returnOrPromisedType = getReturnOrPromisedType(node, functionFlags);
2351023520
checkAllCodePathsInNonVoidFunctionReturnOrThrow(node, returnOrPromisedType);
2351123521
}
2351223522

23513-
if (produceDiagnostics && !returnTypeNode) {
23523+
if (produceDiagnostics && !getEffectiveReturnTypeNode(node)) {
2351423524
// Report an implicit any error if there is no body, no explicit return type, and node is not a private method
2351523525
// in an ambient context
2351623526
if (noImplicitAny && nodeIsMissing(body) && !isPrivateWithinAmbient(node)) {
@@ -23523,6 +23533,13 @@ namespace ts {
2352323533
// yielded values. The only way to trigger these errors is to try checking its return type.
2352423534
getReturnTypeOfSignature(getSignatureFromDeclaration(node));
2352523535
}
23536+
// A js function declaration can have a @type tag instead of a return type node, but that type must have a call signature
23537+
if (isInJavaScriptFile(node)) {
23538+
const typeTag = getJSDocTypeTag(node);
23539+
if (typeTag && typeTag.typeExpression && !getSignaturesOfType(getTypeFromTypeNode(typeTag.typeExpression), SignatureKind.Call).length) {
23540+
error(typeTag, Diagnostics.The_type_of_a_function_declaration_must_be_callable);
23541+
}
23542+
}
2352623543
}
2352723544
}
2352823545

@@ -24785,7 +24802,7 @@ namespace ts {
2478524802
error(node, Diagnostics.Return_type_of_constructor_signature_must_be_assignable_to_the_instance_type_of_the_class);
2478624803
}
2478724804
}
24788-
else if (getEffectiveReturnTypeNode(func) || isGetAccessorWithAnnotatedSetAccessor(func)) {
24805+
else if (getEffectiveReturnTypeNode(func) || isGetAccessorWithAnnotatedSetAccessor(func) || getReturnTypeOfTypeTag(func)) {
2478924806
if (functionFlags & FunctionFlags.Async) { // Async function
2479024807
const promisedType = getPromisedTypeOfPromise(returnType);
2479124808
const awaitedType = checkAwaitedType(exprType, node, Diagnostics.The_return_type_of_an_async_function_must_either_be_a_valid_promise_or_must_not_contain_a_callable_then_member);

src/compiler/diagnosticMessages.json

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4021,6 +4021,10 @@
40214021
"category": "Error",
40224022
"code": 8029
40234023
},
4024+
"The type of a function declaration must be callable.": {
4025+
"category": "Error",
4026+
"code": 8030
4027+
},
40244028
"Only identifiers/qualified-names with optional type arguments are currently supported in a class 'extends' clause.": {
40254029
"category": "Error",
40264030
"code": 9002

tests/baselines/reference/callbackTag2.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ var outside = n => n + 1;
4343
/** @type {Final<{ fantasy }, { heroes }>} */
4444
var noreturn = (barts, tidus, noctis) => "cecil"
4545
>noreturn : Final<{ fantasy: any; }, { heroes: any; }>
46-
>(barts, tidus, noctis) => "cecil" : (barts: { fantasy: any; }, tidus: { heroes: any; }, noctis: { heroes: any; } & { fantasy: any; }) => "cecil"
46+
>(barts, tidus, noctis) => "cecil" : (barts: { fantasy: any; }, tidus: { heroes: any; }, noctis: { heroes: any; } & { fantasy: any; }) => "cecil" | "zidane"
4747
>barts : { fantasy: any; }
4848
>tidus : { heroes: any; }
4949
>noctis : { heroes: any; } & { fantasy: any; }

tests/baselines/reference/callbackTagNamespace.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ var x = 1;
1111

1212
/** @type {NS.Nested.Inner} */
1313
function f(space, peace) {
14-
>f : (space: any, peace: any) => string
14+
>f : (space: any, peace: any) => string | number
1515
>space : any
1616
>peace : any
1717

tests/baselines/reference/checkJsdocTypeTag1.types

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -61,7 +61,7 @@ y(1);
6161
/** @type {function (number)} */
6262
const x1 = (a) => a + 1;
6363
>x1 : (arg0: number) => any
64-
>(a) => a + 1 : (a: number) => number
64+
>(a) => a + 1 : (a: number) => any
6565
>a : number
6666
>a + 1 : number
6767
>a : number

tests/baselines/reference/checkJsdocTypeTag2.errors.txt

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@ tests/cases/conformance/jsdoc/0.js(6,5): error TS2322: Type '"hello"' is not ass
33
tests/cases/conformance/jsdoc/0.js(10,4): error TS2345: Argument of type '"string"' is not assignable to parameter of type 'number'.
44
tests/cases/conformance/jsdoc/0.js(17,1): error TS2322: Type 'number' is not assignable to type 'string'.
55
tests/cases/conformance/jsdoc/0.js(20,21): error TS2339: Property 'concat' does not exist on type 'number'.
6-
tests/cases/conformance/jsdoc/0.js(24,7): error TS2322: Type '(a: number) => number' is not assignable to type '(arg0: number) => string'.
7-
Type 'number' is not assignable to type 'string'.
6+
tests/cases/conformance/jsdoc/0.js(24,19): error TS2322: Type 'number' is not assignable to type 'string'.
87

98

109
==== tests/cases/conformance/jsdoc/0.js (6 errors) ====
@@ -42,7 +41,6 @@ tests/cases/conformance/jsdoc/0.js(24,7): error TS2322: Type '(a: number) => num
4241

4342
/** @type {function (number): string} */
4443
const x4 = (a) => a + 1;
45-
~~
46-
!!! error TS2322: Type '(a: number) => number' is not assignable to type '(arg0: number) => string'.
47-
!!! error TS2322: Type 'number' is not assignable to type 'string'.
44+
~~~~~
45+
!!! error TS2322: Type 'number' is not assignable to type 'string'.
4846
x4(0);

tests/baselines/reference/checkJsdocTypeTag2.types

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ var n = "hello";
1313
/** @type {function (number)} */
1414
const x1 = (a) => a + 1;
1515
>x1 : (arg0: number) => any
16-
>(a) => a + 1 : (a: number) => number
16+
>(a) => a + 1 : (a: number) => any
1717
>a : number
1818
>a + 1 : number
1919
>a : number
@@ -47,7 +47,7 @@ a = x2(0);
4747
/** @type {function (number): number} */
4848
const x3 = (a) => a.concat("hi");
4949
>x3 : (arg0: number) => number
50-
>(a) => a.concat("hi") : (a: number) => any
50+
>(a) => a.concat("hi") : (a: number) => number
5151
>a : number
5252
>a.concat("hi") : any
5353
>a.concat : any
@@ -63,7 +63,7 @@ x3(0);
6363
/** @type {function (number): string} */
6464
const x4 = (a) => a + 1;
6565
>x4 : (arg0: number) => string
66-
>(a) => a + 1 : (a: number) => number
66+
>(a) => a + 1 : (a: number) => string
6767
>a : number
6868
>a + 1 : number
6969
>a : number

tests/baselines/reference/checkJsdocTypeTag5.errors.txt

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,11 @@ tests/cases/conformance/jsdoc/test.js(7,24): error TS2322: Type 'number' is not
44
tests/cases/conformance/jsdoc/test.js(10,17): error TS2322: Type 'number' is not assignable to type 'string'.
55
tests/cases/conformance/jsdoc/test.js(12,14): error TS2322: Type 'number' is not assignable to type 'string'.
66
tests/cases/conformance/jsdoc/test.js(14,24): error TS2322: Type 'number' is not assignable to type 'string'.
7+
tests/cases/conformance/jsdoc/test.js(34,5): error TS2322: Type '1 | 2' is not assignable to type '2 | 3'.
8+
Type '1' is not assignable to type '2 | 3'.
79

810

9-
==== tests/cases/conformance/jsdoc/test.js (6 errors) ====
11+
==== tests/cases/conformance/jsdoc/test.js (7 errors) ====
1012
// all 6 should error on return statement/expression
1113
/** @type {(x: number) => string} */
1214
function h(x) { return x }
@@ -33,4 +35,27 @@ tests/cases/conformance/jsdoc/test.js(14,24): error TS2322: Type 'number' is not
3335
var k = function (x) { return x }
3436
~~~~~~~~
3537
!!! error TS2322: Type 'number' is not assignable to type 'string'.
38+
39+
40+
/** @typedef {(x: 'hi' | 'bye') => 0 | 1 | 2} Argle */
41+
/** @type {Argle} */
42+
function blargle(s) {
43+
return 0;
44+
}
45+
46+
/** @type {0 | 1 | 2} - assignment should not error */
47+
var zeroonetwo = blargle('hi')
48+
49+
/** @typedef {{(s: string): 0 | 1; (b: boolean): 2 | 3 }} Gioconda */
50+
51+
/** @type {Gioconda} */
52+
function monaLisa(sb) {
53+
return typeof sb === 'string' ? 1 : 2;
54+
}
55+
56+
/** @type {2 | 3} - overloads are not supported, so there will be an error */
57+
var twothree = monaLisa(false);
58+
~~~~~~~~
59+
!!! error TS2322: Type '1 | 2' is not assignable to type '2 | 3'.
60+
!!! error TS2322: Type '1' is not assignable to type '2 | 3'.
3661

tests/baselines/reference/checkJsdocTypeTag5.symbols

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -36,3 +36,34 @@ var k = function (x) { return x }
3636
>x : Symbol(x, Decl(test.js, 13, 18))
3737
>x : Symbol(x, Decl(test.js, 13, 18))
3838

39+
40+
/** @typedef {(x: 'hi' | 'bye') => 0 | 1 | 2} Argle */
41+
/** @type {Argle} */
42+
function blargle(s) {
43+
>blargle : Symbol(blargle, Decl(test.js, 13, 33))
44+
>s : Symbol(s, Decl(test.js, 18, 17))
45+
46+
return 0;
47+
}
48+
49+
/** @type {0 | 1 | 2} - assignment should not error */
50+
var zeroonetwo = blargle('hi')
51+
>zeroonetwo : Symbol(zeroonetwo, Decl(test.js, 23, 3))
52+
>blargle : Symbol(blargle, Decl(test.js, 13, 33))
53+
54+
/** @typedef {{(s: string): 0 | 1; (b: boolean): 2 | 3 }} Gioconda */
55+
56+
/** @type {Gioconda} */
57+
function monaLisa(sb) {
58+
>monaLisa : Symbol(monaLisa, Decl(test.js, 23, 30))
59+
>sb : Symbol(sb, Decl(test.js, 28, 18))
60+
61+
return typeof sb === 'string' ? 1 : 2;
62+
>sb : Symbol(sb, Decl(test.js, 28, 18))
63+
}
64+
65+
/** @type {2 | 3} - overloads are not supported, so there will be an error */
66+
var twothree = monaLisa(false);
67+
>twothree : Symbol(twothree, Decl(test.js, 33, 3))
68+
>monaLisa : Symbol(monaLisa, Decl(test.js, 23, 30))
69+

tests/baselines/reference/checkJsdocTypeTag5.types

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,3 +40,45 @@ var k = function (x) { return x }
4040
>x : number
4141
>x : number
4242

43+
44+
/** @typedef {(x: 'hi' | 'bye') => 0 | 1 | 2} Argle */
45+
/** @type {Argle} */
46+
function blargle(s) {
47+
>blargle : (s: "hi" | "bye") => 0 | 1 | 2
48+
>s : "hi" | "bye"
49+
50+
return 0;
51+
>0 : 0
52+
}
53+
54+
/** @type {0 | 1 | 2} - assignment should not error */
55+
var zeroonetwo = blargle('hi')
56+
>zeroonetwo : 0 | 1 | 2
57+
>blargle('hi') : 0 | 1 | 2
58+
>blargle : (s: "hi" | "bye") => 0 | 1 | 2
59+
>'hi' : "hi"
60+
61+
/** @typedef {{(s: string): 0 | 1; (b: boolean): 2 | 3 }} Gioconda */
62+
63+
/** @type {Gioconda} */
64+
function monaLisa(sb) {
65+
>monaLisa : (sb: any) => 1 | 2
66+
>sb : any
67+
68+
return typeof sb === 'string' ? 1 : 2;
69+
>typeof sb === 'string' ? 1 : 2 : 1 | 2
70+
>typeof sb === 'string' : boolean
71+
>typeof sb : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
72+
>sb : any
73+
>'string' : "string"
74+
>1 : 1
75+
>2 : 2
76+
}
77+
78+
/** @type {2 | 3} - overloads are not supported, so there will be an error */
79+
var twothree = monaLisa(false);
80+
>twothree : 2 | 3
81+
>monaLisa(false) : 1 | 2
82+
>monaLisa : (sb: any) => 1 | 2
83+
>false : false
84+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
tests/cases/conformance/jsdoc/test.js(1,5): error TS8030: The type of a function declaration must be callable.
2+
tests/cases/conformance/jsdoc/test.js(7,5): error TS2322: Type '(prop: any) => void' is not assignable to type '{ prop: string; }'.
3+
Property 'prop' is missing in type '(prop: any) => void'.
4+
5+
6+
==== tests/cases/conformance/jsdoc/test.js (2 errors) ====
7+
/** @type {number} */
8+
~~~~~~~~~~~~~~
9+
!!! error TS8030: The type of a function declaration must be callable.
10+
function f() {
11+
return 1
12+
}
13+
14+
/** @type {{ prop: string }} */
15+
var g = function (prop) {
16+
~
17+
!!! error TS2322: Type '(prop: any) => void' is not assignable to type '{ prop: string; }'.
18+
!!! error TS2322: Property 'prop' is missing in type '(prop: any) => void'.
19+
}
20+
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
=== tests/cases/conformance/jsdoc/test.js ===
2+
/** @type {number} */
3+
function f() {
4+
>f : Symbol(f, Decl(test.js, 0, 0))
5+
6+
return 1
7+
}
8+
9+
/** @type {{ prop: string }} */
10+
var g = function (prop) {
11+
>g : Symbol(g, Decl(test.js, 6, 3))
12+
>prop : Symbol(prop, Decl(test.js, 6, 18))
13+
}
14+
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
=== tests/cases/conformance/jsdoc/test.js ===
2+
/** @type {number} */
3+
function f() {
4+
>f : () => number
5+
6+
return 1
7+
>1 : 1
8+
}
9+
10+
/** @type {{ prop: string }} */
11+
var g = function (prop) {
12+
>g : { prop: string; }
13+
>function (prop) {} : (prop: any) => void
14+
>prop : any
15+
}
16+

0 commit comments

Comments
 (0)