Repository navigation
Immutable-By-Default Flags #32758
Description
Search Terms
readonly, default, immutable
Suggestion
Adding flags under the strict umbrella to default to immutability in different cases, and a new mutable keyword.
Use Cases
I'm creating this issue as a parent issue to track a couple of issues that already exist for specific cases:
- Class methods: Suggestion: readonly method #22315, Suggestion: readonly method #22315 (comment) in particular
- Arrays: Default to ReadonlyArray #32467
- Interfaces: No issue yet, left a comment in Default to ReadonlyArray #32467 (comment)
- Suggestion for readonly interface #21152
Off of the top of my head, these flags would have some direct advantages:
- They clearly state the intention of the whole project in regards to mutability
- It's very common for new members of teams to forget to add the
readonlykeyword or useT[]rather thanReadonlyArray<T>accidentally. Defaulting to immutability would help prevent these accidents, and amutablekeyword would be easy to find bytslintor even a simplegrep, to automatically request review in PRs or leave an automated commentthis PR introduces mutation.
Examples
Examples should probably live in the children GitHub issues, but I'm copying here my comment for a quick example:
interface T {
n: number // immutable
mutable s: string // mutable
}
const o: T = {
n: 42,
s: 'hello world',
}
o.n = 43 // error
o.s = '👋🌎' // okChecklist
My suggestion meets these guidelines:
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
- This feature would agree with the rest of TypeScript's Design Goals.
Backwards Compatibility
AnyhowStep made an interesting comment on this issue.
Basically this feature wouldn't be any problem for applications, but it could be problematic for libraries, as the emitted .d.ts may be imported from a library/application that isn't using this flag or is using an older TS version.
Possible solutions:
- A pre-processor directive (Immutable-By-Default Flags #32758 (comment))
- Always emitting
d.tswithreadonly, nevermutable(possibly behind another flag?) - downlevel-dts
Records and Tuples
The Record and Tuple proposal has reached stage 2, so it may be arriving to TS soon-ish. It seems closely related to this issue, but I don't think it fully addresses it.
readonly interface
The possibility of using the readonly keyword for an entire interface has been suggested in A cheaper, easier to implement middle ground could be #21152.
readonly interface State {
prop1: string;
prop2: string;
...
prop22: string;
}This would probably be a much cheaper and less disruptive feature to implement, and could be a great starting point.
Activity
AnyhowStep commentedon Aug 8, 2019
Why not a lint rule where
readonlyis always required?And if they want stuff to be mutable, add eslint-disable-next-line.
Not much to say about methods, though.
lautarodragan commentedon Aug 9, 2019
Three reasons:
readonlyin one of the projects I work on daily, and it's a relatively small codebase. And it's just one project, we have a few.k4b7 commentedon Dec 27, 2019
I've started using prefer-readonly-type with a small project (9K SLOC) and ended up having to add a 346
readonlys throughout the code and 18// eslint-disable-next-line functional/prefer-readonly-typecomments. I'd much rather only have to add 18mutables instead. The code would be much cleaner and easier to maintain.One of the reasons for the high
readonlycount is that theprefer-readonly-typedoesn't support wrapping objects withReadonly<>.AnyhowStep commentedon Dec 28, 2019
I use
readonlyas much as possible, myself. I have 1019 usages ofreadonlyin a hobby project.But I still think this is better as a lint rule (at the moment).
It seems like people don't agree with me, so I figured I'd elaborate.
If library A uses this flag, the emit would still have to be compatible with downstream libraries, whether they use this flag or not.
So, now, library A's
.d.tsemit has to look like this,If it was this instead,
Then downstream libraries that use the flag will think
Mutableis immutable.If it was this instead,
Then downstream libraries that do not use the flag will think
Immutableis mutable.Of course, with the introduction of this
mutablekeyword, you now have a breaking change. One that might not be justifiable because of how much impact it has.Projects on TS-without-mutable-keyword suddenly can't use projects compiled by TS-with-mutable-keyword. And not everyone upgrades their project's version of TS as quickly as the releases come.
I can't think of a way to introduce this flag without breaking
.d.tsfiles, somehow.The ideal implementation would,
You could maybe have some kind of TS-specific pre-processor directive in the emit that says, "Treat the following type as mutable". Then, your emit would be,
Older versions of TS correctly see
Mutableas being mutable.No behaviour has changed. It looks like regular emit.
Newer versions of TS with the flag correctly see
Mutableas being mutable.With the flag switched on, it should think that
Mutableis immutable, sincereadonlyis the default property modifier. But the magical comment should force TS to treatMutableas havingmutableproperties by default.Newer versions of TS without the flag correctly see
Mutableas being mutable.Same as older versions of TS.
However, this requires that magical pre-processor directive. I'm not sure if that's a thing the TS team would enjoy having.
lautarodragan commentedon Jan 2, 2020
Thanks AnyhowStep for that thorough analysis! Initially I didn't realize this would be a breaking change. I've added your comment to the issue's description, along with a possible solution.
AnyhowStep commentedon Jan 24, 2020
Oh, wait.
downlevel-dtsis a thing.https://github.com/sandersn/downlevel-dts
Even if new emit is incompatible, downlevel-dts can be used.
SRachamim commentedon Sep 4, 2020
Making
readonlythe default will makeTypeScripta lot more powerful. In our codebases we use prefer-readonly-type to automatically addreadonlyto everything, and we don't have anyeslint-ignore. Still, it's annoying to see and read all thoseReadonly.If
TypeScriptwants to be here to stay it must go through this soon. I'm shocked to see 2020 code that uses mutations. I thought that already behind us.pleunv commentedon Jan 21, 2021
There's nothing wrong with mutations, and in certain situations (hot paths, etc) they even make more sense than trying desperately to keep things fast and immutable, but having the ability to make them opt-in rather than opt-out would definitely be handy.
Immutability isn't necessarily linked to variable assignments, is it? I think most use-cases people run into are related to object (im)mutability, with the introduction and confusion around
constdefinitely not helping there.lautarodragan commentedon Jan 25, 2021
00ff0000red we were talking about
readonlyhere really. I don't think anyone hadconstin mind, since we're not talking about completely prohibiting anything — just makingreadonlythe default wherever that keyword is currently needed.btoo commentedon Mar 25, 2021
how about adding a
mutablemodifier as the immutable-by-default flag'sreadonlycounterpart:lautarodragan commentedon Apr 28, 2021
btoo I did mention the possibility of using a new keyword
mutablefor interface fields, but I had not thought of arrays at the time.Love your idea!
I think if we were to tackle this, your proposal may fall under #42357 too.
lautarodragan commentedon May 20, 2021
A cheaper, easier to implement middle ground could be #21152
benwiley4000 commentedon Jul 30, 2021
I'm personally less into the idea of a read-only interface (as a solution to this problem). We already have
Readonly<{}>for type aliases but I tend not to use it for exported/shared types because it's annoying on the user side to be forced to build the object in one way or another.The way I think about it, when I'm declaring the type, I shouldn't care about immutability. On the user side, with
readonly(ormutable) I'm able to declare my intentions for how this specific implementing data of the shared type will be used.In fact it would be pretty annoying if lots of shared type authors started wrapping types as readonly but we didn't have a
mutablekeyword to go with it. That would mean it's pretty hard to build this type across multiple lines.With a
Readonlytype alias we can use:With readonly interfaces we could hope that could work but I'd imagine the semantics would be different.
rope-hmg commentedon Dec 23, 2021
If this were to be the default I would end up having to ask for the opposite of my current feature request #35313
I like the idea of having immutable by default since that's also how Rust and many other modern and functional languages do things. One of the current issues with marking things
readonlyis trying to useReadonly<T>on classes. It doesn't actually do anything for you because you can still call the methods that mutate the object. We haveReadonlyArray,ReadonlyMapandReadonlySetto address this for those types, but nothing general.I'd appreciate some feedback on my feature request which tries to address the above. Or, if this is something that's adopted then I'd like some feedback on the opposite version of my idea.
mmkal commentedon Apr 10, 2022
If
d.tscompatibility is a concern, could themutablekeyword be disallowed in declarations? So for a source filefoo.ts:the generated
foo.d.tscould be:the only downside would be that manually written declarations wouldn’t be able to use this feature (yet), but that doesn’t seem like a big deal to me.
lautarodragan commentedon Feb 20, 2023
I think all of these immutable-by-default or
readonly interface(either shallow or deep, ideally deep) are made redundant / unnecessary if Records and Tuples reaches Stage 3.adrian-gierakowski commentedon Feb 20, 2023
Some people think that this proposal is bad for immutability in TS
lautarodragan commentedon Feb 21, 2023
Adrian (@adrian-gierakowski) maybe so. But many of us are already following that pattern, and would love first-class support of it.
Regardless, if that proposal winds up being rejected by TC39, we can always continue this discussion and propose ts-specific improvements to immutability.
Kazark commentedon Jul 14, 2025
Looks like the ECMAScript Records & Tuples proposal has been withdrawn 😞 tc39/proposal-record-tuple#394