Skip to content
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

Open
wants to merge 26 commits into
base: main
Choose a base branch
from

Conversation

ahejlsberg
Copy link
Member

@ahejlsberg ahejlsberg commented Apr 23, 2024

This PR introduces a new --enforceReadonly compiler option to enforce read-only semantics in type relations. Currently, the readonly modifier prohibits assignments to properties so marked, but it still considers a readonly property to be a match for a mutable property in subtyping and assignability type relationships. With this PR, readonly properties are required to remain readonly across type relationships:

type Box<T> = { value: T };
type ImmutableBox<T> = { readonly value: T };

const immutable: ImmutableBox<string> = { value: "hello" };
const mutable: Box<string> = immutable;  // Error with --enforceReadonly
mutable.value = "ouch!";

When compiled with --enforceReadonly the assignment of immutable to mutable becomes an error because the value property is readonly in the source type but not in the target type.

The --enforceReadonly option also validates that derived classes and interfaces don't violate inherited readonly 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 effectively readonly, yet assignments are allowed when treating an instance as the base type).

interface Base {
    x: number;
}

interface Derived extends Base {  // Error with --enforceReadonly
    get x(): number;
}

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:

type Mutable<T> = { -readonly [P in keyof T]: T[P] };

function foo<T>(mt: Mutable<T>, tt: T, rt: Readonly<T>) {
    mt = tt;  // Error with --enforceReadonly
    mt = rt;  // Error with --enforceReadonly
    tt = mt;
    tt = rt;  // Error with --enforceReadonly
    rt = mt;
    rt = tt;
}

The --enforceReadonly option slightly modifies the effect of as const assertions and const type parameters to mean "as const as possible" without violating constraints. For example, the following compiles successfully with --enforceReadonly:

const obj: { a: string, b: number } = { a: "hello", b: 42 } as const;

whereas the following does not:

const c = { a: "hello", b: 42 } as const;  // { readonly a: "hello", readonly b: 42 }
const obj: { a: string, b: number } = c;  // Error

Some examples using const type parameters:

declare function f10<T>(obj: T): T;
declare function f11<const T>(obj: T): T;
declare function f12<const T extends { a: string, b: number }>(obj: T): T;
declare function f13<const T extends { a: string, readonly b: number }>(obj: T): T;
declare function f14<const T extends Record<string, unknown>>(obj: T): T;
declare function f15<const T extends Readonly<Record<string, unknown>>>(obj: T): T;

f10({ a: "hello", b: 42 });  // { a: string; b: number; }
f11({ a: "hello", b: 42 });  // { readonly a: "hello"; readonly b: 42; }
f12({ a: "hello", b: 42 });  // { a: "hello"; b: 42; }
f13({ a: "hello", b: 42 });  // { a: "hello"; readonly b: 42; }
f14({ a: "hello", b: 42 });  // { a: "hello"; b: 42; }
f15({ a: "hello", b: 42 });  // { readonly a: "hello"; readonly b: 42; }

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 enforce readonly semantics across type relationships. For this reason, the --enforceReadonly compiler option defaults to false. However, by introducing the option now, it becomes possible to gradually update code bases to correct readonly semantics in anticipation of the option possibly becoming part of the --strict family in the future.

Fixes #13347.

@typescript-bot typescript-bot added Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug labels Apr 23, 2024
@typescript-bot
Copy link
Collaborator

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.

