Skip to content

Commit a6c5d50

Browse files
authored
Allow type predicates in JSDoc (#26343)
* Allow type predicates 1. Parse type predicates. Note that they are parsed everywhere, and get the appropriate error when used places besides a return type. 2. When creating a type predicate, correctly find the function's parameters starting from the jsdoc return type. * Fix type of TypePredicateNode.parent: add JSDocTypeExpression * Update API baselines * Handle JSDoc signature inside @type annotations * Fix circularity when getting type predicates Also move createTypePredicateFromTypePredicateNode closer to its use * More cleanup based on review comments
1 parent 46d3caa commit a6c5d50

10 files changed

+187
-43
lines changed

src/compiler/checker.ts

+33-28
Original file line numberDiff line numberDiff line change
@@ -7406,20 +7406,6 @@ namespace ts {
74067406
return isBracketed || !!typeExpression && typeExpression.type.kind === SyntaxKind.JSDocOptionalType;
74077407
}
74087408

7409-
function createTypePredicateFromTypePredicateNode(node: TypePredicateNode): IdentifierTypePredicate | ThisTypePredicate {
7410-
const { parameterName } = node;
7411-
const type = getTypeFromTypeNode(node.type);
7412-
if (parameterName.kind === SyntaxKind.Identifier) {
7413-
return createIdentifierTypePredicate(
7414-
parameterName && parameterName.escapedText as string, // TODO: GH#18217
7415-
parameterName && getTypePredicateParameterIndex(node.parent.parameters, parameterName),
7416-
type);
7417-
}
7418-
else {
7419-
return createThisTypePredicate(type);
7420-
}
7421-
}
7422-
74237409
function createIdentifierTypePredicate(parameterName: string, parameterIndex: number, type: Type): IdentifierTypePredicate {
74247410
return { kind: TypePredicateKind.Identifier, parameterName, parameterIndex, type };
74257411
}
@@ -7678,15 +7664,46 @@ namespace ts {
76787664
}
76797665
else {
76807666
const type = signature.declaration && getEffectiveReturnTypeNode(signature.declaration);
7667+
let jsdocPredicate: TypePredicate | undefined;
7668+
if (!type && isInJavaScriptFile(signature.declaration)) {
7669+
const jsdocSignature = getSignatureOfTypeTag(signature.declaration!);
7670+
if (jsdocSignature && signature !== jsdocSignature) {
7671+
jsdocPredicate = getTypePredicateOfSignature(jsdocSignature);
7672+
}
7673+
}
76817674
signature.resolvedTypePredicate = type && isTypePredicateNode(type) ?
7682-
createTypePredicateFromTypePredicateNode(type) :
7683-
noTypePredicate;
7675+
createTypePredicateFromTypePredicateNode(type, signature.declaration!) :
7676+
jsdocPredicate || noTypePredicate;
76847677
}
76857678
Debug.assert(!!signature.resolvedTypePredicate);
76867679
}
76877680
return signature.resolvedTypePredicate === noTypePredicate ? undefined : signature.resolvedTypePredicate;
76887681
}
76897682

