Skip to content

Commit f8a378a

Browse files
authored
Merge pull request #21919 from Microsoft/mappedTypeModifiers
Improved control over mapped type modifiers
2 parents 274bb5d + 57fe347 commit f8a378a

14 files changed

+1828
-54
lines changed

src/compiler/checker.ts

+41-25
Original file line numberDiff line numberDiff line change
@@ -572,8 +572,10 @@ namespace ts {
572572
}
573573

574574
const enum MappedTypeModifiers {
575-
Readonly = 1 << 0,
576-
Optional = 1 << 1,
575+
IncludeReadonly = 1 << 0,
576+
ExcludeReadonly = 1 << 1,
577+
IncludeOptional = 1 << 2,
578+
ExcludeOptional = 1 << 3,
577579
}
578580

579581
const enum ExpandingFlags {
@@ -2967,11 +2969,10 @@ namespace ts {
29672969

29682970
function createMappedTypeNodeFromType(type: MappedType) {
29692971
Debug.assert(!!(type.flags & TypeFlags.Object));
2970-
const readonlyToken = type.declaration && type.declaration.readonlyToken ? createToken(SyntaxKind.ReadonlyKeyword) : undefined;
2971-
const questionToken = type.declaration && type.declaration.questionToken ? createToken(SyntaxKind.QuestionToken) : undefined;
2972+
const readonlyToken = type.declaration.readonlyToken ? <ReadonlyToken | PlusToken | MinusToken>createToken(type.declaration.readonlyToken.kind) : undefined;
2973+
const questionToken = type.declaration.questionToken ? <QuestionToken | PlusToken | MinusToken>createToken(type.declaration.questionToken.kind) : undefined;
29722974
const typeParameterNode = typeParameterToDeclaration(getTypeParameterFromMappedType(type), context, getConstraintTypeFromMappedType(type));
29732975
const templateTypeNode = typeToTypeNodeHelper(getTemplateTypeFromMappedType(type), context);
2974-
29752976
const mappedTypeNode = createMappedTypeNode(readonlyToken, typeParameterNode, questionToken, templateTypeNode);
29762977
return setEmitFlags(mappedTypeNode, EmitFlags.SingleLine);
29772978
}
@@ -5819,8 +5820,9 @@ namespace ts {
58195820

58205821
function resolveReverseMappedTypeMembers(type: ReverseMappedType) {
58215822
const indexInfo = getIndexInfoOfType(type.source, IndexKind.String);
5822-
const readonlyMask = type.mappedType.declaration.readonlyToken ? false : true;
5823-
const optionalMask = type.mappedType.declaration.questionToken ? 0 : SymbolFlags.Optional;
5823+
const modifiers = getMappedTypeModifiers(type.mappedType);
5824+
const readonlyMask = modifiers & MappedTypeModifiers.IncludeReadonly ? false : true;
5825+
const optionalMask = modifiers & MappedTypeModifiers.IncludeOptional ? 0 : SymbolFlags.Optional;
58245826
const stringIndexInfo = indexInfo && createIndexInfo(inferReverseMappedType(indexInfo.type, type.mappedType), readonlyMask && indexInfo.isReadonly);
58255827
const members = createSymbolTable();
58265828
for (const prop of getPropertiesOfType(type.source)) {
@@ -5846,8 +5848,7 @@ namespace ts {
58465848
const constraintType = getConstraintTypeFromMappedType(type);
58475849
const templateType = getTemplateTypeFromMappedType(<MappedType>type.target || type);
58485850
const modifiersType = getApparentType(getModifiersTypeFromMappedType(type)); // The 'T' in 'keyof T'
5849-
const templateReadonly = !!type.declaration.readonlyToken;
5850-
const templateOptional = !!type.declaration.questionToken;
5851+
const templateModifiers = getMappedTypeModifiers(type);
58515852
const constraintDeclaration = type.declaration.typeParameter.constraint;
58525853
if (constraintDeclaration.kind === SyntaxKind.TypeOperator &&
58535854
(<TypeOperatorNode>constraintDeclaration).operator === SyntaxKind.KeyOfKeyword) {
@@ -5888,10 +5889,17 @@ namespace ts {
58885889
if (t.flags & TypeFlags.StringLiteral) {
58895890
const propName = escapeLeadingUnderscores((<StringLiteralType>t).value);
58905891
const modifiersProp = getPropertyOfType(modifiersType, propName);
5891-
const isOptional = templateOptional || !!(modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
5892-
const checkFlags = templateReadonly || modifiersProp && isReadonlySymbol(modifiersProp) ? CheckFlags.Readonly : 0;
5893-
const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, checkFlags);
5894-
prop.type = propType;
5892+
const isOptional = !!(templateModifiers & MappedTypeModifiers.IncludeOptional ||
5893+
!(templateModifiers & MappedTypeModifiers.ExcludeOptional) && modifiersProp && modifiersProp.flags & SymbolFlags.Optional);
5894+
const isReadonly = !!(templateModifiers & MappedTypeModifiers.IncludeReadonly ||
5895+
!(templateModifiers & MappedTypeModifiers.ExcludeReadonly) && modifiersProp && isReadonlySymbol(modifiersProp));
5896+
const prop = createSymbol(SymbolFlags.Property | (isOptional ? SymbolFlags.Optional : 0), propName, isReadonly ? CheckFlags.Readonly : 0);
5897+
// When creating an optional property in strictNullChecks mode, if 'undefined' isn't assignable to the
5898+
// type, we include 'undefined' in the type. Similarly, when creating a non-optional property in strictNullChecks
5899+
// mode, if the underlying property is optional we remove 'undefined' from the type.
5900+
prop.type = strictNullChecks && isOptional && !isTypeAssignableTo(undefinedType, propType) ? getOptionalType(propType) :
5901+
strictNullChecks && !isOptional && modifiersProp && modifiersProp.flags & SymbolFlags.Optional ? getTypeWithFacts(propType, TypeFacts.NEUndefined) :
5902+
propType;
58955903
if (propertySymbol) {
58965904
prop.syntheticOrigin = propertySymbol;
58975905
prop.declarations = propertySymbol.declarations;
@@ -5900,7 +5908,7 @@ namespace ts {
59005908
members.set(propName, prop);
59015909
}
59025910
else if (t.flags & (TypeFlags.Any | TypeFlags.String)) {
5903-
stringIndexInfo = createIndexInfo(propType, templateReadonly);
5911+
stringIndexInfo = createIndexInfo(propType, !!(templateModifiers & MappedTypeModifiers.IncludeReadonly));
59045912
}
59055913
}
59065914
}
@@ -5918,7 +5926,7 @@ namespace ts {
59185926
function getTemplateTypeFromMappedType(type: MappedType) {
59195927
return type.templateType ||
59205928
(type.templateType = type.declaration.type ?
5921-
instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), !!type.declaration.questionToken), type.mapper || identityMapper) :
5929+
instantiateType(addOptionality(getTypeFromTypeNode(type.declaration.type), !!(getMappedTypeModifiers(type) & MappedTypeModifiers.IncludeOptional)), type.mapper || identityMapper) :
59225930
unknownType);
59235931
}
59245932

@@ -5946,18 +5954,24 @@ namespace ts {
59465954
}
59475955

59485956
function getMappedTypeModifiers(type: MappedType): MappedTypeModifiers {
5949-
return (type.declaration.readonlyToken ? MappedTypeModifiers.Readonly : 0) |
5950-
(type.declaration.questionToken ? MappedTypeModifiers.Optional : 0);
5957+
const declaration = type.declaration;
5958+
return (declaration.readonlyToken ? declaration.readonlyToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeReadonly : MappedTypeModifiers.IncludeReadonly : 0) |
5959+
(declaration.questionToken ? declaration.questionToken.kind === SyntaxKind.MinusToken ? MappedTypeModifiers.ExcludeOptional : MappedTypeModifiers.IncludeOptional : 0);
5960+
}
5961+
5962+
function getMappedTypeOptionality(type: MappedType): number {
5963+
const modifiers = getMappedTypeModifiers(type);
5964+
return modifiers & MappedTypeModifiers.ExcludeOptional ? -1 : modifiers & MappedTypeModifiers.IncludeOptional ? 1 : 0;
59515965
}
59525966

5953-
function getCombinedMappedTypeModifiers(type: MappedType): MappedTypeModifiers {
5967+
function getCombinedMappedTypeOptionality(type: MappedType): number {
5968+
const optionality = getMappedTypeOptionality(type);
59545969
const modifiersType = getModifiersTypeFromMappedType(type);
5955-
return getMappedTypeModifiers(type) |
5956-
(isGenericMappedType(modifiersType) ? getMappedTypeModifiers(<MappedType>modifiersType) : 0);
5970+
return optionality || (isGenericMappedType(modifiersType) ? getMappedTypeOptionality(<MappedType>modifiersType) : 0);
59575971
}
59585972

59595973
function isPartialMappedType(type: Type) {
5960-
return getObjectFlags(type) & ObjectFlags.Mapped && !!(<MappedType>type).declaration.questionToken;
5974+
return !!(getObjectFlags(type) & ObjectFlags.Mapped && getMappedTypeModifiers(<MappedType>type) & MappedTypeModifiers.IncludeOptional);
59615975
}
59625976

59635977
function isGenericMappedType(type: Type): type is MappedType {
@@ -9960,7 +9974,7 @@ namespace ts {
99609974
if (target.flags & TypeFlags.TypeParameter) {
99619975
// A source type { [P in keyof T]: X } is related to a target type T if X is related to T[P].
99629976
if (getObjectFlags(source) & ObjectFlags.Mapped && getConstraintTypeFromMappedType(<MappedType>source) === getIndexType(target)) {
9963-
if (!(<MappedType>source).declaration.questionToken) {
9977+
if (!(getMappedTypeModifiers(<MappedType>source) & MappedTypeModifiers.IncludeOptional)) {
99649978
const templateType = getTemplateTypeFromMappedType(<MappedType>source);
99659979
const indexedAccessType = getIndexedAccessType(target, getTypeParameterFromMappedType(<MappedType>source));
99669980
if (result = isRelatedTo(templateType, indexedAccessType, reportErrors)) {
@@ -9999,6 +10013,8 @@ namespace ts {
999910013
else if (isGenericMappedType(target)) {
1000010014
// A source type T is related to a target type { [P in X]: T[P] }
1000110015
const template = getTemplateTypeFromMappedType(<MappedType>target);
10016+
const modifiers = getMappedTypeModifiers(<MappedType>target);
10017+
if (!(modifiers & MappedTypeModifiers.ExcludeOptional)) {
1000210018
if (template.flags & TypeFlags.IndexedAccess && (<IndexedAccessType>template).objectType === source &&
1000310019
(<IndexedAccessType>template).indexType === getTypeParameterFromMappedType(<MappedType>target)) {
1000410020
return Ternary.True;
@@ -10013,6 +10029,7 @@ namespace ts {
1001310029
}
1001410030
}
1001510031
}
10032+
}
1001610033

1001710034
if (source.flags & TypeFlags.TypeParameter) {
1001810035
let constraint = getConstraintForRelation(<TypeParameter>source);
@@ -10162,8 +10179,7 @@ namespace ts {
1016210179
function mappedTypeRelatedTo(source: MappedType, target: MappedType, reportErrors: boolean): Ternary {
1016310180
const modifiersRelated = relation === comparableRelation || (
1016410181
relation === identityRelation ? getMappedTypeModifiers(source) === getMappedTypeModifiers(target) :
10165-
!(getCombinedMappedTypeModifiers(source) & MappedTypeModifiers.Optional) ||
10166-
getCombinedMappedTypeModifiers(target) & MappedTypeModifiers.Optional);
10182+
getCombinedMappedTypeOptionality(source) <= getCombinedMappedTypeOptionality(target));
1016710183
if (modifiersRelated) {
1016810184
let result: Ternary;
1016910185
if (result = isRelatedTo(getConstraintTypeFromMappedType(<MappedType>target), getConstraintTypeFromMappedType(<MappedType>source), reportErrors)) {
@@ -20345,7 +20361,7 @@ namespace ts {
2034520361
const indexType = (<IndexedAccessType>type).indexType;
2034620362
if (isTypeAssignableTo(indexType, getIndexType(objectType))) {
2034720363
if (accessNode.kind === SyntaxKind.ElementAccessExpression && isAssignmentTarget(accessNode) &&
20348-
getObjectFlags(objectType) & ObjectFlags.Mapped && (<MappedType>objectType).declaration.readonlyToken) {
20364+
getObjectFlags(objectType) & ObjectFlags.Mapped && getMappedTypeModifiers(<MappedType>objectType) & MappedTypeModifiers.IncludeReadonly) {
2034920365
error(accessNode, Diagnostics.Index_signature_in_type_0_only_permits_reading, typeToString(objectType));
2035020366
}
2035120367
return type;

src/compiler/declarationEmitter.ts

+6-2
Original file line numberDiff line numberDiff line change
@@ -593,15 +593,19 @@ namespace ts {
593593
writeLine();
594594
increaseIndent();
595595
if (node.readonlyToken) {
596-
write("readonly ");
596+
write(node.readonlyToken.kind === SyntaxKind.PlusToken ? "+readonly " :
597+
node.readonlyToken.kind === SyntaxKind.MinusToken ? "-readonly " :
598+
"readonly ");
597599
}
598600
write("[");
599601
writeEntityName(node.typeParameter.name);
600602
write(" in ");
601603
emitType(node.typeParameter.constraint);
602604
write("]");
603605
if (node.questionToken) {
604-
write("?");
606+
write(node.questionToken.kind === SyntaxKind.PlusToken ? "+?" :
607+
node.questionToken.kind === SyntaxKind.MinusToken ? "-?" :
608+
"?");
605609
}
606610
write(": ");
607611
emitType(node.type);

src/compiler/emitter.ts

+9-3
Original file line numberDiff line numberDiff line change
@@ -1251,14 +1251,20 @@ namespace ts {
12511251
}
12521252
if (node.readonlyToken) {
12531253
emit(node.readonlyToken);
1254+
if (node.readonlyToken.kind !== SyntaxKind.ReadonlyKeyword) {
1255+
writeKeyword("readonly");
1256+
}
12541257
writeSpace();
12551258
}
1256-
12571259
writePunctuation("[");
12581260
pipelineEmitWithNotification(EmitHint.MappedTypeParameter, node.typeParameter);
12591261
writePunctuation("]");
1260-
1261-
emitIfPresent(node.questionToken);
1262+
if (node.questionToken) {
1263+
emit(node.questionToken);
1264+
if (node.questionToken.kind !== SyntaxKind.QuestionToken) {
1265+
writePunctuation("?");
1266+
}
1267+
}
12621268
writePunctuation(":");
12631269
writeSpace();
12641270
emit(node.type);

src/compiler/factory.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -804,7 +804,7 @@ namespace ts {
804804
: node;
805805
}
806806

807-
export function createMappedTypeNode(readonlyToken: ReadonlyToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | undefined, type: TypeNode | undefined): MappedTypeNode {
807+
export function createMappedTypeNode(readonlyToken: ReadonlyToken | PlusToken | MinusToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | PlusToken | MinusToken | undefined, type: TypeNode | undefined): MappedTypeNode {
808808
const node = createSynthesizedNode(SyntaxKind.MappedType) as MappedTypeNode;
809809
node.readonlyToken = readonlyToken;
810810
node.typeParameter = typeParameter;
@@ -813,7 +813,7 @@ namespace ts {
813813
return node;
814814
}
815815

816-
export function updateMappedTypeNode(node: MappedTypeNode, readonlyToken: ReadonlyToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | undefined, type: TypeNode | undefined): MappedTypeNode {
816+
export function updateMappedTypeNode(node: MappedTypeNode, readonlyToken: ReadonlyToken | PlusToken | MinusToken | undefined, typeParameter: TypeParameterDeclaration, questionToken: QuestionToken | PlusToken | MinusToken | undefined, type: TypeNode | undefined): MappedTypeNode {
817817
return node.readonlyToken !== readonlyToken
818818
|| node.typeParameter !== typeParameter
819819
|| node.questionToken !== questionToken

0 commit comments

Comments
 (0)