@@ -6223,6 +6231,10 @@
"category": "Message",
"code": 6718
},
"Ensure that 'readonly' properties remain read-only in type relationships.": {
Copy link
Contributor

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:

Suggested change
"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
@ahejlsberg
Copy link
Member Author

@typescript-bot test it

@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 23, 2024

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
test top400 ✅ Started ✅ Results
user test this ✅ Started ✅ Results
run dt ✅ Started ✅ Results
perf test this faster ✅ Started 👀 Results

@typescript-bot
Copy link
Collaborator

Hey @ahejlsberg, the results of running the DT tests are ready.

Everything looks the same!

You can check the log here.

@typescript-bot
Copy link
Collaborator

@ahejlsberg Here are the results of running the user tests comparing main and refs/pull/58296/merge:

Everything looks good!

@typescript-bot
Copy link
Collaborator

@ahejlsberg
The results of the perf run you requested are in!

Here they are:

tsc

Comparison Report - baseline..pr
Metric baseline pr Delta Best Worst p-value
Compiler-Unions - node (v18.15.0, x64)
Memory used 192,785k (± 0.76%) 193,449k (± 0.94%) ~ 192,136k 195,896k p=0.173 n=6
Parse Time 1.35s (± 1.18%) 1.34s (± 1.67%) ~ 1.32s 1.37s p=0.796 n=6
Bind Time 0.72s (± 0.00%) 0.72s (± 0.00%) ~ 0.72s 0.72s p=1.000 n=6
Check Time 9.60s (± 0.24%) 9.58s (± 0.28%) ~ 9.55s 9.62s p=0.517 n=6
Emit Time 2.63s (± 0.64%) 2.63s (± 0.91%) ~ 2.59s 2.66s p=1.000 n=6
Total Time 14.30s (± 0.24%) 14.28s (± 0.38%) ~ 14.21s 14.33s p=0.421 n=6
angular-1 - node (v18.15.0, x64)
Memory used 1,221,857k (± 0.01%) 1,221,802k (± 0.01%) ~ 1,221,720k 1,221,930k p=0.173 n=6
Parse Time 8.30s (± 0.27%) 8.26s (± 0.25%) -0.04s (- 0.42%) 8.23s 8.29s p=0.035 n=6
Bind Time 2.23s (± 0.18%) 2.23s (± 0.38%) ~ 2.21s 2.23s p=0.115 n=6
Check Time 36.43s (± 0.20%) 36.42s (± 0.32%) ~ 36.26s 36.54s p=1.000 n=6
Emit Time 17.40s (± 1.04%) 17.37s (± 0.32%) ~ 17.28s 17.44s p=0.936 n=6
Total Time 64.35s (± 0.22%) 64.28s (± 0.17%) ~ 64.15s 64.43s p=0.470 n=6
mui-docs - node (v18.15.0, x64)
Memory used 1,753,194k (± 0.00%) 1,753,205k (± 0.00%) ~ 1,753,154k 1,753,270k p=0.873 n=6
Parse Time 10.05s (± 0.73%) 10.02s (± 0.50%) ~ 9.94s 10.07s p=0.628 n=6
Bind Time 3.35s (± 0.78%) 3.37s (± 0.69%) ~ 3.34s 3.41s p=0.196 n=6
Check Time 81.91s (± 0.27%) 82.18s (± 0.21%) +0.27s (+ 0.34%) 81.86s 82.34s p=0.045 n=6
Emit Time 0.19s (± 2.67%) 0.20s (± 2.62%) ~ 0.19s 0.20s p=0.311 n=6
Total Time 95.51s (± 0.22%) 95.77s (± 0.20%) ~ 95.43s 95.94s p=0.054 n=6
self-build-src - node (v18.15.0, x64)
Memory used 2,322,319k (± 0.04%) 2,322,364k (± 0.05%) ~ 2,321,022k 2,323,679k p=1.000 n=6
Parse Time 7.63s (± 0.85%) 7.54s (± 0.56%) -0.09s (- 1.18%) 7.50s 7.59s p=0.045 n=6
Bind Time 2.73s (± 0.78%) 2.72s (± 0.79%) ~ 2.69s 2.75s p=0.468 n=6
Check Time 49.35s (± 0.40%) 49.48s (± 0.70%) ~ 49.16s 50.13s p=0.471 n=6
Emit Time 4.02s (± 2.74%) 4.04s (± 1.59%) ~ 3.95s 4.14s p=0.471 n=6
Total Time 63.73s (± 0.41%) 63.81s (± 0.55%) ~ 63.50s 64.49s p=1.000 n=6
self-build-src-public-api - node (v18.15.0, x64)
Memory used 2,396,698k (± 0.02%) 2,396,771k (± 0.01%) ~ 2,396,518k 2,397,093k p=1.000 n=6
Parse Time 7.75s (± 0.80%) 7.78s (± 0.31%) ~ 7.74s 7.80s p=0.297 n=6
Bind Time 2.44s (± 0.62%) 2.46s (± 0.44%) ~ 2.44s 2.47s p=0.075 n=6
Check Time 50.15s (± 0.41%) 50.26s (± 0.54%) ~ 49.85s 50.66s p=0.378 n=6
Emit Time 3.96s (± 2.75%) 3.96s (± 2.51%) ~ 3.82s 4.12s p=0.689 n=6
Total Time 64.31s (± 0.44%) 64.48s (± 0.45%) ~ 63.93s 64.79s p=0.230 n=6
self-compiler - node (v18.15.0, x64)
Memory used 423,917k (± 0.01%) 423,950k (± 0.01%) ~ 423,901k 424,007k p=0.173 n=6
Parse Time 2.88s (± 1.11%) 2.89s (± 0.69%) ~ 2.86s 2.91s p=0.871 n=6
Bind Time 1.10s (± 0.89%) 1.09s (± 0.47%) -0.01s (- 1.06%) 1.08s 1.09s p=0.039 n=6
Check Time 15.38s (± 0.25%) 15.40s (± 0.46%) ~ 15.29s 15.47s p=0.470 n=6
Emit Time 1.16s (± 1.27%) 1.18s (± 1.99%) ~ 1.14s 1.20s p=0.223 n=6
Total Time 20.53s (± 0.28%) 20.55s (± 0.43%) ~ 20.42s 20.66s p=0.748 n=6
ts-pre-modules - node (v18.15.0, x64)
Memory used 369,266k (± 0.02%) 369,479k (± 0.04%) +213k (+ 0.06%) 369,312k 369,696k p=0.020 n=6
Parse Time 2.45s (± 1.11%) 2.45s (± 0.95%) ~ 2.42s 2.48s p=0.747 n=6
Bind Time 1.32s (± 1.91%) 1.32s (± 1.39%) ~ 1.30s 1.34s p=0.935 n=6
Check Time 13.31s (± 0.14%) 13.35s (± 0.26%) +0.04s (+ 0.31%) 13.29s 13.38s p=0.043 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 17.08s (± 0.16%) 17.12s (± 0.19%) ~ 17.09s 17.17s p=0.090 n=6
vscode - node (v18.15.0, x64)
Memory used 2,923,686k (± 0.00%) 2,923,626k (± 0.00%) ~ 2,923,500k 2,923,730k p=0.378 n=6
Parse Time 11.39s (± 1.30%) 11.35s (± 0.25%) ~ 11.31s 11.39s p=0.746 n=6
Bind Time 3.54s (± 2.60%) 3.51s (± 2.50%) ~ 3.42s 3.59s p=0.516 n=6
Check Time 62.84s (± 0.44%) 62.60s (± 0.60%) ~ 62.22s 63.32s p=0.173 n=6
Emit Time 17.10s (± 7.97%) 17.66s (± 9.96%) ~ 16.49s 19.96s p=0.872 n=6
Total Time 94.87s (± 1.24%) 95.11s (± 1.60%) ~ 93.91s 97.19s p=0.810 n=6
webpack - node (v18.15.0, x64)
Memory used 409,862k (± 0.02%) 409,822k (± 0.01%) ~ 409,725k 409,901k p=0.630 n=6
Parse Time 3.95s (± 0.71%) 3.94s (± 1.10%) ~ 3.88s 3.99s p=0.418 n=6
Bind Time 1.65s (± 0.89%) 1.65s (± 1.14%) ~ 1.61s 1.66s p=1.000 n=6
Check Time 17.02s (± 0.40%) 17.02s (± 0.45%) ~ 16.94s 17.13s p=0.936 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 22.62s (± 0.39%) 22.60s (± 0.38%) ~ 22.51s 22.73s p=0.628 n=6
xstate-main - node (v18.15.0, x64)
Memory used 459,429k (± 0.02%) 459,351k (± 0.01%) ~ 459,290k 459,404k p=0.173 n=6
Parse Time 2.69s (± 0.77%) 2.69s (± 0.80%) ~ 2.67s 2.72s p=0.685 n=6
Bind Time 0.99s (± 0.64%) 0.99s (± 0.00%) ~ 0.99s 0.99s p=1.000 n=6
Check Time 15.41s (± 0.29%) 15.40s (± 0.37%) ~ 15.32s 15.48s p=0.748 n=6
Emit Time 0.00s (± 0.00%) 0.00s (± 0.00%) ~ 0.00s 0.00s p=1.000 n=6
Total Time 19.09s (± 0.26%) 19.08s (± 0.35%) ~ 18.99s 19.19s p=0.520 n=6
System info unknown
Hosts
  • node (v18.15.0, x64)
Scenarios
  • Compiler-Unions - node (v18.15.0, x64)
  • angular-1 - node (v18.15.0, x64)
  • mui-docs - node (v18.15.0, x64)
  • self-build-src - node (v18.15.0, x64)
  • self-build-src-public-api - node (v18.15.0, x64)
  • self-compiler - node (v18.15.0, x64)
  • ts-pre-modules - node (v18.15.0, x64)
  • vscode - node (v18.15.0, x64)
  • webpack - node (v18.15.0, x64)
  • xstate-main - node (v18.15.0, x64)
Benchmark Name Iterations
Current pr 6
Baseline baseline 6

Developer Information:

Download Benchmarks

@weswigham
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 23, 2024

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
pack this ✅ Started ✅ Results

@typescript-bot
Copy link
Collaborator

typescript-bot commented Apr 23, 2024

Hey @weswigham, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/161430/artifacts?artifactName=tgz&fileId=11F679CBDC619C3D19F886C85532AF465B340967E85B64682C46FBF2A9BFE36A02&fileName=/typescript-5.5.0-insiders.20240423.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.5.0-pr-58296-8".;

@typescript-bot
Copy link
Collaborator

@ahejlsberg Here are the results of running the top 400 repos comparing main and refs/pull/58296/merge:

Everything looks good!

@jakebailey
Copy link
Member

Can you update tests/cases/compiler/APILibCheck.ts and enable this flag? This would let us ensure our public API will work with this flag; we already do this for exactOptionalPropertyTypes.

Copy link
Contributor

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)