7683+
function createTypePredicateFromTypePredicateNode(node: TypePredicateNode, func: SignatureDeclaration | JSDocSignature): IdentifierTypePredicate | ThisTypePredicate {
7684+
const { parameterName } = node;
7685+
const type = getTypeFromTypeNode(node.type);
7686+
if (parameterName.kind === SyntaxKind.Identifier) {
7687+
return createIdentifierTypePredicate(
7688+
parameterName.escapedText as string,
7689+
getTypePredicateParameterIndex(func.parameters, parameterName),
7690+
type);
7691+
}
7692+
else {
7693+
return createThisTypePredicate(type);
7694+
}
7695+
}
7696+
7697+
function getTypePredicateParameterIndex(parameterList: ReadonlyArray<ParameterDeclaration | JSDocParameterTag>, parameter: Identifier): number {
7698+
for (let i = 0; i < parameterList.length; i++) {
7699+
const param = parameterList[i];
7700+
if (param.name.kind === SyntaxKind.Identifier && param.name.escapedText === parameter.escapedText) {
7701+
return i;
7702+
}
7703+
}
7704+
return -1;
7705+
}
7706+
76907707
function getReturnTypeOfSignature(signature: Signature): Type {
76917708
if (!signature.resolvedReturnType) {
76927709
if (!pushTypeResolution(signature, TypeSystemPropertyName.ResolvedReturnType)) {
@@ -22064,18 +22081,6 @@ namespace ts {
2206422081
}
2206522082
}
2206622083

22067-
function getTypePredicateParameterIndex(parameterList: NodeArray<ParameterDeclaration>, parameter: Identifier): number {
22068-
if (parameterList) {
22069-
for (let i = 0; i < parameterList.length; i++) {
22070-
const param = parameterList[i];
22071-
if (param.name.kind === SyntaxKind.Identifier && param.name.escapedText === parameter.escapedText) {
22072-
return i;
22073-
}
22074-
}
22075-
}
22076-
return -1;
22077-
}
22078-
2207922084
function checkTypePredicate(node: TypePredicateNode): void {
2208022085
const parent = getTypePredicateParent(node);
2208122086
if (!parent) {

src/compiler/parser.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -2374,7 +2374,7 @@ namespace ts {
23742374

23752375
function parseJSDocType(): TypeNode {
23762376
const dotdotdot = parseOptionalToken(SyntaxKind.DotDotDotToken);
2377-
let type = parseType();
2377+
let type = parseTypeOrTypePredicate();
23782378
if (dotdotdot) {
23792379
const variadic = createNode(SyntaxKind.JSDocVariadicType, dotdotdot.pos) as JSDocVariadicType;
23802380
variadic.type = type;

src/compiler/types.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1082,7 +1082,7 @@ namespace ts {
10821082

10831083
export interface TypePredicateNode extends TypeNode {
10841084
kind: SyntaxKind.TypePredicate;
1085-
parent: SignatureDeclaration;
1085+
parent: SignatureDeclaration | JSDocTypeExpression;
10861086
parameterName: Identifier | ThisTypeNode;
10871087
type: TypeNode;
10881088
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ declare namespace ts {
752752
}
753753
interface TypePredicateNode extends TypeNode {
754754
kind: SyntaxKind.TypePredicate;
755-
parent: SignatureDeclaration;
755+
parent: SignatureDeclaration | JSDocTypeExpression;
756756
parameterName: Identifier | ThisTypeNode;
757757
type: TypeNode;
758758
}

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

+1-1
Original file line numberDiff line numberDiff line change
@@ -752,7 +752,7 @@ declare namespace ts {
752752
}
753753
interface TypePredicateNode extends TypeNode {
754754
kind: SyntaxKind.TypePredicate;
755-
parent: SignatureDeclaration;
755+
parent: SignatureDeclaration | JSDocTypeExpression;
756756
parameterName: Identifier | ThisTypeNode;
757757
type: TypeNode;
758758
}

tests/baselines/reference/jsdocTypeTagCast.errors.txt

+4-10
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,7 @@ tests/cases/conformance/jsdoc/b.js(58,1): error TS2322: Type 'SomeFakeClass' is
1111
Types of property 'p' are incompatible.
1212
Type 'string | number' is not assignable to type 'number'.
1313
Type 'string' is not assignable to type 'number'.
14-
tests/cases/conformance/jsdoc/b.js(66,8): error TS2352: Conversion of type 'boolean' to type 'string | number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
15-
tests/cases/conformance/jsdoc/b.js(66,15): error TS2304: Cannot find name 'numOrStr'.
16-
tests/cases/conformance/jsdoc/b.js(66,24): error TS1005: '}' expected.
14+
tests/cases/conformance/jsdoc/b.js(66,15): error TS1228: A type predicate is only allowed in return type position for functions and methods.
1715
tests/cases/conformance/jsdoc/b.js(66,38): error TS2454: Variable 'numOrStr' is used before being assigned.
1816
tests/cases/conformance/jsdoc/b.js(67,2): error TS2322: Type 'string | number' is not assignable to type 'string'.
1917
Type 'number' is not assignable to type 'string'.
@@ -23,7 +21,7 @@ tests/cases/conformance/jsdoc/b.js(67,8): error TS2454: Variable 'numOrStr' is u
2321
==== tests/cases/conformance/jsdoc/a.ts (0 errors) ====
2422
var W: string;
2523

26-
==== tests/cases/conformance/jsdoc/b.js (12 errors) ====
24+
==== tests/cases/conformance/jsdoc/b.js (10 errors) ====
2725
// @ts-check
2826
var W = /** @type {string} */(/** @type {*} */ (4));
2927

@@ -109,12 +107,8 @@ tests/cases/conformance/jsdoc/b.js(67,8): error TS2454: Variable 'numOrStr' is u
109107
/** @type {string} */
110108
var str;
111109
if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error
112-
~~~~~~~~~~~~~~~
113-
!!! error TS2352: Conversion of type 'boolean' to type 'string | number' may be a mistake because neither type sufficiently overlaps with the other. If this was intentional, convert the expression to 'unknown' first.
114-
~~~~~~~~
115-
!!! error TS2304: Cannot find name 'numOrStr'.
116-
~~
117-
!!! error TS1005: '}' expected.
110+
~~~~~~~~~~~~~~~~~~
111+
!!! error TS1228: A type predicate is only allowed in return type position for functions and methods.
118112
~~~~~~~~
119113
!!! error TS2454: Variable 'numOrStr' is used before being assigned.
120114
str = numOrStr; // Error, no narrowing occurred

tests/baselines/reference/jsdocTypeTagCast.types

+1-1
Original file line numberDiff line numberDiff line change
@@ -197,7 +197,7 @@ var str;
197197
>str : string
198198

199199
if(/** @type {numOrStr is string} */(numOrStr === undefined)) { // Error
200-
>(numOrStr === undefined) : string | number
200+
>(numOrStr === undefined) : boolean
201201
>numOrStr === undefined : boolean
202202
>numOrStr : string | number
203203
>undefined : undefined

tests/baselines/reference/returnTagTypeGuard.symbols

+52
Original file line numberDiff line numberDiff line change
@@ -61,3 +61,55 @@ function f(chunk) {
6161
>x : Symbol(x, Decl(bug25127.js, 26, 7))
6262
}
6363

64+
/**
65+
* @param {any} value
66+
* @return {value is boolean}
67+
*/
68+
function isBoolean(value) {
69+
>isBoolean : Symbol(isBoolean, Decl(bug25127.js, 28, 1))
70+
>value : Symbol(value, Decl(bug25127.js, 34, 19))
71+
72+
return typeof value === "boolean";
73+
>value : Symbol(value, Decl(bug25127.js, 34, 19))
74+
}
75+
76+
/** @param {boolean | number} val */
77+
function foo(val) {
78+
>foo : Symbol(foo, Decl(bug25127.js, 36, 1))
79+
>val : Symbol(val, Decl(bug25127.js, 39, 13))
80+
81+
if (isBoolean(val)) {
82+
>isBoolean : Symbol(isBoolean, Decl(bug25127.js, 28, 1))
83+
>val : Symbol(val, Decl(bug25127.js, 39, 13))
84+
85+
val;
86+
>val : Symbol(val, Decl(bug25127.js, 39, 13))
87+
}
88+
}
89+
90+
/**
91+
* @callback Cb
92+
* @param {unknown} x
93+
* @return {x is number}
94+
*/
95+
96+
/** @type {Cb} */
97+
function isNumber(x) { return typeof x === "number" }
98+
>isNumber : Symbol(isNumber, Decl(bug25127.js, 43, 1))
99+
>x : Symbol(x, Decl(bug25127.js, 52, 18))
100+
>x : Symbol(x, Decl(bug25127.js, 52, 18))
101+
102+
/** @param {unknown} x */
103+
function g(x) {
104+
>g : Symbol(g, Decl(bug25127.js, 52, 53))
105+
>x : Symbol(x, Decl(bug25127.js, 55, 11))
106+
107+
if (isNumber(x)) {
108+
>isNumber : Symbol(isNumber, Decl(bug25127.js, 43, 1))
109+
>x : Symbol(x, Decl(bug25127.js, 55, 11))
110+
111+
x * 2;
112+
>x : Symbol(x, Decl(bug25127.js, 55, 11))
113+
}
114+
}
115+

tests/baselines/reference/returnTagTypeGuard.types

+62
Original file line numberDiff line numberDiff line change
@@ -69,3 +69,65 @@ function f(chunk) {
6969
>x : string | number
7070
}
7171

72+
/**
73+
* @param {any} value
74+
* @return {value is boolean}
75+
*/
76+
function isBoolean(value) {
77+
>isBoolean : (value: any) => value is boolean
78+
>value : any
79+
80+
return typeof value === "boolean";
81+
>typeof value === "boolean" : boolean
82+
>typeof value : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
83+
>value : any
84+
>"boolean" : "boolean"
85+
}
86+
87+
/** @param {boolean | number} val */
88+
function foo(val) {
89+
>foo : (val: number | boolean) => void
90+
>val : number | boolean
91+
92+
if (isBoolean(val)) {
93+
>isBoolean(val) : boolean
94+
>isBoolean : (value: any) => value is boolean
95+
>val : number | boolean
96+
97+
val;
98+
>val : boolean
99+
}
100+
}
101+
102+
/**
103+
* @callback Cb
104+
* @param {unknown} x
105+
* @return {x is number}
106+
*/
107+
108+
/** @type {Cb} */
109+
function isNumber(x) { return typeof x === "number" }
110+
>isNumber : (x: unknown) => x is number
111+
>x : unknown
112+
>typeof x === "number" : boolean
113+
>typeof x : "string" | "number" | "boolean" | "symbol" | "undefined" | "object" | "function"
114+
>x : unknown
115+
>"number" : "number"
116+
117+
/** @param {unknown} x */
118+
function g(x) {
119+
>g : (x: unknown) => void
120+
>x : unknown
121+
122+
if (isNumber(x)) {
123+
>isNumber(x) : boolean
124+
>isNumber : (x: unknown) => x is number
125+
>x : unknown
126+
127+
x * 2;
128+
>x * 2 : number
129+
>x : number
130+
>2 : 2
131+
}
132+
}
133+

tests/cases/conformance/jsdoc/returnTagTypeGuard.ts

+31
Original file line numberDiff line numberDiff line change
@@ -32,3 +32,34 @@ function f(chunk) {
3232
let x = chunk.isInit(chunk) ? chunk.c : chunk.d
3333
return x
3434
}
35+
36+
/**
37+
* @param {any} value
38+
* @return {value is boolean}
39+
*/
40+
function isBoolean(value) {
41+
return typeof value === "boolean";
42+
}
43+
44+
/** @param {boolean | number} val */
45+
function foo(val) {
46+
if (isBoolean(val)) {
47+
val;
48+
}
49+
}
50+
51+
/**
52+
* @callback Cb
53+
* @param {unknown} x
54+
* @return {x is number}
55+
*/
56+
57+
/** @type {Cb} */
58+
function isNumber(x) { return typeof x === "number" }
59+
60+
/** @param {unknown} x */
61+
function g(x) {
62+
if (isNumber(x)) {
63+
x * 2;
64+
}
65+
}

0 commit comments

Comments
 (0)