diff --git a/src/compiler/binder.ts b/src/compiler/binder.ts index 22209e2e41e17..f3aa73c6e4eb2 100644 --- a/src/compiler/binder.ts +++ b/src/compiler/binder.ts @@ -2101,7 +2101,8 @@ namespace ts { case SyntaxKind.Constructor: return declareSymbolAndAddToSymbolTable(node, SymbolFlags.Constructor, /*symbolExcludes:*/ SymbolFlags.None); case SyntaxKind.GetAccessor: - return bindPropertyOrMethodOrAccessor(node, SymbolFlags.GetAccessor, SymbolFlags.GetAccessorExcludes); + return bindPropertyOrMethodOrAccessor(node, SymbolFlags.GetAccessor | ((node).questionToken ? SymbolFlags.Optional : SymbolFlags.None), + SymbolFlags.GetAccessorExcludes); case SyntaxKind.SetAccessor: return bindPropertyOrMethodOrAccessor(node, SymbolFlags.SetAccessor, SymbolFlags.SetAccessorExcludes); case SyntaxKind.FunctionType: diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 2657843192e18..d17903565a6d8 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -4588,7 +4588,12 @@ namespace ts { error(getter, Diagnostics._0_implicitly_has_return_type_any_because_it_does_not_have_a_return_type_annotation_and_is_referenced_directly_or_indirectly_in_one_of_its_return_expressions, symbolToString(symbol)); } } - links.type = type; + if (getter) { + links.type = addOptionality(type, /*optional*/ !!getter.questionToken); + } + else { + links.type = type; + } } return links.type; } @@ -24280,7 +24285,7 @@ namespace ts { else if (isInAmbientContext(accessor)) { return grammarErrorOnNode(accessor.name, Diagnostics.An_accessor_cannot_be_declared_in_an_ambient_context); } - else if (accessor.body === undefined && !(getModifierFlags(accessor) & ModifierFlags.Abstract)) { + else if (accessor.body === undefined && !hasQuestionToken(accessor) && !(getModifierFlags(accessor) & ModifierFlags.Abstract)) { return grammarErrorAtPos(getSourceFileOfNode(accessor), accessor.end - 1, ";".length, Diagnostics._0_expected, "{"); } else if (accessor.body && getModifierFlags(accessor) & ModifierFlags.Abstract) { diff --git a/src/compiler/declarationEmitter.ts b/src/compiler/declarationEmitter.ts index 987157843b605..a5c15cf779ab0 100644 --- a/src/compiler/declarationEmitter.ts +++ b/src/compiler/declarationEmitter.ts @@ -1393,6 +1393,9 @@ namespace ts { emitJsDocComments(accessors.setAccessor); emitClassMemberDeclarationFlags(getModifierFlags(node) | (accessors.setAccessor ? 0 : ModifierFlags.Readonly)); writeTextOfNode(currentText, node.name); + if (hasQuestionToken(accessors.getAccessor)) { + write("?"); + } if (!hasModifier(node, ModifierFlags.Private)) { accessorWithTypeAnnotation = node; let type = getTypeAnnotationFromAccessor(node); diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 8002709f3f690..dfa41827a3658 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -4263,12 +4263,12 @@ namespace ts { return finishNode(node); } - function tryParseAccessorDeclaration(fullStart: number, decorators: NodeArray, modifiers: NodeArray): AccessorDeclaration { + function tryParseAccessorDeclaration(fullStart: number, decorators: NodeArray, modifiers: NodeArray, allowQuestionToken: boolean): AccessorDeclaration { if (parseContextualModifier(SyntaxKind.GetKeyword)) { - return parseAccessorDeclaration(SyntaxKind.GetAccessor, fullStart, decorators, modifiers); + return parseAccessorDeclaration(SyntaxKind.GetAccessor, fullStart, decorators, modifiers, allowQuestionToken); } else if (parseContextualModifier(SyntaxKind.SetKeyword)) { - return parseAccessorDeclaration(SyntaxKind.SetAccessor, fullStart, decorators, modifiers); + return parseAccessorDeclaration(SyntaxKind.SetAccessor, fullStart, decorators, modifiers, /* allowQuestionToken */ false); } return undefined; @@ -4285,7 +4285,7 @@ namespace ts { const decorators = parseDecorators(); const modifiers = parseModifiers(); - const accessor = tryParseAccessorDeclaration(fullStart, decorators, modifiers); + const accessor = tryParseAccessorDeclaration(fullStart, decorators, modifiers, /* allowQuestionToken */ false); if (accessor) { return accessor; } @@ -5193,11 +5193,14 @@ namespace ts { return parseInitializer(/*inParameter*/ false); } - function parseAccessorDeclaration(kind: SyntaxKind, fullStart: number, decorators: NodeArray, modifiers: NodeArray): AccessorDeclaration { + function parseAccessorDeclaration(kind: SyntaxKind, fullStart: number, decorators: NodeArray, modifiers: NodeArray, allowQuestionToken: boolean): AccessorDeclaration { const node = createNode(kind, fullStart); node.decorators = decorators; node.modifiers = modifiers; node.name = parsePropertyName(); + if (allowQuestionToken) { + node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken); + } fillSignature(SyntaxKind.ColonToken, /*yieldContext*/ false, /*awaitContext*/ false, /*requireCompleteParameterList*/ false, node); node.body = parseFunctionBlockOrSemicolon(/*isGenerator*/ false, /*isAsync*/ false); return addJSDocComment(finishNode(node)); @@ -5373,7 +5376,7 @@ namespace ts { const decorators = parseDecorators(); const modifiers = parseModifiers(/*permitInvalidConstAsModifier*/ true); - const accessor = tryParseAccessorDeclaration(fullStart, decorators, modifiers); + const accessor = tryParseAccessorDeclaration(fullStart, decorators, modifiers, /* allowQuestionToken */ true); if (accessor) { return accessor; } diff --git a/src/compiler/utilities.ts b/src/compiler/utilities.ts index 77f1e43c9cbaa..97203a53cdf56 100644 --- a/src/compiler/utilities.ts +++ b/src/compiler/utilities.ts @@ -1411,6 +1411,7 @@ namespace ts { case SyntaxKind.Parameter: case SyntaxKind.MethodDeclaration: case SyntaxKind.MethodSignature: + case SyntaxKind.GetAccessor: case SyntaxKind.ShorthandPropertyAssignment: case SyntaxKind.PropertyAssignment: case SyntaxKind.PropertyDeclaration: diff --git a/tests/baselines/reference/optionalClassGetters.errors.txt b/tests/baselines/reference/optionalClassGetters.errors.txt new file mode 100644 index 0000000000000..450ff87258cc8 --- /dev/null +++ b/tests/baselines/reference/optionalClassGetters.errors.txt @@ -0,0 +1,102 @@ +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(1,15): error TS2349: Cannot invoke an expression whose type lacks a call signature. Type '{ readonly a: void; }' has no compatible call signatures. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(2,10): error TS1005: '(' expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(2,11): error TS1136: Property assignment expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(2,14): error TS1005: ',' expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(3,16): error TS1005: ':' expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(3,17): error TS1005: ',' expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(4,5): error TS1128: Declaration or statement expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(5,1): error TS1128: Declaration or statement expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(11,10): error TS1005: '(' expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(11,12): error TS1068: Unexpected token. A constructor, method, accessor, or property was expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(11,24): error TS1005: '=>' expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(12,1): error TS1128: Declaration or statement expected. +tests/cases/conformance/types/namedTypes/optionalClassGetters.ts(34,14): error TS2532: Object is possibly 'undefined'. + + +==== tests/cases/conformance/types/namedTypes/optionalClassGetters.ts (13 errors) ==== + const test1 = { + ~ + get a?() { // Object literal getters may not be marked optional + ~~~~~~~~~~~~ +!!! error TS2349: Cannot invoke an expression whose type lacks a call signature. Type '{ readonly a: void; }' has no compatible call signatures. + ~ +!!! error TS1005: '(' expected. + ~ +!!! error TS1136: Property assignment expected. + ~ +!!! error TS1005: ',' expected. + return 2; + ~ +!!! error TS1005: ':' expected. + ~ +!!! error TS1005: ',' expected. + } + ~ +!!! error TS1128: Declaration or statement expected. + } + ~ +!!! error TS1128: Declaration or statement expected. + + class Foo { + get a? () { + return 1; + } + set a? (v: number) { } // Setters may not be marked optional + ~ +!!! error TS1005: '(' expected. + ~ +!!! error TS1068: Unexpected token. A constructor, method, accessor, or property was expected. + ~ +!!! error TS1005: '=>' expected. + } + ~ +!!! error TS1128: Declaration or statement expected. + + class Bar { + get a() { + return 1; + } + get b?(): string; // Body of optional getter can be omitted + get c?() { + return 'foo'; + } + get d?() { + return 'bar'; + } + } + + function test2(x: Bar) { + x.a; + x.b; + x.c; + x.d; + let a1 = x.a; + let b1 = x.b; + let c1 = x.c.toString(); + ~~~ +!!! error TS2532: Object is possibly 'undefined'. + let d1 = x.d && x.d.toString(); + } + + class Base { + get f?(): number; + } + + class Derived extends Base { + get f(): number { return 1; } + } + + class Person { + firstName: string; + lastName: string; + + get fullName?() { + return this.firstName + ' ' + this.lastName; + } + } + + const person: Person = { + firstName: 'foo', + lastName: 'bar' + } + \ No newline at end of file diff --git a/tests/baselines/reference/optionalClassGetters.js b/tests/baselines/reference/optionalClassGetters.js new file mode 100644 index 0000000000000..65193eebc76d4 --- /dev/null +++ b/tests/baselines/reference/optionalClassGetters.js @@ -0,0 +1,197 @@ +//// [optionalClassGetters.ts] +const test1 = { + get a?() { // Object literal getters may not be marked optional + return 2; + } +} + +class Foo { + get a? () { + return 1; + } + set a? (v: number) { } // Setters may not be marked optional +} + +class Bar { + get a() { + return 1; + } + get b?(): string; // Body of optional getter can be omitted + get c?() { + return 'foo'; + } + get d?() { + return 'bar'; + } +} + +function test2(x: Bar) { + x.a; + x.b; + x.c; + x.d; + let a1 = x.a; + let b1 = x.b; + let c1 = x.c.toString(); + let d1 = x.d && x.d.toString(); +} + +class Base { + get f?(): number; +} + +class Derived extends Base { + get f(): number { return 1; } +} + +class Person { + firstName: string; + lastName: string; + + get fullName?() { + return this.firstName + ' ' + this.lastName; + } +} + +const person: Person = { + firstName: 'foo', + lastName: 'bar' +} + + +//// [optionalClassGetters.js] +var __extends = (this && this.__extends) || (function () { + var extendStatics = Object.setPrototypeOf || + ({ __proto__: [] } instanceof Array && function (d, b) { d.__proto__ = b; }) || + function (d, b) { for (var p in b) if (b.hasOwnProperty(p)) d[p] = b[p]; }; + return function (d, b) { + extendStatics(d, b); + function __() { this.constructor = d; } + d.prototype = b === null ? Object.create(b) : (__.prototype = b.prototype, new __()); + }; +})(); +var test1 = { + get a() { } +}(), // Object literal getters may not be marked optional +_a = (void 0)["return"], // Object literal getters may not be marked optional + = _a === void 0 ? 2 : _a; +var Foo = (function () { + function Foo() { + } + Object.defineProperty(Foo.prototype, "a", { + get: function () { + return 1; + }, + set: function () { }, + enumerable: true, + configurable: true + }); + return Foo; +}()); +(function (v) { }); // Setters may not be marked optional +var Bar = (function () { + function Bar() { + } + Object.defineProperty(Bar.prototype, "a", { + get: function () { + return 1; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Bar.prototype, "b", { + get: function () { } // Body of optional getter can be omitted + , + enumerable: true, + configurable: true + }); + Object.defineProperty(Bar.prototype, "c", { + get: function () { + return 'foo'; + }, + enumerable: true, + configurable: true + }); + Object.defineProperty(Bar.prototype, "d", { + get: function () { + return 'bar'; + }, + enumerable: true, + configurable: true + }); + return Bar; +}()); +function test2(x) { + x.a; + x.b; + x.c; + x.d; + var a1 = x.a; + var b1 = x.b; + var c1 = x.c.toString(); + var d1 = x.d && x.d.toString(); +} +var Base = (function () { + function Base() { + } + Object.defineProperty(Base.prototype, "f", { + get: function () { }, + enumerable: true, + configurable: true + }); + return Base; +}()); +var Derived = (function (_super) { + __extends(Derived, _super); + function Derived() { + return _super !== null && _super.apply(this, arguments) || this; + } + Object.defineProperty(Derived.prototype, "f", { + get: function () { return 1; }, + enumerable: true, + configurable: true + }); + return Derived; +}(Base)); +var Person = (function () { + function Person() { + } + Object.defineProperty(Person.prototype, "fullName", { + get: function () { + return this.firstName + ' ' + this.lastName; + }, + enumerable: true, + configurable: true + }); + return Person; +}()); +var person = { + firstName: 'foo', + lastName: 'bar' +}; + + +//// [optionalClassGetters.d.ts] +declare const test1: any, : number; +declare class Foo { + a?: number | undefined; +} +declare class Bar { + readonly a: number; + readonly b?: string; + readonly c?: string | undefined; + readonly d?: string | undefined; +} +declare function test2(x: Bar): void; +declare class Base { + readonly f?: number; +} +declare class Derived extends Base { + readonly f: number; +} +declare class Person { + firstName: string; + lastName: string; + readonly fullName?: string | undefined; +} +declare const person: Person; diff --git a/tests/cases/conformance/types/namedTypes/optionalClassGetters.ts b/tests/cases/conformance/types/namedTypes/optionalClassGetters.ts new file mode 100644 index 0000000000000..3f2a20a21bd43 --- /dev/null +++ b/tests/cases/conformance/types/namedTypes/optionalClassGetters.ts @@ -0,0 +1,61 @@ +// @strictNullChecks: true +// @declaration: true + +const test1 = { + get a?() { // Object literal getters may not be marked optional + return 2; + } +} + +class Foo { + get a? () { + return 1; + } + set a? (v: number) { } // Setters may not be marked optional +} + +class Bar { + get a() { + return 1; + } + get b?(): string; // Body of optional getter can be omitted + get c?() { + return 'foo'; + } + get d?() { + return 'bar'; + } +} + +function test2(x: Bar) { + x.a; + x.b; + x.c; + x.d; + let a1 = x.a; + let b1 = x.b; + let c1 = x.c.toString(); + let d1 = x.d && x.d.toString(); +} + +class Base { + get f?(): number; +} + +class Derived extends Base { + get f(): number { return 1; } +} + +class Person { + firstName: string; + lastName: string; + + get fullName?() { + return this.firstName + ' ' + this.lastName; + } +} + +const person: Person = { + firstName: 'foo', + lastName: 'bar' +}