@robpalme
Copy link

nit: In the PR description the second code example does not match the preceding wording - it doesn't use readonly

@fatcerberus
Copy link

Any reason why this isn't called strictReadonly?

Comment on lines 395 to 396
readonly trace?: (s: string) => void;
readonly realpath?: (path: string) => string;
trace?(s: string): void;
realpath?(path: string): string;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Were these changes required to make the flag pass on our codebase, or needed for the PR at all?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actually, doesn't look like this change is necessary anymore. I'll back it out.

@erights
Copy link

erights commented Jul 17, 2024

While I welcome this direction, please do not use the term "enforce" for things that are not actually enforced. TypeScript is unsound and erased, so it cannot and does not actually enforce anything. It is useful for catching some many accidents, which is great. But some accidents will slip through the unsoundness, and much malice will. The term "enforce" implies otherwise and will give some people a false sense of security, which is dangerous.

@RyanCavanaugh
Copy link
Member

RyanCavanaugh commented Jul 17, 2024

@erights did you have a synonym in mind?

But some accidents will slip through the unsoundness, and much malice will.

No one should be using a language with any as a defense in an adversarial situation. What scenarios are you thinking of here?

@erights
Copy link

erights commented Jul 17, 2024

Object.freeze, which is the fundamental enforcement mechanism for read-only-like stability, was introduced in ES5 specifically to be genuine enforcement which could be used in adversarial situations. Since then, Object.freeze has been used as an adversarial enforcement mechanism many times.

