diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 59f2d40799c..62a15349dec 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -535,9 +535,11 @@ import { isJSDocNode, isJSDocNonNullableType, isJSDocNullableType, + isJSDocOptionalParameter, isJSDocOptionalType, isJSDocParameterTag, isJSDocPropertyLikeTag, + isJSDocPropertyTag, isJSDocReturnTag, isJSDocSignature, isJSDocTemplateTag, @@ -599,6 +601,7 @@ import { isOmittedExpression, isOptionalChain, isOptionalChainRoot, + isOptionalDeclaration, isOptionalJSDocPropertyLikeTag, isOptionalTypeNode, isOutermostOptionalChain, @@ -10164,11 +10167,8 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return getTypeForBindingElement(declaration as BindingElement); } - const isProperty = isPropertyDeclaration(declaration) && !hasAccessorModifier(declaration) || isPropertySignature(declaration); - const isOptional = includeOptionality && ( - isProperty && !!declaration.questionToken || - isParameter(declaration) && (!!declaration.questionToken || isJSDocOptionalParameter(declaration)) || - isOptionalJSDocPropertyLikeTag(declaration)); + const isProperty = (isPropertyDeclaration(declaration) && !hasAccessorModifier(declaration)) || isPropertySignature(declaration) || isJSDocPropertyTag(declaration); + const isOptional = includeOptionality && isOptionalDeclaration(declaration); // Use type from type annotation if one is present const declaredType = tryGetTypeFromEffectiveTypeNode(declaration); @@ -13991,14 +13991,6 @@ export function createTypeChecker(host: TypeCheckerHost): TypeChecker { return result; } - function isJSDocOptionalParameter(node: ParameterDeclaration) { - return isInJSFile(node) && ( - // node.type should only be a JSDocOptionalType when node is a parameter of a JSDocFunctionType - node.type && node.type.kind === SyntaxKind.JSDocOptionalType - || getJSDocParameterTags(node).some(({ isBracketed, typeExpression }) => - isBracketed || !!typeExpression && typeExpression.type.kind === SyntaxKind.JSDocOptionalType)); - } - function tryFindAmbientModule(moduleName: string, withAugmentations: boolean) { if (isExternalModuleNameRelative(moduleName)) { return undefined; diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index d2bb79697a4..252ebe202f8 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -414,6 +414,7 @@ import { PropertyDeclaration, PropertyName, PropertyNameLiteral, + PropertySignature, PseudoBigInt, QualifiedName, ReadonlyCollection, @@ -9139,3 +9140,26 @@ export function canUsePropertyAccess(name: string, languageVersion: ScriptTarget export function hasTabstop(node: Node): boolean { return getSnippetElement(node)?.kind === SnippetKind.TabStop; } + +export function isJSDocOptionalParameter(node: ParameterDeclaration) { + return isInJSFile(node) && ( + // node.type should only be a JSDocOptionalType when node is a parameter of a JSDocFunctionType + node.type && node.type.kind === SyntaxKind.JSDocOptionalType + || getJSDocParameterTags(node).some(({ isBracketed, typeExpression }) => + isBracketed || !!typeExpression && typeExpression.type.kind === SyntaxKind.JSDocOptionalType)); +} + +export function isOptionalDeclaration(declaration: Declaration): boolean { + switch (declaration.kind) { + case SyntaxKind.PropertyDeclaration: + case SyntaxKind.PropertySignature: + return !!(declaration as PropertyDeclaration | PropertySignature).questionToken; + case SyntaxKind.Parameter: + return !!(declaration as ParameterDeclaration).questionToken || isJSDocOptionalParameter(declaration as ParameterDeclaration); + case SyntaxKind.JSDocPropertyTag: + case SyntaxKind.JSDocParameterTag: + return isOptionalJSDocPropertyLikeTag(declaration); + default: + return false; + } +} diff --git a/tests/baselines/reference/api/tsserverlibrary.d.ts b/tests/baselines/reference/api/tsserverlibrary.d.ts index e7d7ff77215..be4bf4cb7a4 100644 --- a/tests/baselines/reference/api/tsserverlibrary.d.ts +++ b/tests/baselines/reference/api/tsserverlibrary.d.ts @@ -8766,6 +8766,8 @@ declare namespace ts { parent: ConstructorDeclaration; name: Identifier; }; + function isJSDocOptionalParameter(node: ParameterDeclaration): boolean; + function isOptionalDeclaration(declaration: Declaration): boolean; function createUnparsedSourceFile(text: string): UnparsedSource; function createUnparsedSourceFile(inputFile: InputFiles, type: "js" | "dts", stripInternal?: boolean): UnparsedSource; function createUnparsedSourceFile(text: string, mapPath: string | undefined, map: string | undefined): UnparsedSource; diff --git a/tests/baselines/reference/api/typescript.d.ts b/tests/baselines/reference/api/typescript.d.ts index 8ce49d6142f..8ddf62571c5 100644 --- a/tests/baselines/reference/api/typescript.d.ts +++ b/tests/baselines/reference/api/typescript.d.ts @@ -4832,6 +4832,8 @@ declare namespace ts { parent: ConstructorDeclaration; name: Identifier; }; + function isJSDocOptionalParameter(node: ParameterDeclaration): boolean; + function isOptionalDeclaration(declaration: Declaration): boolean; function createUnparsedSourceFile(text: string): UnparsedSource; function createUnparsedSourceFile(inputFile: InputFiles, type: "js" | "dts", stripInternal?: boolean): UnparsedSource; function createUnparsedSourceFile(text: string, mapPath: string | undefined, map: string | undefined): UnparsedSource; diff --git a/tests/baselines/reference/strictOptionalProperties3.errors.txt b/tests/baselines/reference/strictOptionalProperties3.errors.txt new file mode 100644 index 00000000000..1c56589165c --- /dev/null +++ b/tests/baselines/reference/strictOptionalProperties3.errors.txt @@ -0,0 +1,32 @@ +tests/cases/compiler/a.js(7,7): error TS2375: Type '{ value: undefined; }' is not assignable to type 'A' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. + Types of property 'value' are incompatible. + Type 'undefined' is not assignable to type 'number'. +tests/cases/compiler/a.js(14,7): error TS2375: Type '{ value: undefined; }' is not assignable to type 'B' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. + Types of property 'value' are incompatible. + Type 'undefined' is not assignable to type 'number'. + + +==== tests/cases/compiler/a.js (2 errors) ==== + /** + * @typedef {object} A + * @property {number} [value] + */ + + /** @type {A} */ + const a = { value: undefined }; // error + ~ +!!! error TS2375: Type '{ value: undefined; }' is not assignable to type 'A' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. +!!! error TS2375: Types of property 'value' are incompatible. +!!! error TS2375: Type 'undefined' is not assignable to type 'number'. + + /** + * @typedef {{ value?: number }} B + */ + + /** @type {B} */ + const b = { value: undefined }; // error + ~ +!!! error TS2375: Type '{ value: undefined; }' is not assignable to type 'B' with 'exactOptionalPropertyTypes: true'. Consider adding 'undefined' to the types of the target's properties. +!!! error TS2375: Types of property 'value' are incompatible. +!!! error TS2375: Type 'undefined' is not assignable to type 'number'. + \ No newline at end of file diff --git a/tests/baselines/reference/strictOptionalProperties3.symbols b/tests/baselines/reference/strictOptionalProperties3.symbols new file mode 100644 index 00000000000..db658b8cbcb --- /dev/null +++ b/tests/baselines/reference/strictOptionalProperties3.symbols @@ -0,0 +1,22 @@ +=== tests/cases/compiler/a.js === +/** + * @typedef {object} A + * @property {number} [value] + */ + +/** @type {A} */ +const a = { value: undefined }; // error +>a : Symbol(a, Decl(a.js, 6, 5)) +>value : Symbol(value, Decl(a.js, 6, 11)) +>undefined : Symbol(undefined) + +/** + * @typedef {{ value?: number }} B + */ + +/** @type {B} */ +const b = { value: undefined }; // error +>b : Symbol(b, Decl(a.js, 13, 5)) +>value : Symbol(value, Decl(a.js, 13, 11)) +>undefined : Symbol(undefined) + diff --git a/tests/baselines/reference/strictOptionalProperties3.types b/tests/baselines/reference/strictOptionalProperties3.types new file mode 100644 index 00000000000..5f8c18f5648 --- /dev/null +++ b/tests/baselines/reference/strictOptionalProperties3.types @@ -0,0 +1,24 @@ +=== tests/cases/compiler/a.js === +/** + * @typedef {object} A + * @property {number} [value] + */ + +/** @type {A} */ +const a = { value: undefined }; // error +>a : A +>{ value: undefined } : { value: undefined; } +>value : undefined +>undefined : undefined + +/** + * @typedef {{ value?: number }} B + */ + +/** @type {B} */ +const b = { value: undefined }; // error +>b : B +>{ value: undefined } : { value: undefined; } +>value : undefined +>undefined : undefined + diff --git a/tests/baselines/reference/strictOptionalProperties4.symbols b/tests/baselines/reference/strictOptionalProperties4.symbols new file mode 100644 index 00000000000..22c970fc6ac --- /dev/null +++ b/tests/baselines/reference/strictOptionalProperties4.symbols @@ -0,0 +1,22 @@ +=== tests/cases/compiler/a.js === +/** + * @typedef Foo + * @property {number} [foo] + */ + +const x = /** @type {Foo} */ ({}); +>x : Symbol(x, Decl(a.js, 5, 5)) + +x.foo; // number | undefined +>x.foo : Symbol(foo, Decl(a.js, 2, 3)) +>x : Symbol(x, Decl(a.js, 5, 5)) +>foo : Symbol(foo, Decl(a.js, 2, 3)) + +const y = /** @type {Required} */ ({}); +>y : Symbol(y, Decl(a.js, 8, 5)) + +y.foo; // number +>y.foo : Symbol(foo, Decl(a.js, 2, 3)) +>y : Symbol(y, Decl(a.js, 8, 5)) +>foo : Symbol(foo, Decl(a.js, 2, 3)) + diff --git a/tests/baselines/reference/strictOptionalProperties4.types b/tests/baselines/reference/strictOptionalProperties4.types new file mode 100644 index 00000000000..fb7c6ed9186 --- /dev/null +++ b/tests/baselines/reference/strictOptionalProperties4.types @@ -0,0 +1,26 @@ +=== tests/cases/compiler/a.js === +/** + * @typedef Foo + * @property {number} [foo] + */ + +const x = /** @type {Foo} */ ({}); +>x : Foo +>({}) : Foo +>{} : {} + +x.foo; // number | undefined +>x.foo : number | undefined +>x : Foo +>foo : number | undefined + +const y = /** @type {Required} */ ({}); +>y : Required +>({}) : Required +>{} : {} + +y.foo; // number +>y.foo : number +>y : Required +>foo : number + diff --git a/tests/cases/compiler/strictOptionalProperties3.ts b/tests/cases/compiler/strictOptionalProperties3.ts new file mode 100644 index 00000000000..c1bff467f19 --- /dev/null +++ b/tests/cases/compiler/strictOptionalProperties3.ts @@ -0,0 +1,21 @@ +// @strictNullChecks: true +// @exactOptionalPropertyTypes: true +// @allowJs: true +// @checkJs: true +// @noEmit: true +// @filename: a.js + +/** + * @typedef {object} A + * @property {number} [value] + */ + +/** @type {A} */ +const a = { value: undefined }; // error + +/** + * @typedef {{ value?: number }} B + */ + +/** @type {B} */ +const b = { value: undefined }; // error diff --git a/tests/cases/compiler/strictOptionalProperties4.ts b/tests/cases/compiler/strictOptionalProperties4.ts new file mode 100644 index 00000000000..c902cde58b2 --- /dev/null +++ b/tests/cases/compiler/strictOptionalProperties4.ts @@ -0,0 +1,17 @@ +// @strictNullChecks: true +// @exactOptionalPropertyTypes: true +// @allowJs: true +// @checkJs: true +// @noEmit: true +// @filename: a.js + +/** + * @typedef Foo + * @property {number} [foo] + */ + +const x = /** @type {Foo} */ ({}); +x.foo; // number | undefined + +const y = /** @type {Required} */ ({}); +y.foo; // number