-
Notifications
You must be signed in to change notification settings - Fork 12.3k
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Fix type when annotated with a JSDoc function type #22692
Changes from 5 commits
aeaf83c
d79b4df
34c977e
dfbae12
1f6106b
e09bd63
deba8e2
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -6699,7 +6699,14 @@ namespace ts { | |
let hasThisParameter: boolean; | ||
const iife = getImmediatelyInvokedFunctionExpression(declaration); | ||
const isJSConstructSignature = isJSDocConstructSignature(declaration); | ||
const isUntypedSignatureInJSFile = !iife && !isJSConstructSignature && isInJavaScriptFile(declaration) && !hasJSDocParameterTags(declaration); | ||
const isUntypedSignatureInJSFile = !iife && | ||
!isJSConstructSignature && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Nice catch. Removed. |
||
isInJavaScriptFile(declaration) && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Rather than skipping (some of the) the type-space signature declaration kinds, can we just check if it is a concrete declaration kind? That is the point here, right; to exclude type-space declaration kinds? export type ConcreteSignatureDeclaration =
| FunctionDeclaration
| MethodDeclaration
| ConstructorDeclaration
| AccessorDeclaration
| FunctionExpression
| ArrowFunction;
export function isConcreteSignatureDeclaration(declaration: Node): node is ConcreteSignatureDeclaration {
return isFunctionExpression(declaration) || isArrowFunction(declaration) || isMethodOrAccessor(declaration) || isFunctionDeclaration(declaration) || isConstructorDeclaration(declaration);
} There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Yes, a type signature is by definition not untyped, even if it's in a JS file. I used your code in the new commit. |
||
declaration.kind !== SyntaxKind.FunctionType && | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
declaration.kind !== SyntaxKind.ConstructorType && | ||
declaration.kind !== SyntaxKind.JSDocFunctionType && | ||
!hasJSDocParameterTags(declaration) && | ||
!getJSDocType(declaration); | ||
|
||
// If this is a JSDoc construct signature, then skip the first parameter in the | ||
// parameter list. The first parameter represents the return type of the construct | ||
|
@@ -9123,7 +9130,8 @@ namespace ts { | |
} | ||
|
||
function isContextSensitiveFunctionOrObjectLiteralMethod(func: Node): func is FunctionExpression | ArrowFunction | MethodDeclaration { | ||
return (isFunctionExpressionOrArrowFunction(func) || isObjectLiteralMethod(func)) && isContextSensitiveFunctionLikeDeclaration(func); | ||
return (isInJavaScriptFile(func) && isFunctionDeclaration(func) || isFunctionExpressionOrArrowFunction(func) || isObjectLiteralMethod(func)) && | ||
isContextSensitiveFunctionLikeDeclaration(func); | ||
} | ||
|
||
function getTypeWithoutSignatures(type: Type): Type { | ||
|
@@ -14191,49 +14199,49 @@ namespace ts { | |
// Return contextual type of parameter or undefined if no contextual type is available | ||
function getContextuallyTypedParameterType(parameter: ParameterDeclaration): Type | undefined { | ||
const func = parameter.parent; | ||
if (isContextSensitiveFunctionOrObjectLiteralMethod(func)) { | ||
const iife = getImmediatelyInvokedFunctionExpression(func); | ||
if (iife && iife.arguments) { | ||
const indexOfParameter = func.parameters.indexOf(parameter); | ||
if (parameter.dotDotDotToken) { | ||
const restTypes: Type[] = []; | ||
for (let i = indexOfParameter; i < iife.arguments.length; i++) { | ||
restTypes.push(getWidenedLiteralType(checkExpression(iife.arguments[i]))); | ||
} | ||
return restTypes.length ? createArrayType(getUnionType(restTypes)) : undefined; | ||
} | ||
const links = getNodeLinks(iife); | ||
const cached = links.resolvedSignature; | ||
links.resolvedSignature = anySignature; | ||
const type = indexOfParameter < iife.arguments.length ? | ||
getWidenedLiteralType(checkExpression(iife.arguments[indexOfParameter])) : | ||
parameter.initializer ? undefined : undefinedWideningType; | ||
links.resolvedSignature = cached; | ||
return type; | ||
if (!isContextSensitiveFunctionOrObjectLiteralMethod(func)) { | ||
return undefined; | ||
} | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This change is just an inversion of the guard and a subsequent un-indent. |
||
const iife = getImmediatelyInvokedFunctionExpression(func); | ||
if (iife && iife.arguments) { | ||
const indexOfParameter = func.parameters.indexOf(parameter); | ||
if (parameter.dotDotDotToken) { | ||
const restTypes: Type[] = []; | ||
for (let i = indexOfParameter; i < iife.arguments.length; i++) { | ||
restTypes.push(getWidenedLiteralType(checkExpression(iife.arguments[i]))); | ||
} | ||
return restTypes.length ? createArrayType(getUnionType(restTypes)) : undefined; | ||
} | ||
const links = getNodeLinks(iife); | ||
const cached = links.resolvedSignature; | ||
links.resolvedSignature = anySignature; | ||
const type = indexOfParameter < iife.arguments.length ? | ||
getWidenedLiteralType(checkExpression(iife.arguments[indexOfParameter])) : | ||
parameter.initializer ? undefined : undefinedWideningType; | ||
links.resolvedSignature = cached; | ||
return type; | ||
} | ||
const contextualSignature = getContextualSignature(func); | ||
if (contextualSignature) { | ||
const funcHasRestParameters = hasRestParameter(func); | ||
const len = func.parameters.length - (funcHasRestParameters ? 1 : 0); | ||
let indexOfParameter = func.parameters.indexOf(parameter); | ||
if (getThisParameter(func) !== undefined && !contextualSignature.thisParameter) { | ||
Debug.assert(indexOfParameter !== 0); // Otherwise we should not have called `getContextuallyTypedParameterType`. | ||
indexOfParameter -= 1; | ||
} | ||
const contextualSignature = getContextualSignature(func); | ||
if (contextualSignature) { | ||
const funcHasRestParameters = hasRestParameter(func); | ||
const len = func.parameters.length - (funcHasRestParameters ? 1 : 0); | ||
let indexOfParameter = func.parameters.indexOf(parameter); | ||
if (getThisParameter(func) !== undefined && !contextualSignature.thisParameter) { | ||
Debug.assert(indexOfParameter !== 0); // Otherwise we should not have called `getContextuallyTypedParameterType`. | ||
indexOfParameter -= 1; | ||
} | ||
|
||
if (indexOfParameter < len) { | ||
return getTypeAtPosition(contextualSignature, indexOfParameter); | ||
} | ||
if (indexOfParameter < len) { | ||
return getTypeAtPosition(contextualSignature, indexOfParameter); | ||
} | ||
|
||
// If last parameter is contextually rest parameter get its type | ||
if (funcHasRestParameters && | ||
indexOfParameter === (func.parameters.length - 1) && | ||
isRestParameterIndex(contextualSignature, func.parameters.length - 1)) { | ||
return getTypeOfSymbol(lastOrUndefined(contextualSignature.parameters)); | ||
} | ||
// If last parameter is contextually rest parameter get its type | ||
if (funcHasRestParameters && | ||
indexOfParameter === (func.parameters.length - 1) && | ||
isRestParameterIndex(contextualSignature, func.parameters.length - 1)) { | ||
return getTypeOfSymbol(lastOrUndefined(contextualSignature.parameters)); | ||
} | ||
} | ||
return undefined; | ||
} | ||
|
||
// In a variable, parameter or property declaration with a type annotation, | ||
|
@@ -14796,7 +14804,16 @@ namespace ts { | |
// union type of return types from these signatures | ||
function getContextualSignature(node: FunctionExpression | ArrowFunction | MethodDeclaration): Signature { | ||
Debug.assert(node.kind !== SyntaxKind.MethodDeclaration || isObjectLiteralMethod(node)); | ||
const type = getContextualTypeForFunctionLikeDeclaration(node); | ||
let type: Type; | ||
if (isInJavaScriptFile(node)) { | ||
const jsdoc = getJSDocType(node); | ||
if (jsdoc) { | ||
type = getTypeFromTypeNode(jsdoc); | ||
} | ||
} | ||
if (!type) { | ||
type = getContextualTypeForFunctionLikeDeclaration(node); | ||
} | ||
if (!type) { | ||
return undefined; | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,11 +1,9 @@ | ||
tests/cases/conformance/jsdoc/0.js(5,3): error TS2322: Type 'number' is not assignable to type 'string | undefined'. | ||
tests/cases/conformance/jsdoc/0.js(7,3): error TS2322: Type '(n1: any) => string' is not assignable to type '(arg0: number) => number'. | ||
tests/cases/conformance/jsdoc/0.js(7,3): error TS2322: Type '(n1: number) => string' is not assignable to type '(arg0: number) => number'. | ||
Type 'string' is not assignable to type 'number'. | ||
tests/cases/conformance/jsdoc/0.js(11,3): error TS2322: Type '(n1: any) => string' is not assignable to type '(arg0: number) => number'. | ||
tests/cases/conformance/jsdoc/0.js(11,3): error TS2322: Type '(n1: number) => string' is not assignable to type '(arg0: number) => number'. | ||
Type 'string' is not assignable to type 'number'. | ||
tests/cases/conformance/jsdoc/0.js(13,3): error TS2322: Type '(num?: string) => string' is not assignable to type '(arg0: number) => number'. | ||
Types of parameters 'num' and 'arg0' are incompatible. | ||
Type 'number' is not assignable to type 'string | undefined'. | ||
tests/cases/conformance/jsdoc/0.js(13,15): error TS2322: Type '"0"' is not assignable to type 'number'. | ||
tests/cases/conformance/jsdoc/0.js(15,3): error TS2322: Type 'undefined' is not assignable to type 'string'. | ||
tests/cases/conformance/jsdoc/0.js(19,5): error TS2322: Type 'number' is not assignable to type 'string'. | ||
tests/cases/conformance/jsdoc/0.js(22,22): error TS2345: Argument of type '"0"' is not assignable to parameter of type 'number'. | ||
|
@@ -22,21 +20,19 @@ tests/cases/conformance/jsdoc/0.js(22,22): error TS2345: Argument of type '"0"' | |
/** @type {function(number): number} */ | ||
method1(n1) { | ||
~~~~~~~ | ||
!!! error TS2322: Type '(n1: any) => string' is not assignable to type '(arg0: number) => number'. | ||
!!! error TS2322: Type '(n1: number) => string' is not assignable to type '(arg0: number) => number'. | ||
!!! error TS2322: Type 'string' is not assignable to type 'number'. | ||
return "42"; | ||
}, | ||
/** @type {function(number): number} */ | ||
method2: (n1) => "lol", | ||
~~~~~~~~~~~~~~~~~~~~~~ | ||
!!! error TS2322: Type '(n1: any) => string' is not assignable to type '(arg0: number) => number'. | ||
!!! error TS2322: Type '(n1: number) => string' is not assignable to type '(arg0: number) => number'. | ||
!!! error TS2322: Type 'string' is not assignable to type 'number'. | ||
/** @type {function(number): number} */ | ||
arrowFunc: (num="0") => num + 42, | ||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ | ||
!!! error TS2322: Type '(num?: string) => string' is not assignable to type '(arg0: number) => number'. | ||
!!! error TS2322: Types of parameters 'num' and 'arg0' are incompatible. | ||
!!! error TS2322: Type 'number' is not assignable to type 'string | undefined'. | ||
~~~~~~~ | ||
!!! error TS2322: Type '"0"' is not assignable to type 'number'. | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Do you know why we infer a literal type here now? AFAIK we only generate a literal if the contextual type of the literal is the same domain as the literal itself, which would imply that the contextual type here is There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The inferred type of "0" is for the initialiser, which was already of type "0" (see the type baselines below). You get the same error in Typescript with an type annotation, too: function f(x: number = "hi") {
} Gives the error "Type "hi" is not assignable to type 'number'." This error is expected now that There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Hm. OK. There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. It's not great, but it's consistent with the rest of the the compiler. Note that our baselines have gone back and forth on this in the last year or two. |
||
/** @type {string} */ | ||
lol | ||
~~~ | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
tests/cases/conformance/jsdoc/a.js(3,5): error TS2322: Type '1' is not assignable to type 'string'. | ||
tests/cases/conformance/jsdoc/a.js(7,5): error TS2322: Type '1' is not assignable to type 'string'. | ||
|
||
|
||
==== tests/cases/conformance/jsdoc/a.js (2 errors) ==== | ||
/** @type {function(string): void} */ | ||
const f = (value) => { | ||
value = 1 // should error | ||
~~~~~ | ||
!!! error TS2322: Type '1' is not assignable to type 'string'. | ||
}; | ||
/** @type {(s: string) => void} */ | ||
function g(s) { | ||
s = 1 // Should error | ||
~ | ||
!!! error TS2322: Type '1' is not assignable to type 'string'. | ||
} | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
=== tests/cases/conformance/jsdoc/a.js === | ||
/** @type {function(string): void} */ | ||
const f = (value) => { | ||
>f : Symbol(f, Decl(a.js, 1, 5)) | ||
>value : Symbol(value, Decl(a.js, 1, 11)) | ||
|
||
value = 1 // should error | ||
>value : Symbol(value, Decl(a.js, 1, 11)) | ||
|
||
}; | ||
/** @type {(s: string) => void} */ | ||
function g(s) { | ||
>g : Symbol(g, Decl(a.js, 3, 2)) | ||
>s : Symbol(s, Decl(a.js, 5, 11)) | ||
|
||
s = 1 // Should error | ||
>s : Symbol(s, Decl(a.js, 5, 11)) | ||
} | ||
|
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What case is handling when the declaration is an object literal with call/construct signatures, (ie,
{(): void}
)?There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
!getJSDocType(declaration)
. I'll add a test case. Note that the new code ingetJSDocType
doesn't look deep enough to get type of the parameter out of the method declaration inside the object literal type.