From a629acd8fd4844742fdd01ab6cf55afa9377db0e Mon Sep 17 00:00:00 2001 From: Anders Hejlsberg Date: Mon, 12 Feb 2018 16:20:49 -0800 Subject: [PATCH] Allow +/- to prefix 'readonly' and '?' modifiers in mapped types --- src/compiler/checker.ts | 66 +++++++++++++++++++----------- src/compiler/declarationEmitter.ts | 8 +++- src/compiler/emitter.ts | 12 ++++-- src/compiler/factory.ts | 4 +- src/compiler/parser.ts | 36 ++++++++++------ src/compiler/types.ts | 6 ++- 6 files changed, 86 insertions(+), 46 deletions(-) diff --git a/src/compiler/checker.ts b/src/compiler/checker.ts index a745a384128..75ab23407b3 100644 --- a/src/compiler/checker.ts +++ b/src/compiler/checker.ts @@ -572,8 +572,10 @@ namespace ts { } const enum MappedTypeModifiers { - Readonly = 1 << 0, - Optional = 1 << 1, + IncludeReadonly = 1 << 0, + ExcludeReadonly = 1 << 1, + IncludeOptional = 1 << 2, + ExcludeOptional = 1 << 3, } const enum ExpandingFlags { @@ -2967,11 +2969,10 @@ namespace ts { function createMappedTypeNodeFromType(type: MappedType) { Debug.assert(!!(type.flags & TypeFlags.Object)); - const readonlyToken = type.declaration && type.declaration.readonlyToken ? createToken(SyntaxKind.ReadonlyKeyword) : undefined; - const questionToken = type.declaration && type.declaration.questionToken ? createToken(SyntaxKind.QuestionToken) : undefined; + const readonlyToken = type.declaration.readonlyToken ? createToken(type.declaration.readonlyToken.kind) : undefined; + const questionToken = type.declaration.questionToken ? createToken(type.declaration.questionToken.kind) : undefined; const typeParameterNode = typeParameterToDeclaration(getTypeParameterFromMappedType(type), context, getConstraintTypeFromMappedType(type)); const templateTypeNode = typeToTypeNodeHelper(getTemplateTypeFromMappedType(type), context); - const mappedTypeNode = createMappedTypeNode(readonlyToken, typeParameterNode, questionToken, templateTypeNode); return setEmitFlags(mappedTypeNode, EmitFlags.SingleLine); } @@ -5819,8 +5820,9 @@ namespace ts { function resolveReverseMappedTypeMembers(type: ReverseMappedType) { const indexInfo = getIndexInfoOfType(type.source, IndexKind.String); - const readonlyMask = type.mappedType.declaration.readonlyToken ? false : true; - const optionalMask = type.mappedType.declaration.questionToken ? 0 : SymbolFlags.Optional; + const modifiers = getMappedTypeModifiers(type.mappedType); + const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true; + const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional; const stringIndexInfo = indexInfo && createIndexInfo(inferReverseMappedType(indexInfo.type, type.mappedType), readonlyMask && indexInfo.isReadonly); const members = createSymbolTable(); for (const prop of getPropertiesOfType(type.source)) { @@ -5846,8 +5848,7 @@ namespace ts { const constraintType = getConstraintTypeFromMappedType(type); const templateType = getTemplateTypeFromMappedType(type.target || type); const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T' - const templateReadonly = !!type.declaration.readonlyToken; - const templateOptional = !!type.declaration.questionToken; + const templateModifiers = getMappedTypeModifiers(type); const constraintDeclaration = type.declaration.typeParameter.constraint; if (constraintDeclaration.kind === SyntaxKind.TypeOperator && (constraintDeclaration).operator === SyntaxKind.KeyOfKeyword) { @@ -5888,10 +5889,17 @@ namespace ts { if (t.flags & TypeFlags.StringLiteral) { const propName = escapeLeadingUnderscores((t).value); const modifiersProp = getPropertyOfType(modifiersType, propName); - const isOptional = templateOptional || !!(modifiersProp && modifiersProp.flags & SymbolFlags.Optional); - const checkFlags = templateReadonly || modifiersProp && isReadonlySymbol(modifiersProp) ? CheckFlags.Readonly : 0; - const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, checkFlags); - prop.type = propType; + const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional || + !(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional); + const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly || + !(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp)); + const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, isReadonly ? CheckFlags.Readonly : 0); + // 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. + prop.type = strictNullChecks && isOptional && !isTypeAssignableTo(undefinedType, propType) ? getOptionalType(propType) : + strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) : + propType; if (propertySymbol) { prop.syntheticOrigin = propertySymbol; prop.declarations = propertySymbol.declarations; @@ -5900,7 +5908,7 @@ namespace ts { members.set(propName, prop); } else if (t.flags & (TypeFlags.Any | TypeFlags.String)) { - stringIndexInfo = createIndexInfo(propType, templateReadonly); + stringIndexInfo = createIndexInfo(propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly)); } } } @@ -5918,7 +5926,7 @@ namespace ts { function getTemplateTypeFromMappedType(type: MappedType) { return type.templateType || (type.templateType = type.declaration.type ? - instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), !!type.declaration.questionToken), type.mapper || identityMapper) : + instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), type.mapper || identityMapper) : unknownType); } @@ -5946,18 +5954,24 @@ namespace ts { } function getMappedTypeModifiers(type: MappedType): MappedTypeModifiers { - return (type.declaration.readonlyToken ? MappedTypeModifiers.Readonly : 0) | - (type.declaration.questionToken ? MappedTypeModifiers.Optional : 0); + const declaration = type.declaration; + return (declaration.readonlyToken ? declaration.readonlyToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeReadonly : MappedTypeModifiers.IncludeReadonly : 0) | + (declaration.questionToken ? declaration.questionToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0); } - function getCombinedMappedTypeModifiers(type: MappedType): MappedTypeModifiers { + function getMappedTypeOptionality(type: MappedType): number { + const modifiers = getMappedTypeModifiers(type); + return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0; + } + + function getCombinedMappedTypeOptionality(type: MappedType): number { + const optionality = getMappedTypeOptionality(type); const modifiersType = getModifiersTypeFromMappedType(type); - return getMappedTypeModifiers(type) | - (isGenericMappedType(modifiersType) ? getMappedTypeModifiers(modifiersType) : 0); + return optionality || (isGenericMappedType(modifiersType) ? getMappedTypeOptionality(modifiersType) : 0); } function isPartialMappedType(type: Type) { - return getObjectFlags(type) & ObjectFlags.Mapped && !!(type).declaration.questionToken; + return !!(getObjectFlags(type) & ObjectFlags.Mapped && getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional); } function isGenericMappedType(type: Type): type is MappedType { @@ -9960,7 +9974,7 @@ namespace ts { if (target.flags & TypeFlags.TypeParameter) { // A source type { [P in keyof T]: X } is related to a target type T if X is related to T[P]. if (getObjectFlags(source) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(source) === getIndexType(target)) { - if (!(source).declaration.questionToken) { + if (!(getMappedTypeModifiers(source) & MappedTypeModifiers.IncludeOptional)) { const templateType = getTemplateTypeFromMappedType(source); const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(source)); if (result = isRelatedTo(templateType, indexedAccessType, reportErrors)) { @@ -9999,6 +10013,8 @@ namespace ts { else if (isGenericMappedType(target)) { // A source type T is related to a target type { [P in X]: T[P] } const template = getTemplateTypeFromMappedType(target); + const modifiers = getMappedTypeModifiers(target); + if (!(modifiers & MappedTypeModifiers.ExcludeOptional)) { if (template.flags & TypeFlags.IndexedAccess && (template).objectType === source && (template).indexType === getTypeParameterFromMappedType(target)) { return Ternary.True; @@ -10013,6 +10029,7 @@ namespace ts { } } } + } if (source.flags & TypeFlags.TypeParameter) { let constraint = getConstraintForRelation(source); @@ -10162,8 +10179,7 @@ namespace ts { function mappedTypeRelatedTo(source: MappedType, target: MappedType, reportErrors: boolean): Ternary { const modifiersRelated = relation === comparableRelation || ( relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) : - !(getCombinedMappedTypeModifiers(source) & MappedTypeModifiers.Optional) || - getCombinedMappedTypeModifiers(target) & MappedTypeModifiers.Optional); + getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target)); if (modifiersRelated) { let result: Ternary; if (result = isRelatedTo(getConstraintTypeFromMappedType(target), getConstraintTypeFromMappedType(source), reportErrors)) { @@ -20345,7 +20361,7 @@ namespace ts { const indexType = (type).indexType; if (isTypeAssignableTo(indexType, getIndexType(objectType))) { if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) && - getObjectFlags(objectType) & ObjectFlags.Mapped && (objectType).declaration.readonlyToken) { + getObjectFlags(objectType) & ObjectFlags.Mapped && getMappedTypeModifiers(objectType) & MappedTypeModifiers.IncludeReadonly) { error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType)); } return type; diff --git a/src/compiler/declarationEmitter.ts b/src/compiler/declarationEmitter.ts index 24646ff31eb..f18a32889aa 100644 --- a/src/compiler/declarationEmitter.ts +++ b/src/compiler/declarationEmitter.ts @@ -593,7 +593,9 @@ namespace ts { writeLine(); increaseIndent(); if (node.readonlyToken) { - write("readonly "); + write(node.readonlyToken.kind === SyntaxKind.PlusToken ? "+readonly " : + node.readonlyToken.kind === SyntaxKind.MinusToken ? "-readonly " : + "readonly "); } write("["); writeEntityName(node.typeParameter.name); @@ -601,7 +603,9 @@ namespace ts { emitType(node.typeParameter.constraint); write("]"); if (node.questionToken) { - write("?"); + write(node.questionToken.kind === SyntaxKind.PlusToken ? "+?" : + node.questionToken.kind === SyntaxKind.MinusToken ? "-?" : + "?"); } write(": "); emitType(node.type); diff --git a/src/compiler/emitter.ts b/src/compiler/emitter.ts index 3c9866b3536..735e0d67f47 100644 --- a/src/compiler/emitter.ts +++ b/src/compiler/emitter.ts @@ -1251,14 +1251,20 @@ namespace ts { } if (node.readonlyToken) { emit(node.readonlyToken); + if (node.readonlyToken.kind !== SyntaxKind.ReadonlyKeyword) { + writeKeyword("readonly"); + } writeSpace(); } - writePunctuation("["); pipelineEmitWithNotification(EmitHint.MappedTypeParameter, node.typeParameter); writePunctuation("]"); - - emitIfPresent(node.questionToken); + if (node.questionToken) { + emit(node.questionToken); + if (node.questionToken.kind !== SyntaxKind.QuestionToken) { + writePunctuation("?"); + } + } writePunctuation(":"); writeSpace(); emit(node.type); diff --git a/src/compiler/factory.ts b/src/compiler/factory.ts index 165c70b37e6..8166196e469 100644 --- a/src/compiler/factory.ts +++ b/src/compiler/factory.ts @@ -804,7 +804,7 @@ namespace ts { : node; } - export function createMappedTypeNode(readonlyToken: ReadonlyToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | undefined, type: TypeNode | undefined): MappedTypeNode { + export function createMappedTypeNode(readonlyToken: ReadonlyToken | PlusToken | MinusToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | PlusToken | MinusToken | undefined, type: TypeNode | undefined): MappedTypeNode { const node = createSynthesizedNode(SyntaxKind.MappedType) as MappedTypeNode; node.readonlyToken = readonlyToken; node.typeParameter = typeParameter; @@ -813,7 +813,7 @@ namespace ts { return node; } - export function updateMappedTypeNode(node: MappedTypeNode, readonlyToken: ReadonlyToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | undefined, type: TypeNode | undefined): MappedTypeNode { + export function updateMappedTypeNode(node: MappedTypeNode, readonlyToken: ReadonlyToken | PlusToken | MinusToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | PlusToken | MinusToken | undefined, type: TypeNode | undefined): MappedTypeNode { return node.readonlyToken !== readonlyToken || node.typeParameter !== typeParameter || node.questionToken !== questionToken diff --git a/src/compiler/parser.ts b/src/compiler/parser.ts index 3209f234f85..b71983b9783 100644 --- a/src/compiler/parser.ts +++ b/src/compiler/parser.ts @@ -695,7 +695,7 @@ namespace ts { else if (token() === SyntaxKind.OpenBraceToken || lookAhead(() => token() === SyntaxKind.StringLiteral)) { result.jsonObject = parseObjectLiteralExpression(); - sourceFile.endOfFileToken = parseExpectedToken(SyntaxKind.EndOfFileToken, /*reportAtCurrentPosition*/ false, Diagnostics.Unexpected_token); + sourceFile.endOfFileToken = parseExpectedToken(SyntaxKind.EndOfFileToken, Diagnostics.Unexpected_token); } else { parseExpected(SyntaxKind.OpenBraceToken); @@ -1135,10 +1135,10 @@ namespace ts { return undefined; } - function parseExpectedToken(t: TKind, reportAtCurrentPosition: boolean, diagnosticMessage: DiagnosticMessage, arg0?: any): Token; - function parseExpectedToken(t: SyntaxKind, reportAtCurrentPosition: boolean, diagnosticMessage: DiagnosticMessage, arg0?: any): Node { + function parseExpectedToken(t: TKind, diagnosticMessage?: DiagnosticMessage, arg0?: any): Token; + function parseExpectedToken(t: SyntaxKind, diagnosticMessage?: DiagnosticMessage, arg0?: any): Node { return parseOptionalToken(t) || - createMissingNode(t, reportAtCurrentPosition, diagnosticMessage, arg0); + createMissingNode(t, /*reportAtCurrentPosition*/ false, diagnosticMessage || Diagnostics._0_expected, arg0 || tokenToString(t)); } function parseTokenNode(): T { @@ -2113,7 +2113,7 @@ namespace ts { literal = parseTemplateMiddleOrTemplateTail(); } else { - literal = parseExpectedToken(SyntaxKind.TemplateTail, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, tokenToString(SyntaxKind.CloseBraceToken)); + literal = parseExpectedToken(SyntaxKind.TemplateTail, Diagnostics._0_expected, tokenToString(SyntaxKind.CloseBraceToken)); } span.literal = literal; @@ -2607,6 +2607,9 @@ namespace ts { function isStartOfMappedType() { nextToken(); + if (token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { + return nextToken() === SyntaxKind.ReadonlyKeyword; + } if (token() === SyntaxKind.ReadonlyKeyword) { nextToken(); } @@ -2624,11 +2627,21 @@ namespace ts { function parseMappedType() { const node = createNode(SyntaxKind.MappedType); parseExpected(SyntaxKind.OpenBraceToken); - node.readonlyToken = parseOptionalToken(SyntaxKind.ReadonlyKeyword); + if (token() === SyntaxKind.ReadonlyKeyword || token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { + node.readonlyToken = parseTokenNode(); + if (node.readonlyToken.kind !== SyntaxKind.ReadonlyKeyword) { + parseExpectedToken(SyntaxKind.ReadonlyKeyword); + } + } parseExpected(SyntaxKind.OpenBracketToken); node.typeParameter = parseMappedTypeParameter(); parseExpected(SyntaxKind.CloseBracketToken); - node.questionToken = parseOptionalToken(SyntaxKind.QuestionToken); + if (token() === SyntaxKind.QuestionToken || token() === SyntaxKind.PlusToken || token() === SyntaxKind.MinusToken) { + node.questionToken = parseTokenNode(); + if (node.questionToken.kind !== SyntaxKind.QuestionToken) { + parseExpectedToken(SyntaxKind.QuestionToken); + } + } node.type = parseTypeAnnotation(); parseSemicolon(); parseExpected(SyntaxKind.CloseBraceToken); @@ -3242,7 +3255,7 @@ namespace ts { node.parameters = createNodeArray([parameter], parameter.pos, parameter.end); - node.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, "=>"); + node.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken); node.body = parseArrowFunctionExpressionBody(/*isAsync*/ !!asyncModifier); return addJSDocComment(finishNode(node)); @@ -3273,7 +3286,7 @@ namespace ts { // If we have an arrow, then try to parse the body. Even if not, try to parse if we // have an opening brace, just in case we're in an error state. const lastToken = token(); - arrowFunction.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, "=>"); + arrowFunction.equalsGreaterThanToken = parseExpectedToken(SyntaxKind.EqualsGreaterThanToken); arrowFunction.body = (lastToken === SyntaxKind.EqualsGreaterThanToken || lastToken === SyntaxKind.OpenBraceToken) ? parseArrowFunctionExpressionBody(isAsync) : parseIdentifier(); @@ -3539,8 +3552,7 @@ namespace ts { node.condition = leftOperand; node.questionToken = questionToken; node.whenTrue = doOutsideOfContext(disallowInAndDecoratorContext, parseAssignmentExpressionOrHigher); - node.colonToken = parseExpectedToken(SyntaxKind.ColonToken, /*reportAtCurrentPosition*/ false, - Diagnostics._0_expected, tokenToString(SyntaxKind.ColonToken)); + node.colonToken = parseExpectedToken(SyntaxKind.ColonToken); node.whenFalse = nodeIsPresent(node.colonToken) ? parseAssignmentExpressionOrHigher() : createMissingNode(SyntaxKind.Identifier, /*reportAtCurrentPosition*/ false, Diagnostics._0_expected, tokenToString(SyntaxKind.ColonToken)); @@ -4014,7 +4026,7 @@ namespace ts { // If it wasn't then just try to parse out a '.' and report an error. const node = createNode(SyntaxKind.PropertyAccessExpression, expression.pos); node.expression = expression; - parseExpectedToken(SyntaxKind.DotToken, /*reportAtCurrentPosition*/ false, Diagnostics.super_must_be_followed_by_an_argument_list_or_member_access); + parseExpectedToken(SyntaxKind.DotToken, Diagnostics.super_must_be_followed_by_an_argument_list_or_member_access); node.name = parseRightSideOfDot(/*allowIdentifierNames*/ true); return finishNode(node); } diff --git a/src/compiler/types.ts b/src/compiler/types.ts index b74ed84e809..f4a4fb8a79e 100644 --- a/src/compiler/types.ts +++ b/src/compiler/types.ts @@ -661,6 +661,8 @@ namespace ts { export type AtToken = Token; export type ReadonlyToken = Token; export type AwaitKeywordToken = Token; + export type PlusToken = Token; + export type MinusToken = Token; export type Modifier = Token @@ -1158,9 +1160,9 @@ namespace ts { export interface MappedTypeNode extends TypeNode, Declaration { kind: SyntaxKind.MappedType; - readonlyToken?: ReadonlyToken; + readonlyToken?: ReadonlyToken | PlusToken | MinusToken; typeParameter: TypeParameterDeclaration; - questionToken?: QuestionToken; + questionToken?: QuestionToken | PlusToken | MinusToken; type?: TypeNode; }