Skip to content

Nullable variable declarations #1

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

Open
wants to merge 3 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
23 changes: 17 additions & 6 deletions src/compiler/checker.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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
Expand All @@ -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)) {
Expand Down
2 changes: 1 addition & 1 deletion src/compiler/emitter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
}
Expand Down
41 changes: 34 additions & 7 deletions src/compiler/factory/nodeFactory.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<VariableDeclaration>(
SyntaxKind.VariableDeclaration,
/*decorators*/ undefined,
Expand All @@ -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;
}

Expand Down
41 changes: 32 additions & 9 deletions src/compiler/parser.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Token<SyntaxKind.ExclamationToken>>();

if (questionOrExclamationToken) {
switch (token()) {
case SyntaxKind.ExclamationToken:
exclamationToken = parseTokenNode<Token<SyntaxKind.ExclamationToken>>();
break;
case SyntaxKind.QuestionToken:
questionToken = parseTokenNode<Token<SyntaxKind.QuestionToken>>();
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);
}

Expand Down Expand Up @@ -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);
}
Expand Down
4 changes: 3 additions & 1 deletion src/compiler/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -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
Expand Down Expand Up @@ -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;
Expand Down
10 changes: 10 additions & 0 deletions testing.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
"use strict";
var a;
a = undefined;
// function testing()?: string {
// return "test";
// }
// let a: string | undefined;
// function testing(): string {
// return "test";
// }
14 changes: 14 additions & 0 deletions testing.ts
Original file line number Diff line number Diff line change
@@ -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";
// }