Skip to content
Draft
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
1 change: 1 addition & 0 deletions packages/typescript/src/api/proto.generated.ts
Original file line number Diff line number Diff line change
Expand Up @@ -1229,6 +1229,7 @@ export interface CompilerOptions {
disableSolutionSearching?: boolean;
disableReferencedProjectLoad?: boolean;
erasableSyntaxOnly?: boolean;
enforceReadonly?: boolean;
exactOptionalPropertyTypes?: boolean;
experimentalDecorators?: boolean;
forceConsistentCasingInFileNames?: boolean;
Expand Down
83 changes: 53 additions & 30 deletions tsc/internal/checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -615,6 +615,7 @@ type Checker struct {
noImplicitAny bool
noImplicitThis bool
useUnknownInCatchVariables bool
enforceReadonly bool
exactOptionalPropertyTypes bool
canCollectSymbolAliasAccessibilityData bool
wasCanceled bool
Expand Down Expand Up @@ -930,6 +931,7 @@ func NewChecker(program Program, tracer *Tracer) (*Checker, *sync.Mutex) {
c.noImplicitAny = c.compilerOptions.GetStrictOptionValue(c.compilerOptions.NoImplicitAny)
c.noImplicitThis = c.compilerOptions.GetStrictOptionValue(c.compilerOptions.NoImplicitThis)
c.useUnknownInCatchVariables = c.compilerOptions.GetStrictOptionValue(c.compilerOptions.UseUnknownInCatchVariables)
c.enforceReadonly = c.compilerOptions.EnforceReadonly == core.TSTrue
c.exactOptionalPropertyTypes = c.compilerOptions.ExactOptionalPropertyTypes == core.TSTrue
c.canCollectSymbolAliasAccessibilityData = c.compilerOptions.VerbatimModuleSyntax.IsFalseOrUnknown()
c.arrayVariances = []VarianceFlags{VarianceFlagsCovariant}
Expand Down Expand Up @@ -13266,10 +13268,6 @@ func (c *Checker) checkObjectLiteral(node *ast.Node, checkMode CheckMode) *Type
}
}
inConstContext := c.isConstContext(node)
var checkFlags ast.CheckFlags
if inConstContext {
checkFlags = ast.CheckFlagsReadonly
}
objectFlags := ObjectFlagsFreshLiteral
patternWithComputedProperties := false
hasComputedStringProperty := false
Expand Down Expand Up @@ -13330,6 +13328,10 @@ func (c *Checker) checkObjectLiteral(node *ast.Node, checkMode CheckMode) *Type
if computedNameType != nil && isTypeUsableAsPropertyName(computedNameType) {
nameType = computedNameType
}
checkFlags := ast.CheckFlags(0)
if inConstContext && !(c.enforceReadonly && contextualType != nil && c.isContextualPropertyMutable(contextualType, member.Name, nameType)) {
checkFlags = ast.CheckFlagsReadonly
}
var prop *ast.Symbol
if nameType != nil {
prop = c.newSymbolEx(ast.SymbolFlagsProperty|member.Flags, getPropertyNameFromType(nameType), checkFlags|ast.CheckFlagsLate)
Expand Down Expand Up @@ -29168,38 +29170,37 @@ func getMappedTypeModifiers(t *Type) MappedTypeModifiers {
return modifiers
}

// Return -1, 0, or 1, where -1 means optionality is stripped (i.e. -?), 0 means optionality is unchanged, and 1 means
// optionality is added (i.e. +?).
func getMappedTypeOptionality(t *Type) int {
modifiers := getMappedTypeModifiers(t)
switch {
case modifiers&MappedTypeModifiersExcludeOptional != 0:
return -1
case modifiers&MappedTypeModifiersIncludeOptional != 0:
return 1
}
return 0
}

// Return -1, 0, or 1, for stripped, unchanged, or added optionality respectively. When a homomorphic mapped type doesn't
// modify optionality, recursively consult the optionality of the type being mapped over to see if it strips or adds optionality.
// For intersections, return -1 or 1 when all constituents strip or add optionality, otherwise return 0.
func (c *Checker) getCombinedMappedTypeOptionality(t *Type) int {
// Return the effective modifiers of a mapped type. When a homomorphic mapped type doesn't include modifiers, instead
// recursively obtain the effective modifiers of the type being mapped over. For intersections, return 0 if the effective
// modifiers differ between constituent types.
func (c *Checker) getEffectiveMappedTypeModifiers(t *Type, mask MappedTypeModifiers) MappedTypeModifiers {
if t.objectFlags&ObjectFlagsMapped != 0 {
optionality := getMappedTypeOptionality(t)
if optionality != 0 {
return optionality
modifiers := getMappedTypeModifiers(t) & mask
if modifiers != 0 {
return modifiers
}
return c.getCombinedMappedTypeOptionality(c.getModifiersTypeFromMappedType(t))
return c.getEffectiveMappedTypeModifiers(c.getModifiersTypeFromMappedType(t), mask)
}
if t.flags&TypeFlagsIntersection != 0 {
optionality := c.getCombinedMappedTypeOptionality(t.Types()[0])
modifiers := c.getEffectiveMappedTypeModifiers(t.Types()[0], mask)
for _, t := range t.Types()[1:] {
if c.getCombinedMappedTypeOptionality(t) != optionality {
if c.getEffectiveMappedTypeModifiers(t, mask) != modifiers {
return 0
}
}
return optionality
return modifiers
}
return 0
}

// Return -1, 0, or 1, where -1 means modifier is stripped, 0 means modifier is unchanged, and 1 means modifier is added.
func (c *Checker) getMappedTypeModifiersRank(t *Type, mask MappedTypeModifiers) int {
modifiers := c.getEffectiveMappedTypeModifiers(t, mask)
if modifiers&(MappedTypeModifiersExcludeReadonly|MappedTypeModifiersExcludeOptional) != 0 {
return -1
}
if modifiers&(MappedTypeModifiersIncludeReadonly|MappedTypeModifiersIncludeOptional) != 0 {
return 1
}
return 0
}
Expand Down Expand Up @@ -29439,10 +29440,10 @@ func (c *Checker) substituteIndexedMappedType(objectType *Type, index *Type) *Ty
mapper := newSimpleTypeMapper(c.getTypeParameterFromMappedType(objectType), index)
templateMapper := c.combineTypeMappers(objectType.AsMappedType().mapper, mapper)
instantiatedTemplateType := c.instantiateType(c.getTemplateTypeFromMappedType(core.OrElse(objectType.AsMappedType().target, objectType)), templateMapper)
isOptional := getMappedTypeOptionality(objectType) > 0
isOptional := getMappedTypeModifiers(objectType)&MappedTypeModifiersIncludeOptional != 0
if !isOptional {
if c.isGenericType(objectType) {
isOptional = c.getCombinedMappedTypeOptionality(c.getModifiersTypeFromMappedType(objectType)) > 0
isOptional = c.getEffectiveMappedTypeModifiers(c.getModifiersTypeFromMappedType(objectType), MappedTypeModifiersIncludeOptional|MappedTypeModifiersExcludeOptional)&MappedTypeModifiersIncludeOptional != 0
} else {
isOptional = c.couldAccessOptionalProperty(objectType, index)
}
Expand Down Expand Up @@ -30751,6 +30752,28 @@ func (c *Checker) getTypeOfPropertyOfContextualTypeEx(t *Type, name string, name
}, true /*noReductions*/)
}

func (c *Checker) isContextualPropertyMutable(t *Type, name string, nameType *Type) bool {
return someType(t, func(t *Type) bool {
propertyName := name
if nameType != nil {
if !isTypeUsableAsPropertyName(nameType) {
return false
}
propertyName = getPropertyNameFromType(nameType)
}
if prop := c.getPropertyOfType(t, propertyName); prop != nil {
return !c.isReadonlySymbol(prop)
}
if nameType == nil {
nameType = c.getStringLiteralType(name)
}
if indexInfo := c.findApplicableIndexInfo(c.getIndexInfosOfStructuredType(t), nameType); indexInfo != nil {
return !indexInfo.isReadonly
}
return false
})
}

func (c *Checker) getIndexedMappedTypeSubstitutedTypeOfContextualType(t *Type, name string, nameType *Type) *Type {
propertyNameType := nameType
if propertyNameType == nil {
Expand Down
37 changes: 27 additions & 10 deletions tsc/internal/checker/relater.go
Original file line number Diff line number Diff line change
Expand Up @@ -3455,7 +3455,8 @@ func (r *Relater) structuredTypeRelatedToWorker(source *Type, target *Type, repo
case target.flags&TypeFlagsTypeParameter != 0:
// A source type { [P in Q]: X } is related to a target type T if keyof T is related to Q and X is related to T[Q].
if source.objectFlags&ObjectFlagsMapped != 0 && source.AsMappedType().declaration.NameType == nil && r.isRelatedTo(r.c.getIndexType(target), r.c.getConstraintTypeFromMappedType(source), RecursionFlagsBoth, false) != TernaryFalse {
if getMappedTypeModifiers(source)&MappedTypeModifiersIncludeOptional == 0 {
if getMappedTypeModifiers(source)&MappedTypeModifiersIncludeOptional == 0 &&
!(r.c.enforceReadonly && r.relation != r.c.comparableRelation && getMappedTypeModifiers(source)&MappedTypeModifiersIncludeReadonly != 0) {
templateType := r.c.getTemplateTypeFromMappedType(source)
indexedAccessType := r.c.getIndexedAccessType(target, r.c.getTypeParameterFromMappedType(source))
result = r.isRelatedTo(templateType, indexedAccessType, RecursionFlagsBoth, reportErrors)
Expand Down Expand Up @@ -3627,7 +3628,8 @@ func (r *Relater) structuredTypeRelatedToWorker(source *Type, target *Type, repo
keysRemapped := target.AsMappedType().declaration.NameType != nil
templateType := r.c.getTemplateTypeFromMappedType(target)
modifiers := getMappedTypeModifiers(target)
if modifiers&MappedTypeModifiersExcludeOptional == 0 {
if modifiers&MappedTypeModifiersExcludeOptional == 0 &&
!(r.c.enforceReadonly && r.relation != r.c.comparableRelation && modifiers&MappedTypeModifiersExcludeReadonly != 0) {
// If the mapped type has shape `{ [P in Q]: T[P] }`,
// source `S` is related to target if `T` = `S`, i.e. `S` is related to `{ [P in Q]: S[P] }`.
if !keysRemapped && templateType.flags&TypeFlagsIndexedAccess != 0 && templateType.AsIndexedAccessType().objectType == source && templateType.AsIndexedAccessType().indexType == r.c.getTypeParameterFromMappedType(target) {
Expand Down Expand Up @@ -4004,10 +4006,12 @@ func (r *Relater) typeArgumentsRelatedTo(sources []*Type, targets []*Type, varia
func (r *Relater) mappedTypeRelatedTo(source *Type, target *Type, reportErrors bool) Ternary {
modifiersRelated := r.relation == r.c.comparableRelation ||
r.relation == r.c.identityRelation && getMappedTypeModifiers(source) == getMappedTypeModifiers(target) ||
r.relation != r.c.identityRelation && r.c.getCombinedMappedTypeOptionality(source) <= r.c.getCombinedMappedTypeOptionality(target)
r.relation != r.c.identityRelation &&
r.c.getMappedTypeModifiersRank(source, MappedTypeModifiersIncludeOptional|MappedTypeModifiersExcludeOptional) <= r.c.getMappedTypeModifiersRank(target, MappedTypeModifiersIncludeOptional|MappedTypeModifiersExcludeOptional) &&
(!r.c.enforceReadonly || r.c.getMappedTypeModifiersRank(source, MappedTypeModifiersIncludeReadonly|MappedTypeModifiersExcludeReadonly) <= r.c.getMappedTypeModifiersRank(target, MappedTypeModifiersIncludeReadonly|MappedTypeModifiersExcludeReadonly))
if modifiersRelated {
targetConstraint := r.c.getConstraintTypeFromMappedType(target)
sourceConstraint := r.c.instantiateType(r.c.getConstraintTypeFromMappedType(source), core.IfElse(r.c.getCombinedMappedTypeOptionality(source) < 0, r.c.reportUnmeasurableMapper, r.c.reportUnreliableMapper))
sourceConstraint := r.c.instantiateType(r.c.getConstraintTypeFromMappedType(source), core.IfElse(r.c.getEffectiveMappedTypeModifiers(source, MappedTypeModifiersIncludeOptional|MappedTypeModifiersExcludeOptional)&MappedTypeModifiersExcludeOptional != 0, r.c.reportUnmeasurableMapper, r.c.reportUnreliableMapper))
if result := r.isRelatedTo(targetConstraint, sourceConstraint, RecursionFlagsBoth, reportErrors); result != TernaryFalse {
mapper := newSimpleTypeMapper(r.c.getTypeParameterFromMappedType(source), r.c.getTypeParameterFromMappedType(target))
if r.c.instantiateType(r.c.getNameTypeFromMappedType(source), mapper) == r.c.instantiateType(r.c.getNameTypeFromMappedType(target), mapper) {
Expand Down Expand Up @@ -4335,7 +4339,11 @@ func (r *Relater) propertyRelatedTo(source *Type, target *Type, sourceProp *ast.
// from deciding which type "wins" in union subtype reduction.
// They're still assignable to one another, since `readonly` doesn't affect assignability.
// This is only applied during the strictSubtypeRelation -- currently used in subtype reduction
if r.relation == r.c.strictSubtypeRelation && r.c.isReadonlySymbol(sourceProp) && !r.c.isReadonlySymbol(targetProp) {
if (r.relation == r.c.strictSubtypeRelation || r.c.enforceReadonly && r.relation != r.c.comparableRelation) &&
r.c.isReadonlySymbol(sourceProp) && !r.c.isReadonlySymbol(targetProp) && targetProp.Flags&ast.SymbolFlagsMethod == 0 {
if reportErrors {
r.reportError(diagnostics.Property_0_is_readonly_in_the_source_but_not_in_the_target, r.c.symbolToString(targetProp))
}
return TernaryFalse
}
// If the target comes from a partial union prop, allow `undefined` in the target type
Expand Down Expand Up @@ -4709,12 +4717,21 @@ func (r *Relater) membersRelatedToIndexInfo(source *Type, targetInfo *IndexInfo,

func (r *Relater) indexInfoRelatedTo(sourceInfo *IndexInfo, targetInfo *IndexInfo, reportErrors bool, intersectionState IntersectionState) Ternary {
related := r.isRelatedToEx(sourceInfo.valueType, targetInfo.valueType, RecursionFlagsBoth, reportErrors, nil /*headMessage*/, intersectionState)
if related == TernaryFalse && reportErrors {
if sourceInfo.keyType == targetInfo.keyType {
r.reportError(diagnostics.X_0_index_signatures_are_incompatible, r.c.TypeToString(sourceInfo.keyType))
} else {
r.reportError(diagnostics.X_0_and_1_index_signatures_are_incompatible, r.c.TypeToString(sourceInfo.keyType), r.c.TypeToString(targetInfo.keyType))
if related == TernaryFalse {
if reportErrors {
if sourceInfo.keyType == targetInfo.keyType {
r.reportError(diagnostics.X_0_index_signatures_are_incompatible, r.c.TypeToString(sourceInfo.keyType))
} else {
r.reportError(diagnostics.X_0_and_1_index_signatures_are_incompatible, r.c.TypeToString(sourceInfo.keyType), r.c.TypeToString(targetInfo.keyType))
}
}
return TernaryFalse
}
if r.c.enforceReadonly && r.relation != r.c.comparableRelation && sourceInfo.isReadonly && !targetInfo.isReadonly {
if reportErrors {
r.reportError(diagnostics.X_0_index_signature_is_readonly_in_the_source_but_not_in_the_target, r.c.TypeToString(sourceInfo.keyType))
}
return TernaryFalse
}
return related
}
Expand Down
1 change: 1 addition & 0 deletions tsc/internal/core/compileroptions.go
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,7 @@ type CompilerOptions struct {
DisableSolutionSearching Tristate `json:"disableSolutionSearching,omitzero"`
DisableReferencedProjectLoad Tristate `json:"disableReferencedProjectLoad,omitzero"`
ErasableSyntaxOnly Tristate `json:"erasableSyntaxOnly,omitzero"`
EnforceReadonly Tristate `json:"enforceReadonly,omitzero"`
ExactOptionalPropertyTypes Tristate `json:"exactOptionalPropertyTypes,omitzero"`
ExperimentalDecorators Tristate `json:"experimentalDecorators,omitzero"`
ForceConsistentCasingInFileNames Tristate `json:"forceConsistentCasingInFileNames,omitzero"`
Expand Down
12 changes: 12 additions & 0 deletions tsc/internal/diagnostics/diagnosticMessages.json
Original file line number Diff line number Diff line change
Expand Up @@ -4463,6 +4463,14 @@
"category": "Error",
"code": 4126
},
"Property '{0}' is 'readonly' in the source but not in the target.": {
"category": "Error",
"code": 4129
},
"'{0}' index signature is 'readonly' in the source but not in the target.": {
"category": "Error",
"code": 4130
},
"This member cannot have an 'override' modifier because its name is dynamic.": {
"category": "Error",
"code": 4127
Expand Down Expand Up @@ -6565,6 +6573,10 @@
"category": "Message",
"code": 6720
},
"Ensure that 'readonly' properties remain read-only in type relationships.": {
"category": "Message",
"code": 6722
},
"Do not allow runtime constructs that are not part of ECMAScript.": {
"category": "Message",
"code": 6721
Expand Down
12 changes: 12 additions & 0 deletions tsc/internal/diagnostics/diagnostics_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions tsc/internal/execute/tsc/init.go
Original file line number Diff line number Diff line change
Expand Up @@ -176,6 +176,7 @@ func generateTSConfig(options *collections.OrderedMap[string, any], locale local

emitHeader(diagnostics.Stricter_Typechecking_Options)
emitOption("noUncheckedIndexedAccess" /*defaultValue*/, true, commentedNever)
emitOption("enforceReadonly" /*defaultValue*/, true, commentedNever)
emitOption("exactOptionalPropertyTypes" /*defaultValue*/, true, commentedNever)

newline()
Expand Down
Loading