Skip to content

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.

"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/[email protected]".;

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

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

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
Contributor

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.

@MichaelMitchell-at
Copy link
Contributor

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

@HansBrende
Copy link
Contributor

The FAQ section of the wiki states:

Enable --enforceReadonly (available in TypeScript 5.6; see #58296)

Seems like this functionality is not actually available yet, considering this PR is still open? Should the wiki be updated to say "coming soon" or something? Somewhat confusing as-is.

@mkantor
Copy link
Contributor

mkantor commented Mar 13, 2025

@HansBrende you could have proposed that edit yourself. I did so here.

@Ghabriel
Copy link

Is there something currently blocking this PR? This would be an amazing feature to have — I often have to refactor codebases that use a lot of mutability to try and reduce overall mutations, and sadly the current behavior of readonly doesn't give me nearly enough confidence that functions which take readonly arguments do in fact "keep their word" and leave them as-is.

@zimmed
Copy link

zimmed commented Jun 20, 2025

Apparently I have gaslit myself. For years, I thought this was already enforced behavior and recently discovered it wasn't.

It's hard to believe that this is valid in typescript:

interface ImmutablePoint {
  readonly x: number;
  readonly y: number;
}

class FixedPoint  {
  get x() { return 5; }
  get y() { return 10; }
}

function move(mutablePoint: { x: number, y: number }) {
  mutablePoint.x += 1;
  mutablePoint.y += 1;
}

const point: ImmutablePoint = { x: 1, y: 2 };

move(point);
move(new FixedPoint());

Let's get this merged, please.

@AFatNiBBa
Copy link

I too came across this problem, are there any news on this?

@lishaduck
Copy link

lishaduck commented Oct 3, 2025

I too came across this problem, are there any news on this?

TypeScript is in ~feature freeze until Corsa lands, so we've got to wait at least another year 😢

@AFatNiBBa
Copy link

Quite unfortunate ☹️

In the meantime, would this pull request change the behaviour of this code?

function f<K extends PropertyKey, T extends { [k in K]: number }>(obj: T, k: K) {
    // Type 'number' is not assignable to type 'T[K]'.
    //     'number' is assignable to the constraint of type 'T[K]', but 'T[K]' could be instantiated with a different subtype of constraint 'number'.
    obj[k] = 1;
    return obj[k];
}

const k = f({ a: 1 }, "a");
//    ^? const k: number

Maybe I'm wrong, but I think that T[K] should technically be allowed to be a sub-type of number (Instead of exactly number) only if K was readonly, precisely because it can't be set otherwise.
If I'm not mistaken, the way of saying it technically would be that "It shouldn't assume covariance"

@RobertSandiford
Copy link

RobertSandiford commented Oct 6, 2025

In the meantime, would this pull request change the behaviour of this code?

I don't see why it would. There are no readonly markers here to begin with.

Maybe I'm wrong, but I think that T[K] should technically be allowed to be a sub-type of number (Instead of exactly number) only if K was readonly, precisely because it can't be set otherwise. If I'm not mistaken, the way of saying it technically would be that "It shouldn't assume covariance"

TypeScript's assignability rules act as if all objects are readonly, so uses covariance rules. This makes most mutable objects unsafe as reading does not support contravariance and writing does not support covariance, so only objects of exactly the same type should be allowed. E.g the follow code is broken:

const o = { a: 1 }
function set(o: { a: string | number }) {
    o.a = 'foo'
}
set(o)
console.log(o.a.toPrecision(2)) // err: o.a.toPrecision is not a function 

In your example it is analyzing a bit more deeply than normal and is actually behaving correctly. (Removing the generic T and just using { [k in K]: number } as the type for obj removes the error but is unsafe, as with the code above.

To be safe your code needs to specify that obj[K] is exactly number which just isn't naturally supported. There's no syntax for specific exactness or marking an argument as accepting contravariant input instead of covariant. You can fudge it with a validator type that uses an extends check to check that obj[K] is not narrower than a number, but you'll need to use casting inside the function because now you have a conditional type for obj that can't be assigned to directly: Playground

The solution would be something along the lines of this: #9252 (expanded to apply to object properties, and with support for exact match only as well as supertype.

@AFatNiBBa
Copy link

What I meant was that it seems that TypeScript assignability rules are based on readonly checks not being strict.
With this flag, covariance ans contravariance could be enforced in the right places, since now there would be no need to assume every prop is readonly.
This said, I understand that a change of this magnitude could bring more harm than good

@RobertSandiford
Copy link

RobertSandiford commented Oct 6, 2025

What I meant was that it seems that TypeScript assignability rules are based on readonly checks not being strict.
With this flag, covariance ans contravariance could be enforced in the right places, since now there would be no need to assume every prop is readonly.

TS could move to exact match only for mutable objects, and require readonly props for covariant assignability. With a writeonly flag they could allow contravariant props. But this is separate to this change, and could happen without this. With that change and not this ticket you'd be able to assign { readonly a: T|U } to { a: T|U }, and wouldn't be able to assign { readonly a: T } to { a: T|U } (covariant). This is readonly breaking as already exists, but certainly not a blocker to going ahead with the other change.

The above though is unlikely to happen due to the issues it would cause in existing codebases. Calls to libraries that expect objects and only read, that rely on covariant types being allowed (mega common) would break, unless those libs were updated with readonly keywords. A more likely change would be a strict flag that made objects received by functions readonly by default, with a mutable flag to enable writing and stricter assignability, because this would move to issue upstream into the libraries with nothing breaking unless a lib identified that it was writing to an object and needed to add the mutable keyword, and of course downstream projects wouldn't break until they updated those libs. Plus breaks would only occur where writing was actually happening, rather than many false positives.

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