diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index 49c45384e6df5..5903d3ef83c2a 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -8179,8 +8179,11 @@ namespace ts { const isOptional = includeOptionality && ( isParameter(declaration) && isJSDocOptionalParameter(declaration) || isOptionalJSDocPropertyLikeTag(declaration) - || !isBindingElement(declaration) && !isVariableDeclaration(declaration) && !!declaration.questionToken); + || !isBindingElement(declaration) && !!declaration.questionToken); + if (isOptional) { + console.log(declaration); + } // Use type from type annotation if one is present const declaredType = tryGetTypeFromEffectiveTypeNode(declaration); if (declaredType) { @@ -10808,9 +10811,14 @@ namespace ts { // When creating an optional property in strictNullChecks mode, if 'undefined' isn't assignable to the // type, we include 'undefined' in the type. Similarly, when creating a non-optional property in strictNullChecks // mode, if the underlying property is optional we remove 'undefined' from the type. - let type = strictNullChecks && symbol.flags & SymbolFlags.Optional && !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) ? getOptionalType(propType) : - symbol.checkFlags & CheckFlags.StripOptional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : - propType; + let type = + strictNullChecks && + symbol.flags & SymbolFlags.Optional && + !maybeTypeOfKind(propType, TypeFlags.Undefined | TypeFlags.Void) + ? getOptionalType(propType) + : symbol.checkFlags & CheckFlags.StripOptional + ? getTypeWithFacts(propType, TypeFacts.NEUndefined) + : propType; if (!popTypeResolution()) { error(currentNode, Diagnostics.Type_of_property_0_circularly_references_itself_in_mapped_type_1, symbolToString(symbol), typeToString(mappedType)); type = errorType; @@ -40494,7 +40502,7 @@ namespace ts { } } } - + // Disallow non-null assertion without initializer if (node.exclamationToken && (node.parent.parent.kind !== SyntaxKind.VariableStatement || !node.type || node.initializer || node.flags & NodeFlags.Ambient)) { const message = node.initializer ? Diagnostics.Declarations_with_initializers_cannot_also_have_definite_assignment_assertions @@ -40503,7 +40511,10 @@ namespace ts { : Diagnostics.A_definite_assignment_assertion_is_not_permitted_in_this_context; return grammarErrorOnNode(node.exclamationToken, message); } - + // ... + // No need to check for questionToken as optional variables only + // imply type union with `undefined` ... + // ... const moduleKind = getEmitModuleKind(compilerOptions); if (moduleKind < ModuleKind.ES2015 && moduleKind !== ModuleKind.System && !(node.parent.parent.flags & NodeFlags.Ambient) && hasSyntacticModifier(node.parent.parent, ModifierFlags.Export)) { diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 24174483fdc6d..3acd5adcf9a6a 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -2937,7 +2937,7 @@ namespace ts { function emitVariableDeclaration(node: VariableDeclaration) { emit(node.name); - emit(node.exclamationToken); + emit(node.questionToken || node.exclamationToken); emitTypeAnnotation(node.type); emitInitializer(node.initializer, node.type ? node.type.end : node.name.end, node); } diff --git a/src/compiler/factory/nodeFactory.ts b/src/compiler/factory/nodeFactory.ts index 5f3a19a8949ac..f92f453f28c68 100644 --- a/src/compiler/factory/nodeFactory.ts +++ b/src/compiler/factory/nodeFactory.ts @@ -3428,7 +3428,12 @@ namespace ts { } // @api - function createVariableDeclaration(name: string | BindingName, exclamationToken: ExclamationToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) { + function createVariableDeclaration( + name: string | BindingName, + questionOrExclamationToken?: QuestionToken | ExclamationToken, + type?: TypeNode, + initializer?: Expression, + ) { const node = createBaseVariableLikeDeclaration( SyntaxKind.VariableDeclaration, /*decorators*/ undefined, @@ -3437,21 +3442,43 @@ namespace ts { type, initializer && parenthesizerRules().parenthesizeExpressionForDisallowedComma(initializer) ); - node.exclamationToken = exclamationToken; - node.transformFlags |= propagateChildFlags(node.exclamationToken); - if (exclamationToken) { + if (questionOrExclamationToken) { node.transformFlags |= TransformFlags.ContainsTypeScript; + if (isQuestionToken(questionOrExclamationToken)) { + node.questionToken = questionOrExclamationToken; + node.flags |= SymbolFlags.Optional; + } + else if (isExclamationToken(questionOrExclamationToken)) { + node.exclamationToken = questionOrExclamationToken; + } } + node.transformFlags |= + propagateChildFlags(node.questionToken) | propagateChildFlags(node.exclamationToken); return node; } // @api - function updateVariableDeclaration(node: VariableDeclaration, name: BindingName, exclamationToken: ExclamationToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined) { + function updateVariableDeclaration( + node: VariableDeclaration, + name: BindingName, + questionOrExclamationToken?: QuestionToken | ExclamationToken, + type?: TypeNode, + initializer?: Expression, + ) { return node.name !== name || node.type !== type - || node.exclamationToken !== exclamationToken + || node.questionToken !== questionOrExclamationToken + || node.exclamationToken !== questionOrExclamationToken || node.initializer !== initializer - ? update(createVariableDeclaration(name, exclamationToken, type, initializer), node) + ? update( + createVariableDeclaration( + name, + questionOrExclamationToken, + type, + initializer, + ), + node + ) : node; } diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index cd61ad8ec807d..9a56ef96279c1 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -6247,21 +6247,40 @@ namespace ts { return parseBindingIdentifier(privateIdentifierDiagnosticMessage); } - function parseVariableDeclarationAllowExclamation() { - return parseVariableDeclaration(/*allowExclamation*/ true); + function parseVariableDeclarationAllowNullableSpecifiers() { + return parseVariableDeclaration(/*questionOrExclamationToken*/ true); } - function parseVariableDeclaration(allowExclamation?: boolean): VariableDeclaration { + function parseVariableDeclaration(questionOrExclamationToken?: boolean): VariableDeclaration { const pos = getNodePos(); const name = parseIdentifierOrPattern(Diagnostics.Private_identifiers_are_not_allowed_in_variable_declarations); + /** + * These definitions can be changed to the following form after this + * functionality is added: + * + * let questionToken?: QuestionToken; + */ + let questionToken: QuestionToken | undefined; let exclamationToken: ExclamationToken | undefined; - if (allowExclamation && name.kind === SyntaxKind.Identifier && - token() === SyntaxKind.ExclamationToken && !scanner.hasPrecedingLineBreak()) { - exclamationToken = parseTokenNode>(); + + if (questionOrExclamationToken) { + switch (token()) { + case SyntaxKind.ExclamationToken: + exclamationToken = parseTokenNode>(); + break; + case SyntaxKind.QuestionToken: + questionToken = parseTokenNode>(); + break; + } } const type = parseTypeAnnotation(); const initializer = isInOrOfKeyword(token()) ? undefined : parseInitializer(); - const node = factory.createVariableDeclaration(name, exclamationToken, type, initializer); + const node = factory.createVariableDeclaration( + name, + questionToken || exclamationToken, + type, + initializer, + ); return finishNode(node, pos); } @@ -6301,8 +6320,12 @@ namespace ts { const savedDisallowIn = inDisallowInContext(); setDisallowInContext(inForStatementInitializer); - declarations = parseDelimitedList(ParsingContext.VariableDeclarations, - inForStatementInitializer ? parseVariableDeclaration : parseVariableDeclarationAllowExclamation); + declarations = parseDelimitedList( + ParsingContext.VariableDeclarations, + inForStatementInitializer + ? parseVariableDeclaration + : parseVariableDeclarationAllowNullableSpecifiers + ); setDisallowInContext(savedDisallowIn); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index 4352e2f2171e3..244f10251b652 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -1243,6 +1243,7 @@ namespace ts { readonly parent: VariableDeclarationList | CatchClause; readonly name: BindingName; // Declared variable name readonly exclamationToken?: ExclamationToken; // Optional definite assignment assertion + readonly questionToken?: QuestionToken; // Present on optional variable readonly type?: TypeNode; // Optional type annotation readonly initializer?: Expression; // Optional initializer } @@ -4806,6 +4807,7 @@ namespace ts { HasNeverType = 1 << 17, // Synthetic property with at least one never type in constituents Mapped = 1 << 18, // Property of mapped type StripOptional = 1 << 19, // Strip optionality in mapped property + OptionalVariable = 1 << 20, // Optional variable Synthetic = SyntheticProperty | SyntheticMethod, Discriminant = HasNonUniformType | HasLiteralType, Partial = ReadPartial | WritePartial @@ -7071,7 +7073,7 @@ namespace ts { createTryStatement(tryBlock: Block, catchClause: CatchClause | undefined, finallyBlock: Block | undefined): TryStatement; updateTryStatement(node: TryStatement, tryBlock: Block, catchClause: CatchClause | undefined, finallyBlock: Block | undefined): TryStatement; createDebuggerStatement(): DebuggerStatement; - createVariableDeclaration(name: string | BindingName, exclamationToken?: ExclamationToken, type?: TypeNode, initializer?: Expression): VariableDeclaration; + createVariableDeclaration(name: string | BindingName, questionOrExclamationToken?: QuestionToken | ExclamationToken, type?: TypeNode, initializer?: Expression, QuestionToken?: QuestionToken): VariableDeclaration; updateVariableDeclaration(node: VariableDeclaration, name: BindingName, exclamationToken: ExclamationToken | undefined, type: TypeNode | undefined, initializer: Expression | undefined): VariableDeclaration; createVariableDeclarationList(declarations: readonly VariableDeclaration[], flags?: NodeFlags): VariableDeclarationList; updateVariableDeclarationList(node: VariableDeclarationList, declarations: readonly VariableDeclaration[]): VariableDeclarationList; diff --git a/testing.js b/testing.js new file mode 100644 index 0000000000000..3f16ab47c94fd --- /dev/null +++ b/testing.js @@ -0,0 +1,10 @@ +"use strict"; +var a; +a = undefined; +// function testing()?: string { +// return "test"; +// } +// let a: string | undefined; +// function testing(): string { +// return "test"; +// } diff --git a/testing.ts b/testing.ts new file mode 100644 index 0000000000000..439bd4a3aff4d --- /dev/null +++ b/testing.ts @@ -0,0 +1,14 @@ +interface TestingInterface { + a?: string; +} + +let a?: string; +a = undefined; +// function testing()?: string { +// return "test"; +// } + +// let a: string | undefined; +// function testing(): string { +// return "test"; +// } \ No newline at end of file