We are of course using Hardened JavaScript and harden in many adversarial situations. We also use TypeScript in various ways in that context. Better TypeScript readonly support is welcome for many things, including better typing of harden. However, we already find we need to continually remind users of TypeScript-on-Hardened-JS that the TypeScript typing portions of their code are unsound and erased, and therefore unenforced. When they want to enforce something, they need to rely on unerased runtime enforcement mechanisms. That's fine.

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.

@turadg
Copy link

turadg commented Jul 17, 2024

a synonym in mind?

checkReadonly implies a check for type system violations without implying that they're enforced.

Another option is strictReadonly since some aspects of readonly are already checked and this is a more strict form.

@ahejlsberg
Copy link
Member Author

Regarding the name of the flag, we're perfectly happy to entertain suggestions for better options.

The issue with --checkReadonly is it would imply that no readonly checks occur when it is disabled, which isn't the case. We always disallow assignments to readonly properties.

The issue with --strictReadonly is that it implies being in the --strict family of flags. That's not the case because it would be too significant of a breaking change.

@codecov-commenter

This comment was marked as off-topic.

@jakebailey

This comment was marked as off-topic.

@erights
Copy link

erights commented Jul 18, 2024

Regarding the name of the flag, we're perfectly happy to entertain suggestions for better options.

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?

