Skip to content

Commit 9a22d08

Browse files
committed
Merge pull request #8625 from Microsoft/optionalClassProperties
Optional class properties
2 parents 0795c8d + 8498ef1 commit 9a22d08

17 files changed

+729
-70
lines changed

src/compiler/checker.ts

+24-20
Original file line numberDiff line numberDiff line change
@@ -79,6 +79,7 @@ namespace ts {
7979
getIndexTypeOfType,
8080
getBaseTypes,
8181
getReturnTypeOfSignature,
82+
getNonNullableType,
8283
getSymbolsInScope,
8384
getSymbolAtLocation,
8485
getShorthandAssignmentValueSymbol,
@@ -2884,6 +2885,10 @@ namespace ts {
28842885
return undefined;
28852886
}
28862887

2888+
function addOptionality(type: Type, optional: boolean): Type {
2889+
return strictNullChecks && optional ? addNullableKind(type, TypeFlags.Undefined) : type;
2890+
}
2891+
28872892
// Return the inferred type for a variable, parameter, or property declaration
28882893
function getTypeForVariableLikeDeclaration(declaration: VariableLikeDeclaration): Type {
28892894
if (declaration.flags & NodeFlags.JavaScriptFile) {
@@ -2915,8 +2920,7 @@ namespace ts {
29152920

29162921
// Use type from type annotation if one is present
29172922
if (declaration.type) {
2918-
const type = getTypeFromTypeNode(declaration.type);
2919-
return strictNullChecks && declaration.questionToken ? addNullableKind(type, TypeFlags.Undefined) : type;
2923+
return addOptionality(getTypeFromTypeNode(declaration.type), /*optional*/ !!declaration.questionToken);
29202924
}
29212925

29222926
if (declaration.kind === SyntaxKind.Parameter) {
@@ -2938,13 +2942,13 @@ namespace ts {
29382942
? getContextuallyTypedThisType(func)
29392943
: getContextuallyTypedParameterType(<ParameterDeclaration>declaration);
29402944
if (type) {
2941-
return strictNullChecks && declaration.questionToken ? addNullableKind(type, TypeFlags.Undefined) : type;
2945+
return addOptionality(type, /*optional*/ !!declaration.questionToken);
29422946
}
29432947
}
29442948

29452949
// Use the type of the initializer expression if one is present
29462950
if (declaration.initializer) {
2947-
return checkExpressionCached(declaration.initializer);
2951+
return addOptionality(checkExpressionCached(declaration.initializer), /*optional*/ !!declaration.questionToken);
29482952
}
29492953

29502954
// If it is a short-hand property assignment, use the type of the identifier
@@ -3215,7 +3219,9 @@ namespace ts {
32153219
function getTypeOfFuncClassEnumModule(symbol: Symbol): Type {
32163220
const links = getSymbolLinks(symbol);
32173221
if (!links.type) {
3218-
links.type = createObjectType(TypeFlags.Anonymous, symbol);
3222+
const type = createObjectType(TypeFlags.Anonymous, symbol);
3223+
links.type = strictNullChecks && symbol.flags & SymbolFlags.Optional ?
3224+
addNullableKind(type, TypeFlags.Undefined) : type;
32193225
}
32203226
return links.type;
32213227
}
@@ -7614,11 +7620,12 @@ namespace ts {
76147620
getInitialTypeOfBindingElement(<BindingElement>node);
76157621
}
76167622

7617-
function getFlowTypeOfReference(reference: Node, declaredType: Type, initialType: Type) {
7623+
function getFlowTypeOfReference(reference: Node, declaredType: Type, assumeInitialized: boolean) {
76187624
let key: string;
7619-
if (!reference.flowNode || declaredType === initialType && !(declaredType.flags & TypeFlags.Narrowable)) {
7625+
if (!reference.flowNode || assumeInitialized && !(declaredType.flags & TypeFlags.Narrowable)) {
76207626
return declaredType;
76217627
}
7628+
const initialType = assumeInitialized ? declaredType : addNullableKind(declaredType, TypeFlags.Undefined);
76227629
const visitedFlowStart = visitedFlowCount;
76237630
const result = getTypeAtFlowNode(reference.flowNode);
76247631
visitedFlowCount = visitedFlowStart;
@@ -8092,11 +8099,11 @@ namespace ts {
80928099
return type;
80938100
}
80948101
const declaration = localOrExportSymbol.valueDeclaration;
8095-
const defaultsToDeclaredType = !strictNullChecks || type.flags & TypeFlags.Any || !declaration ||
8102+
const assumeInitialized = !strictNullChecks || (type.flags & TypeFlags.Any) !== 0 || !declaration ||
80968103
getRootDeclaration(declaration).kind === SyntaxKind.Parameter || isInAmbientContext(declaration) ||
80978104
getContainingFunctionOrModule(declaration) !== getContainingFunctionOrModule(node);
8098-
const flowType = getFlowTypeOfReference(node, type, defaultsToDeclaredType ? type : addNullableKind(type, TypeFlags.Undefined));
8099-
if (strictNullChecks && !(type.flags & TypeFlags.Any) && !(getNullableKind(type) & TypeFlags.Undefined) && getNullableKind(flowType) & TypeFlags.Undefined) {
8105+
const flowType = getFlowTypeOfReference(node, type, assumeInitialized);
8106+
if (!assumeInitialized && !(getNullableKind(type) & TypeFlags.Undefined) && getNullableKind(flowType) & TypeFlags.Undefined) {
81008107
error(node, Diagnostics.Variable_0_is_used_before_being_assigned, symbolToString(symbol));
81018108
// Return the declared type to reduce follow-on errors
81028109
return type;
@@ -8344,7 +8351,7 @@ namespace ts {
83448351
if (isClassLike(container.parent)) {
83458352
const symbol = getSymbolOfNode(container.parent);
83468353
const type = container.flags & NodeFlags.Static ? getTypeOfSymbol(symbol) : (<InterfaceType>getDeclaredTypeOfSymbol(symbol)).thisType;
8347-
return getFlowTypeOfReference(node, type, type);
8354+
return getFlowTypeOfReference(node, type, /*assumeInitialized*/ true);
83488355
}
83498356

83508357
if (isInJavaScriptFile(node)) {
@@ -9919,7 +9926,8 @@ namespace ts {
99199926
}
99209927

99219928
const propType = getTypeOfSymbol(prop);
9922-
if (node.kind !== SyntaxKind.PropertyAccessExpression || !(prop.flags & (SymbolFlags.Variable | SymbolFlags.Property | SymbolFlags.Accessor)) || isAssignmentTarget(node)) {
9929+
if (node.kind !== SyntaxKind.PropertyAccessExpression || isAssignmentTarget(node) ||
9930+
!(propType.flags & TypeFlags.Union) && !(prop.flags & (SymbolFlags.Variable | SymbolFlags.Property | SymbolFlags.Accessor))) {
99239931
return propType;
99249932
}
99259933
const leftmostNode = getLeftmostIdentifierOrThis(node);
@@ -9936,7 +9944,7 @@ namespace ts {
99369944
return propType;
99379945
}
99389946
}
9939-
return getFlowTypeOfReference(node, propType, propType);
9947+
return getFlowTypeOfReference(node, propType, /*assumeInitialized*/ true);
99409948
}
99419949

99429950
function isValidPropertyAccess(node: PropertyAccessExpression | QualifiedName, propertyName: string): boolean {
@@ -13400,7 +13408,7 @@ namespace ts {
1340013408

1340113409
// Abstract methods can't have an implementation -- in particular, they don't need one.
1340213410
if (!isExportSymbolInsideModule && lastSeenNonAmbientDeclaration && !lastSeenNonAmbientDeclaration.body &&
13403-
!(lastSeenNonAmbientDeclaration.flags & NodeFlags.Abstract)) {
13411+
!(lastSeenNonAmbientDeclaration.flags & NodeFlags.Abstract) && !lastSeenNonAmbientDeclaration.questionToken) {
1340413412
reportImplementationExpectedError(lastSeenNonAmbientDeclaration);
1340513413
}
1340613414

@@ -18290,7 +18298,7 @@ namespace ts {
1829018298
}
1829118299

1829218300
if (node.parent.kind === SyntaxKind.ObjectLiteralExpression) {
18293-
if (checkGrammarForInvalidQuestionMark(node, node.questionToken, Diagnostics.A_class_member_cannot_be_declared_optional)) {
18301+
if (checkGrammarForInvalidQuestionMark(node, node.questionToken, Diagnostics.An_object_member_cannot_be_declared_optional)) {
1829418302
return true;
1829518303
}
1829618304
else if (node.body === undefined) {
@@ -18299,9 +18307,6 @@ namespace ts {
1829918307
}
1830018308

1830118309
if (isClassLike(node.parent)) {
18302-
if (checkGrammarForInvalidQuestionMark(node, node.questionToken, Diagnostics.A_class_member_cannot_be_declared_optional)) {
18303-
return true;
18304-
}
1830518310
// Technically, computed properties in ambient contexts is disallowed
1830618311
// for property declarations and accessors too, not just methods.
1830718312
// However, property declarations disallow computed names in general,
@@ -18523,8 +18528,7 @@ namespace ts {
1852318528

1852418529
function checkGrammarProperty(node: PropertyDeclaration) {
1852518530
if (isClassLike(node.parent)) {
18526-
if (checkGrammarForInvalidQuestionMark(node, node.questionToken, Diagnostics.A_class_member_cannot_be_declared_optional) ||
18527-
checkGrammarForNonSymbolComputedProperty(node.name, Diagnostics.A_computed_property_name_in_a_class_property_declaration_must_directly_refer_to_a_built_in_symbol)) {
18531+
if (checkGrammarForNonSymbolComputedProperty(node.name, Diagnostics.A_computed_property_name_in_a_class_property_declaration_must_directly_refer_to_a_built_in_symbol)) {
1852818532
return true;
1852918533
}
1853018534
}

src/compiler/declarationEmitter.ts

+1-1
Original file line numberDiff line numberDiff line change
@@ -1129,7 +1129,7 @@ namespace ts {
11291129
// what we want, namely the name expression enclosed in brackets.
11301130
writeTextOfNode(currentText, node.name);
11311131
// If optional property emit ?
1132-
if ((node.kind === SyntaxKind.PropertyDeclaration || node.kind === SyntaxKind.PropertySignature) && hasQuestionToken(node)) {
1132+
if ((node.kind === SyntaxKind.PropertyDeclaration || node.kind === SyntaxKind.PropertySignature || node.kind === SyntaxKind.Parameter) && hasQuestionToken(node)) {
11331133
write("?");
11341134
}
11351135
if ((node.kind === SyntaxKind.PropertyDeclaration || node.kind === SyntaxKind.PropertySignature) && node.parent.kind === SyntaxKind.TypeLiteral) {

src/compiler/diagnosticMessages.json

-4
Original file line numberDiff line numberDiff line change
@@ -315,10 +315,6 @@
315315
"category": "Error",
316316
"code": 1110
317317
},
318-
"A class member cannot be declared optional.": {
319-
"category": "Error",
320-
"code": 1112
321-
},
322318
"A 'default' clause cannot appear more than once in a 'switch' statement.": {
323319
"category": "Error",
324320
"code": 1113

src/compiler/types.ts

+1
Original file line numberDiff line numberDiff line change
@@ -1772,6 +1772,7 @@ namespace ts {
17721772
getIndexTypeOfType(type: Type, kind: IndexKind): Type;
17731773
getBaseTypes(type: InterfaceType): ObjectType[];
17741774
getReturnTypeOfSignature(signature: Signature): Type;
1775+
getNonNullableType(type: Type): Type;
17751776

17761777
getSymbolsInScope(location: Node, meaning: SymbolFlags): Symbol[];
17771778
getSymbolAtLocation(node: Node): Symbol;

src/services/services.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,7 @@ namespace ts {
5050
getStringIndexType(): Type;
5151
getNumberIndexType(): Type;
5252
getBaseTypes(): ObjectType[];
53+
getNonNullableType(): Type;
5354
}
5455

5556
export interface Signature {
@@ -735,6 +736,9 @@ namespace ts {
735736
? this.checker.getBaseTypes(<InterfaceType><Type>this)
736737
: undefined;
737738
}
739+
getNonNullableType(): Type {
740+
return this.checker.getNonNullableType(this);
741+
}
738742
}
739743

740744
class SignatureObject implements Signature {
@@ -4366,7 +4370,7 @@ namespace ts {
43664370
(location.kind === SyntaxKind.ConstructorKeyword && location.parent.kind === SyntaxKind.Constructor)) { // At constructor keyword of constructor declaration
43674371
// get the signature from the declaration and write it
43684372
const functionDeclaration = <FunctionLikeDeclaration>location.parent;
4369-
const allSignatures = functionDeclaration.kind === SyntaxKind.Constructor ? type.getConstructSignatures() : type.getCallSignatures();
4373+
const allSignatures = functionDeclaration.kind === SyntaxKind.Constructor ? type.getNonNullableType().getConstructSignatures() : type.getNonNullableType().getCallSignatures();
43704374
if (!typeChecker.isImplementationOfOverload(functionDeclaration)) {
43714375
signature = typeChecker.getSignatureFromDeclaration(functionDeclaration);
43724376
}
@@ -4564,7 +4568,7 @@ namespace ts {
45644568
symbolFlags & SymbolFlags.Signature ||
45654569
symbolFlags & SymbolFlags.Accessor ||
45664570
symbolKind === ScriptElementKind.memberFunctionElement) {
4567-
const allSignatures = type.getCallSignatures();
4571+
const allSignatures = type.getNonNullableType().getCallSignatures();
45684572
addSignatureDisplayParts(allSignatures[0], allSignatures);
45694573
}
45704574
}

tests/baselines/reference/classWithOptionalParameter.errors.txt

-26
This file was deleted.
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
=== tests/cases/conformance/types/namedTypes/classWithOptionalParameter.ts ===
2+
// classes do not permit optional parameters, these are errors
3+
4+
class C {
5+
>C : Symbol(C, Decl(classWithOptionalParameter.ts, 0, 0))
6+
7+
x?: string;
8+
>x : Symbol(C.x, Decl(classWithOptionalParameter.ts, 2, 9))
9+
10+
f?() {}
11+
>f : Symbol(C.f, Decl(classWithOptionalParameter.ts, 3, 15))
12+
}
13+
14+
class C2<T> {
15+
>C2 : Symbol(C2, Decl(classWithOptionalParameter.ts, 5, 1))
16+
>T : Symbol(T, Decl(classWithOptionalParameter.ts, 7, 9))
17+
18+
x?: T;
19+
>x : Symbol(C2.x, Decl(classWithOptionalParameter.ts, 7, 13))
20+
>T : Symbol(T, Decl(classWithOptionalParameter.ts, 7, 9))
21+
22+
f?(x: T) {}
23+
>f : Symbol(C2.f, Decl(classWithOptionalParameter.ts, 8, 10))
24+
>x : Symbol(x, Decl(classWithOptionalParameter.ts, 9, 7))
25+
>T : Symbol(T, Decl(classWithOptionalParameter.ts, 7, 9))
26+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
=== tests/cases/conformance/types/namedTypes/classWithOptionalParameter.ts ===
2+
// classes do not permit optional parameters, these are errors
3+
4+
class C {
5+
>C : C
6+
7+
x?: string;
8+
>x : string
9+
10+
f?() {}
11+
>f : () => void
12+
}
13+
14+
class C2<T> {
15+
>C2 : C2<T>
16+
>T : T
17+
18+
x?: T;
19+
>x : T
20+
>T : T
21+
22+
f?(x: T) {}
23+
>f : (x: T) => void
24+
>x : T
25+
>T : T
26+
}

tests/baselines/reference/declFileConstructors.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -247,7 +247,7 @@ export declare class ConstructorWithPrivateParameterProperty {
247247
constructor(x: string);
248248
}
249249
export declare class ConstructorWithOptionalParameterProperty {
250-
x: string;
250+
x?: string;
251251
constructor(x?: string);
252252
}
253253
export declare class ConstructorWithParameterInitializer {
@@ -281,7 +281,7 @@ declare class GlobalConstructorWithPrivateParameterProperty {
281281
constructor(x: string);
282282
}
283283
declare class GlobalConstructorWithOptionalParameterProperty {
284-
x: string;
284+
x?: string;
285285
constructor(x?: string);
286286
}
287287
declare class GlobalConstructorWithParameterInitializer {
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
tests/cases/compiler/objectLiteralMemberWithQuestionMark1.ts(1,14): error TS1112: A class member cannot be declared optional.
1+
tests/cases/compiler/objectLiteralMemberWithQuestionMark1.ts(1,14): error TS1162: An object member cannot be declared optional.
22

33

44
==== tests/cases/compiler/objectLiteralMemberWithQuestionMark1.ts (1 errors) ====
55
var v = { foo?() { } }
66
~
7-
!!! error TS1112: A class member cannot be declared optional.
7+
!!! error TS1162: An object member cannot be declared optional.

tests/baselines/reference/objectTypesWithOptionalProperties.errors.txt

+3-9
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,7 @@
1-
tests/cases/conformance/types/objectTypeLiteral/methodSignatures/objectTypesWithOptionalProperties.ts(12,6): error TS1112: A class member cannot be declared optional.
2-
tests/cases/conformance/types/objectTypeLiteral/methodSignatures/objectTypesWithOptionalProperties.ts(20,6): error TS1112: A class member cannot be declared optional.
31
tests/cases/conformance/types/objectTypeLiteral/methodSignatures/objectTypesWithOptionalProperties.ts(24,6): error TS1162: An object member cannot be declared optional.
42

53

6-
==== tests/cases/conformance/types/objectTypeLiteral/methodSignatures/objectTypesWithOptionalProperties.ts (3 errors) ====
4+
==== tests/cases/conformance/types/objectTypeLiteral/methodSignatures/objectTypesWithOptionalProperties.ts (1 errors) ====
75
// Basic uses of optional properties
86

97
var a: {
@@ -15,19 +13,15 @@ tests/cases/conformance/types/objectTypeLiteral/methodSignatures/objectTypesWith
1513
}
1614

1715
class C {
18-
x?: number; // error
19-
~
20-
!!! error TS1112: A class member cannot be declared optional.
16+
x?: number; // ok
2117
}
2218

2319
interface I2<T> {
2420
x?: T; // ok
2521
}
2622

2723
class C2<T> {
28-
x?: T; // error
29-
~
30-
!!! error TS1112: A class member cannot be declared optional.
24+
x?: T; // ok
3125
}
3226

3327
var b = {

tests/baselines/reference/objectTypesWithOptionalProperties.js

+2-2
Original file line numberDiff line numberDiff line change
@@ -10,15 +10,15 @@ interface I {
1010
}
1111

1212
class C {
13-
x?: number; // error
13+
x?: number; // ok
1414
}
1515

1616
interface I2<T> {
1717
x?: T; // ok
1818
}
1919

2020
class C2<T> {
21-
x?: T; // error
21+
x?: T; // ok
2222
}
2323

2424
var b = {

0 commit comments

Comments
 (0)