readonly, default
Adding flags under the strict
umbrella to default to immutability in different cases, and a new mutable
keyword.
I'm creating this issue as a _parent issue_ to track a couple of issues that already exist for specific cases:
Off of the top of my head, these flags would have some direct advantages:
readonly
keyword or use T[]
rather than ReadonlyArray<T>
accidentally. Defaulting to immutability would help prevent these accidents, and a mutable
keyword would be easy to find by tslint
or even a simple grep
, to automatically request review in PRs or leave an automated comment this PR introduces mutation
. 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 = '馃憢馃寧' // ok
My suggestion meets these guidelines:
@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:
d.ts
with readonly
, never mutable
(possibly behind another flag?)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.
Why not a lint rule where readonly
is always required?
And if they want stuff to be mutable, add eslint-disable-next-line.
Not much to say about methods, though.
Why not a lint rule where
readonly
is always required?And if they want stuff to be mutable, add eslint-disable-next-line.
Not much to say about methods, though.
Three reasons:
readonly
in 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. I've started using prefer-readonly-type with a small project (9K SLOC) and ended up having to add a 346 readonly
s throughout the code and 18 // eslint-disable-next-line functional/prefer-readonly-type
comments. I'd much rather only have to add 18 mutable
s instead. The code would be much cleaner and easier to maintain.
One of the reasons for the high readonly
count is that the prefer-readonly-type
doesn't support wrapping objects with Readonly<>
.
I use readonly
as much as possible, myself. I have 1019 usages of readonly
in 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.ts
emit has to look like this,
export type Immutable = { readonly x : number };
export type Mutable = { mutable x : number };
If it was this instead,
export type Immutable = { readonly x : number };
export type Mutable = { x : number };
Then downstream libraries that use the flag will think Mutable
is immutable.
If it was this instead,
export type Immutable = { x : number };
export type Mutable = { mutable x : number };
Then downstream libraries that do not use the flag will think Immutable
is mutable.
Of course, with the introduction of this mutable
keyword, 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.ts
files, 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,
export type Immutable = { readonly x : number };
/* magic-pre-processor-directive-that-says-following-type-is-mutable */
export type Mutable = { x : number };
Older versions of TS correctly see Mutable
as being mutable.
No behaviour has changed. It looks like regular emit.
Newer versions of TS with the flag correctly see Mutable
as being mutable.
With the flag switched on, it should think that Mutable
is immutable, since readonly
is the default property modifier. But the magical comment should force TS to treat Mutable
as having mutable
properties by default.
Newer versions of TS without the flag correctly see Mutable
as 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.
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.
Oh, wait. downlevel-dts
is a thing.
https://github.com/sandersn/downlevel-dts
Even if new emit is incompatible, downlevel-dts can be used.
Making readonly
the default will make TypeScript
a lot more powerful. In our codebases we use prefer-readonly-type to automatically add readonly
to everything, and we don't have any eslint-ignore
. Still, it's annoying to see and read all those Readonly
.
If TypeScript
wants 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.
Most helpful comment
Three reasons:
readonly
in 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.