-
Notifications
You must be signed in to change notification settings - Fork 12.5k
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
New --enforceReadonly
compiler option to enforce read-only semantics in type relations
#58296
base: main
Are you sure you want to change the base?
Conversation
Looks like you're introducing a change to the public API surface area. If this includes breaking changes, please document them on our wiki's API Breaking Changes page. Also, please make sure @DanielRosenwasser and @RyanCavanaugh are aware of the changes, just as a heads up. |
src/compiler/diagnosticMessages.json
Outdated
@@ -6223,6 +6231,10 @@ | |||
"category": "Message", | |||
"code": 6718 | |||
}, | |||
"Ensure that 'readonly' properties remain read-only in type relationships.": { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just a drive-by comment that as an everyday user of TS, I don't really know what a "type relationship" is. Maybe this could be worded more explicitly as:
"Ensure that 'readonly' properties remain read-only in type relationships.": { | |
"Enforce that mutable properties cannot satisfy 'readonly' requirements in assignment or subtype relationships.": { |
# Conflicts: # src/compiler/diagnosticMessages.json # tests/baselines/reference/es2018IntlAPIs.types # tests/baselines/reference/es2020IntlAPIs.types # tests/baselines/reference/inferenceOptionalPropertiesToIndexSignatures.types # tests/baselines/reference/instantiationExpressions.types # tests/baselines/reference/localesObjectArgument.types # tests/baselines/reference/mappedTypeConstraints2.types # tests/baselines/reference/mappedTypeRecursiveInference.types # tests/baselines/reference/unionTypeInference.types # tests/baselines/reference/useObjectValuesAndEntries1.types # tests/baselines/reference/useObjectValuesAndEntries4.types
@typescript-bot test it |
Hey @ahejlsberg, the results of running the DT tests are ready. Everything looks the same! |
@ahejlsberg Here are the results of running the user tests comparing Everything looks good! |
@ahejlsberg Here they are:
tscComparison Report - baseline..pr
System info unknown
Hosts
Scenarios
Developer Information: |
@typescript-bot pack this |
Hey @weswigham, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
@ahejlsberg Here are the results of running the top 400 repos comparing Everything looks good! |
Can you update |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Mapped type relationships should also check this:
function test<T>(a: T) {
let obj1: { [K in keyof T]: T[K] } = null as any;
let obj2: { readonly [K in keyof T]: T[K] } = null as any;
obj1 = obj2; // should error
obj2 = obj1;
}
Currently both assignments are OK: TS playground (using this PR)
nit: In the PR description the second code example does not match the preceding wording - it doesn't use |
Any reason why this isn't called |
We are of course using Hardened JavaScript and But seeing the term "enforce" in the documentation of the unenforced TypeScript properties will only deepen the confusion and make the explanations harder. It is harmful and confusing. |
Another option is |
Regarding the name of the flag, we're perfectly happy to entertain suggestions for better options. The issue with The issue with |
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
This comment was marked as off-topic.
Glad to hear it! Before trying to generate more suggestions, I'd like to better understand the feature. What surprises me is that it is a compiler flag at all, rather than a stronger form of readonly within the language alongside the existing one. As a flag, it changes meaning of existing code. Let's call the existing semantics the "weaker" constraints, and with the flag, "stronger" constraints. (I am not suggesting these names. Just need terminology to ask questions.) What happens if I have some modules or packages that are correctly written to the weaker constraints, and other modules or packages written correctly to the stronger constraints, and now I want to correctly compose them together into a larger system. Can I statically type check this composite system? Clearly, if I check the composite only with the stronger constraints, the modules correct only by the weaker constraints will fail. If I check the composite only with the weaker constraints, if all the inputs were correct on their own terms, the composite would still pass. But the code written to the stronger constraints could come to violate them without causing a static error. Is all this correct? |
@typescript-bot pack this Edit: I guess that doesn't work for non members. Any chance someone could regenerate a build I could use in the playground ? |
@typescript-bot pack this |
Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your
and then running There is also a playground for this build and an npm module you can use via |
The rationale for the existing functionality of The
The surprising thing really is that we aren't checking
What happens is that you'll see new errors pointing out places where your code potentially indirectly violates We aren't providing a way to control the checking on an individual module or type basis, mostly because it becomes very hard to reason about. For example, what does it mean to derive a "weak" type from a "strong" type, and, in type relations, what happens when you mix "weak" and "strong" types? We have multiple other options (e.g. the entire |
Can we just do that? |
I do understand the concern around breakages this would cause if it falls under the I do have a concrete proposal: let |
Alternatively, perhaps this should just be an |
This not the current effect of "strict". For example both "noUncheckedIndexedAccess" and "useDefineForClassFields" improve safety. That said some sort of versioned "strict" does seem reasonable. Name-wise I don't see a problem with "enforce" because it is enforcing readonly at the type level, which is where TS operates. If users don't understand that TS is a typechecker, not something that enforces runtime constraints, this is an issue with fundamental knowledge of TS that is really a pre-requisite. "Enforce" is not incorrect within the context. The name could be a little confusing however to those who don't understand the issue, as TS appears to already enforce (at the type level) the readonly modifier. The issue technically applies only to type relationships and manifests in values with types that are considered subtypes of the original type with a readonly property. Something like "consistentReadonly" might better communicate that the change helps to rationalise type relationships, and preserve readonly status within subtypes. "preserveReadonly" is also an option, that I like a little less. |
@@ -30,7 +30,7 @@ interface Map<K, V> { | |||
interface MapConstructor { | |||
new (): Map<any, any>; | |||
new <K, V>(entries?: readonly (readonly [K, V])[] | null): Map<K, V>; | |||
readonly prototype: Map<any, any>; | |||
prototype: Map<any, any>; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
@ahejlsberg I'm confused, how does it's name imply it's significance or expected significance as a breaking change? Also, why would me implying that it's part of the As for flag name suggestions, if Side Note: I'm interpreting this from a more surface level understanding of the concepts, since this level of discussion is generally outside my realm of expertise. But I felt the need to contribute since, I just recently encountered a use-case where not having this flag could prove to be dangerous/fragile. Because I don't have a means of gracefully handling this risk other than hoping the dev reads the jsdocs on the function in question. Edit: Oh I think I may have figured it out, my initial interpretation was that |
"strict" enables a variety of flags, some prefixed with strict. It doesn't enable all safety/correctness features ("noUncheckedIndexedAccess", "useDefineForClassFields") |
The Perhaps we might need |
@jakebailey would it be much effort to rebase and pack this branch? I briefly tried to resolve the conflicts but felt I didn't have enough context around the changes. |
This PR introduces a new
--enforceReadonly
compiler option to enforce read-only semantics in type relations. Currently, thereadonly
modifier prohibits assignments to properties so marked, but it still considers areadonly
property to be a match for a mutable property in subtyping and assignability type relationships. With this PR,readonly
properties are required to remainreadonly
across type relationships:When compiled with
--enforceReadonly
the assignment ofimmutable
tomutable
becomes an error because thevalue
property isreadonly
in the source type but not in the target type.The
--enforceReadonly
option also validates that derived classes and interfaces don't violate inheritedreadonly
constraints. For example, an error is reported in the example below because it isn't possible for a derived class to remove mutability from an inherited property (a getter-only declaration is effectivelyreadonly
, yet assignments are allowed when treating an instance as the base type).In type relationships involving generic mapped types,
--enforceReadonly
ensures that properties of the target type are not more mutable than properties of the source type:The
--enforceReadonly
option slightly modifies the effect ofas const
assertions andconst
type parameters to mean "as const as possible" without violating constraints. For example, the following compiles successfully with--enforceReadonly
:whereas the following does not:
Some examples using
const
type parameters:Stricter enforcement of
readonly
has been debated ever since the modifier was introduced eight years ago in #6532. Our rationale for the current design is outlined here. Given the huge body of existing type definitions that were written without deeper consideration of read-only vs. read-write semantics, it would be a significant breaking change to strictly enforcereadonly
semantics across type relationships. For this reason, the--enforceReadonly
compiler option defaults tofalse
. However, by introducing the option now, it becomes possible to gradually update code bases to correctreadonly
semantics in anticipation of the option possibly becoming part of the--strict
family in the future.Fixes #13347.