@mhofman
Copy link

mhofman commented Jul 18, 2024

@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 ?

@jakebailey
Copy link
Member

@typescript-bot pack this

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 18, 2024

Starting jobs; this comment will be updated as builds start and complete.

Command Status Results
pack this ✅ Started ✅ Results

@typescript-bot
Copy link
Collaborator

typescript-bot commented Jul 18, 2024

Hey @jakebailey, I've packed this into an installable tgz. You can install it for testing by referencing it in your package.json like so:

{
    "devDependencies": {
        "typescript": "https://typescript.visualstudio.com/cf7ac146-d525-443c-b23c-0d58337efebc/_apis/build/builds/162843/artifacts?artifactName=tgz&fileId=3BEC331D303F3777E3107FAA20DD4311D8F88CCB397BBCDB49C9226EA413D2EB02&fileName=/typescript-5.6.0-insiders.20240718.tgz"
    }
}

and then running npm install.


There is also a playground for this build and an npm module you can use via "typescript": "npm:@typescript-deploys/pr-build@5.6.0-pr-58296-32".;

@ahejlsberg
Copy link
Member Author

Before trying to generate more suggestions, I'd like to better understand the feature.

The rationale for the existing functionality of readonly is outlined here. The modifier was introduced several years after the initial release of TypeScript, and even then it was too large of a breaking change to check it strictly. Therefore, we decided to just error on direct assignments to readonly properties, but otherwise ignore the modifier for purposes of relations between types containing readonly properties.

The --enforceReadonly option (or whatever we call it) implements that final step of checking that readonly isn't defeated by treating a read-only property as a mutable property through a conversion of the containing type. We know this will be a breaking change for a lot of code, but there is a subset of the community for whom updating their code is a worthwhile cost. Over time we may see enough code updated to the stricter semantics that we could consider including the option in the --strict family of flags.

What surprises me is that it is a compiler flag at all, rather than a stronger form of readonly within the language...

The surprising thing really is that we aren't checking readonly in type relations and --enforceReadonly at least puts us on a path to doing so. I think that introducing another stronger readonly-like modifier would just add to the confusion.

