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 |
Same reason as |
Only having a |
Alright, that makes sense. That being said (and I realize this is just 🚲 🏠 at this point) I'm still not a big fan of |
Thanks for the PR! It looks like you've changed the TSServer protocol in some way. Please ensure that any changes here don't break consumers of the current TSServer API. For some extra review, we'll ping @sheetalkamat, @mjbvz, @zkat, and @joj for you. Feel free to loop in other consumers/maintainers if necessary. |
src/compiler/checker.ts
Outdated
// 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. +?). |
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.
nit:
// 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. +?). | |
// Return -1, 0, or 1, where -1 means modifier is stripped (i.e. -?), 0 means modifier is unchanged, and 1 means | |
// modifier is added (i.e. +?). |
Tried this on a personal project (a few thousand LOC) I wrote in the 4.3 timeframe and only got one (1!!) error. Looking ahead with skipLibCheck off, there's some stuff to address in
|
@@ -58,6 +58,22 @@ enforceReadonly1.ts(97,5): error TS2322: Type 'ImmutableValue<string>' is not as | |||
rt = tt; | |||
} | |||
|
|||
// A read-only property is assignable to a property declared as a method |
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.
it might be good to add rationale behind this in the PR's description
@@ -99,9 +115,6 @@ enforceReadonly1.ts(97,5): error TS2322: Type 'ImmutableValue<string>' is not as | |||
} | |||
|
|||
class D4 extends B4 { // Error |
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.
this // Error
comment is now outdated
Latest commits exclude methods from strict checking. A bit of rationale is in order. Consider scenarios like this: interface Base {
foo(): void;
}
interface Item extends Base {
name: string;
}
interface ReadonlyItem extends Base {
readonly name: string;
}
declare let item1: ReadonlyItem;
declare let item2: Readonly<Item>;
item1 = item2; // Error? It seems unreasonable to error on the assignment, yet the Methods have always been considered mutable, and we don't even allow We could consider having mapped types with In my opinion, the best course of action is to simply exclude methods from strict |
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.