New `--enforceReadonly` compiler option to enforce read-only semantics in type relations
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.
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.
@typescript-bot test it
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 |
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 main and refs/pull/58296/merge:
Everything looks good!
@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 |
- node (v18.15.0, x64)
- 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:
@typescript-bot pack this
Starting jobs; this comment will be updated as builds start and complete.
| Command | Status | Results |
|---|---|---|
pack this |
✅ Started | ✅ Results |
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/[email protected]".;
@ahejlsberg Here are the results of running the top 400 repos comparing main and refs/pull/58296/merge:
Everything looks good!
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.
nit: In the PR description the second code example does not match the preceding wording - it doesn't use readonly
Any reason why this isn't called strictReadonly?
Any reason why this isn't called
strictReadonly?
Same reason as --exactOptionalPropertyTypes. It's too breaky to be in the --strict family of options and not necessarily desired by everyone.
nit: In the PR description the second code example does not match the preceding wording - it doesn't use readonly
Only having a get accessor implies that the property is readonly, but I suppose that is a bit subtle.
It's too breaky to be in the
--strictfamily of options and not necessarily desired by everyone.
Alright, that makes sense. That being said (and I realize this is just :bike: :house: at this point) I'm still not a big fan of enforceReadonly, as the implied opposite of --enforceReadonly is "don't enforce readonly", which... isn't really accurate.
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.
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 @types/node. I'll do a DT PR once the beta is out
node_modules/@types/node/globals.d.ts:85:13 - error TS2403: Subsequent variable declarations must have the same type. Variable 'AbortSignal' must be of type '{ new (): AbortSignal; prototype: AbortSignal; abort(reason?: any): AbortSignal; any(signals: AbortSignal[]): AbortSignal; timeout(milliseconds: number): AbortSignal; }', but here has type '{ new (): AbortSignal; prototype: AbortSignal; }'.
85 declare var AbortSignal: {
~~~~~~~~~~~
../typescript/built/local/lib.dom.d.ts:2360:13
2360 declare var AbortSignal: {
~~~~~~~~~~~
'AbortSignal' was also declared here.
node_modules/@types/node/http2.d.ts:61:22 - error TS2430: Interface 'Http2Stream' incorrectly extends interface 'Duplex'.
Property 'destroyed' is 'readonly' in the source but not in the target.
61 export interface Http2Stream extends stream.Duplex {
~~~~~~~~~~~
node_modules/@types/node/net.d.ts:75:11 - error TS2415: Class 'Socket' incorrectly extends base class 'Duplex'.
Property 'destroyed' is 'readonly' in the source but not in the target.
75 class Socket extends stream.Duplex {
~~~~~~
node_modules/@types/node/stream.d.ts:480:15 - error TS2420: Class 'Writable' incorrectly implements interface 'WritableStream'.
Property 'writable' is 'readonly' in the source but not in the target.
480 class Writable extends Stream implements NodeJS.WritableStream {
~~~~~~~~
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 foo property in Readonly<Item> is a readonly function type property, whereas the foo property in ReadonlyItem is a method that isn't and can't be marked readonly. Previously this wasn't an issue because we never enforced readonly semantics across type relationships.
Methods have always been considered mutable, and we don't even allow readonly modifiers on method declarations. TBH, if we did, I think most users would read it as "this method doesn't mutate the object", which wouldn't at all be the case. Interestingly, we've had very few requests for readonly methods. It doesn't really seem to be a pain point, and mutation is commonly used in interception patterns, function prototype construction, etc.
We could consider having mapped types with readonly modifiers not add readonly to properties that originate in methods, but that just doesn't seem right either. I mean, Readonly<T> is supposed to make every property readonly.
In my opinion, the best course of action is to simply exclude methods from strict readonly checking. Meaning that, in a type relationship, when a target property is declared as a method, we don't care about the readonly state of the source property.
we've had very few requests for readonly methods. It doesn't really seem to be a pain point, and mutation is commonly used in interception patterns, function prototype construction, etc.
There are some interest expressed by #22315
We could add another flag that extends the check of enforceReadonly to methods and make methods readonly by default.
@ahejlsberg
It seems unreasonable to error on the assignment, [..]
Methods have always been considered mutable, and we don't even allow
readonlymodifiers on method declarations. TBH, if we did, I think most users would read it as "this method doesn't mutate the object", which wouldn't at all be the case.
I disagree here. Given enforceReadonly, I'd expect item1 = item2; // Error? to error. Also, I don't see why methods shouldn't be allowed to be declared readonly – I can (and do) already do this, and it works as I expect:
export type Type = {
readonly toString: () => string
}
I'd like to be able to do this, purely based on consistency (I don't really need two ways to describe the same thing, so ¯\_(ツ)_/¯):
export type Type = {
readonly toString () : string
}
Wrapping Readonly<> around types with methods also gives us that behavior. So, from my point of view, disallowing readonly on readonly toString () : string is just an unnecessary hurdle because it merely hinders a feature that is available via different avenues anyway:
class X {
readonly toString : () => string = () => ``
}
class Y {
readonly toString () { // no reason why exactly this should error…
return ``
}
}
class Z {
get toString () {
return () => ``
}
}
const x = new X()
const y = new Y()
const z = new Z()
x.toString = ()=>``
y.toString = ()=>`` // …or why exactly this should not
z.toString = ()=>``
Also, while I consider inline assignment dangerous enough, function parameters are even more dangerous because you usually do not see the code that works on your supposedly readonly data:
const readonlyArray = [] as const
mutateArray(readonlyArray) // The type 'readonly []' is 'readonly' and cannot be assigned to the mutable type 'unknown[]'.ts(2345)
function mutateArray (array : Array<unknown>) {
array.shift()
}
const readonlyObject = { toString () { return `` } } as const
mutateObject(readonlyObject) // This should error
function mutateObject (mutable : { toString() : string }) {
mutable.toString = () => `-`
}
mutateObject(readonlyObject) should error because otherwise, you have no guarantees that the function doesn't do something to your methods. This introduces an opportunity for runtime errors that seems preventable by making the behavior consistent.
And I don't count “interception patterns, function prototype construction, etc.” as strong counter-arguments, because:
-
Proxy - The prototype can be built as a mutable object and then assigned as a readonly prototype
- For runtime-safe tasks, there are usually options that work with the type system
If any specific project absolutely cannot work with this, then I guess that enforceReadonly is generally unfit anyway.
@Conaclos
We could add another flag that extends the check of
enforceReadonlyto methods and make methods readonly by default.
I'm unsure about the second part – I'd like to make everything readonly by default 😅 and I fear that's out of scope here… But a flag to treat methods like everything else would be good. After all, JS doesn't treat them differently, does it?
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.
@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?
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.
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.
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.
Welcome to Codecov :tada:
Once you merge this PR into your default branch, you're all set! Codecov will compare coverage reports and display results in all future pull requests.
Thanks for integrating Codecov - We've got you covered :open_umbrella:
Oh come on, they weren't supposed to comment that (must be the unlucky first PR, sorry)
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?
@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 ?