What happens if I have some modules or packages that are correctly written to the weaker constraints...

What happens is that you'll see new errors pointing out places where your code potentially indirectly violates readonly by treating read-only properties as mutable properties through type conversions. These errors may be corrected by adding or removing readonly modifiers as appropriate, or by adding type assertions that explicitly call out the unsound behavior. In all cases, code that is updated to compile with --enforceReadonly will continue to compile without the switch.

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 --strict family of flags) that are global in nature in the same manner.

@erights
Copy link

erights commented Jul 18, 2024

We have multiple other options (e.g. the entire --strict family of flags) that are global in nature in the same manner.

Can we just do that?

@JoostK
Copy link
Contributor

JoostK commented Jul 18, 2024

I do understand the concern around breakages this would cause if it falls under the strict family of flags, but I do feel it's rather unfortunate; enabling strict, to me, opts you into TypeScript in its strictest form, whatever that may be in any given version. Breakages can be somewhat easily addressed by disabling a newly introduced flag, or by actually fixing the newly-reported errors. Assuming folks opt into strict because they desire the least room for "unsound" code, having to adopt a new flag—that may even become deprecated if the flag is eventually promoted to the strict family—during an update to benefit from better strictness seems undesirable to me.

I do have a concrete proposal: let strict be an option that takes a version string to be able to tell which strict flags have been opted into, preventing future breakages for additional new strict flags. I believe this is similar to the recent work on a deprecation flag. That does raise the question as to whether true should mean everything or "5.5", where I think the former sounds like the more desirable behavior but that would leave the concern that this change would still break apps (and I understand how this would break the "top ###" tests and potentially the performance tests that TS itself relies on for testing).

@JoostK
Copy link
Contributor

JoostK commented Jul 18, 2024

Alternatively, perhaps this should just be an "edition" flag (or whatever name makes sense, I'm using the Rust terminology here) as a high level switch that determines how the options are interpreted.

@RobertSandiford
Copy link

RobertSandiford commented Jul 21, 2024

I do understand the concern around breakages this would cause if it falls under the strict family of flags, but I do feel it's rather unfortunate; enabling strict, to me, opts you into TypeScript in its strictest form, whatever that may be in any given version.

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>;
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm concerned with this change as prototype is actually read-only on Map, Set, WeakMap, WeakSet, etc.:

image

@JustinBordage
Copy link

JustinBordage commented Aug 9, 2024

The issue with --strictReadonly is that it implies being in the --strict family of flags. That's not the case because it would be too significant of a breaking change.

@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 --strict family of flags be a bad thing? The way I interpret --strictReadonly is that I'd be more constrained with the flag enabled than without it. Which I believe fits the linguistic meaning of "strict", would it not?

As for flag name suggestions, if --strictReadonly is not appropriate, then perhaps something along the lines of --noReadonlyCoercion?

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 was just a prefix and nothing more. However, if I've understood correctly, --strict is actually it's own flag which includes the effects of all the other flags that are prefixed with --strict?

@RobertSandiford
Copy link

RobertSandiford commented Aug 9, 2024

Edit: Oh I think I may have figured it out, my initial interpretation was that --strict was just a prefix and nothing more. However, if I've understood correctly, --strict is actually it's own flag which includes the effects of all the other flags that are prefixed with --strict?

"strict" enables a variety of flags, some prefixed with strict. It doesn't enable all safety/correctness features ("noUncheckedIndexedAccess", "useDefineForClassFields")

@petamoriken
Copy link
Contributor

The prototype properties need to be treated specially.

Perhaps we might need // @ts-ignore for interface DOMMatrix extends DOMMatrixReadOnly in the DOM types.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Author: Team For Uncommitted Bug PR for untriaged, rejected, closed or missing bug
Projects
Status: Waiting on reviewers
Development

Successfully merging this pull request may close these issues.

Interface with readonly property is assignable to interface with mutable property