Open rope-hmg opened 4 years ago
How does the compiler know which methods are mutative and which ones aren’t?
Remove all the methods!
It knows because you mark the non mutating methods.
class Foo {
private state = 1;
mutateState() {
this.state += 1;
}
readonly getState() {
// Inside a readonly method this is treated as Readonly<this> which will mark all
// properties as readonly and only all you to call readonly methods.
return this.state;
}
}
Don't worry about the syntax at the moment. I don't know what would be best for that. Right now I just want to discuss the idea.
It’s a good idea, but I’d expect it to work something like “const correctness” in C/C++—that is, non-mutating methods should not be able to call anything mutative, and this should be verified by the compiler. Otherwise the annotation (whatever it ends up being) has no teeth and would be easy to get wrong.
I could have a go at making a PR if that would be useful?
I've been experimenting with some code and I've refined my pattern some more. So the following is how it works today:
export class Rectangle {
public constructor(
private width: number,
private height: number,
) {}
public getArea(this: InternalReadonlyRectangle): number {
return this.width * this.height;
}
public setWidth(value: number): void {
this.width = value;
}
public setHeight(value: number): void {
this.height = value;
}
}
interface InternalReadonlyRectangle {
readonly width: number;
readonly height: number;
getArea(): number;
}
export interface ReadonlyRectangle {
getArea(): number;
}
This is not bad, but it does leave some things to be desired.
I would like to be able to write this:
class Rectangle {
public constructor(
private width: number,
private height: number,
) {}
public getArea(this: Readonly<this>): number {
return this.width * this.height;
}
// Some syntax ideas:
// readonly public getArea(): number
// const public getArea(): number
// public getArea(this as const): number
// Rust style:
// public getArea(Readonly<this>): number
// Or just what we have today. I'm not too bothered really.
public setWidth(value: number): void {
this.width = value;
}
public setHeight(value: number): void {
this.height = value;
}
}
Two more things that I have either noticed, or thought about:
as const
.this: ReadonlyFoo
in the readonly methods. TypeScript doesn't know that they're the same property.Note method(this: Readonly
class Foo {
x: number = 0;
test(this: Readonly<Foo>){
// this.x = 5; will raise error.
console.log(x);
}
}
class Bar extends Foo {
test(){
this.x = 5; // Compiler won't complain.
}
}
getArea(this: Readonly<this>): number
is a bit too wordy for my preference.
I would prefer something like const getArea(): number
, and I would prefer const
over readonly
as it may help with understanding if people are familiar with the concept in c++. Though readonly
would be more consistent with typescript.
https://stackoverflow.com/a/751783
It may be worth thinking about the idea of mutable
too, I'd never heard of it before reading that answer though I'm not a C++ developer. There is an issue about it already #24509.
Also, I don't think this would be a breaking change if the const foo(): void
syntax was used, though it would be a breaking change for a library to start using it.
Another blocker with the method(this: PrivateReadonlyWhatever): void
is:
You can't model private properties with an interface because the properties in an interface are public.
@OldStarchy Using const
might be a better idea anyway since as const
is deep and I would like this feature to also be deep.
What I mean by that is:
class Thing {
private value = { prop: { inner: 0 } };
setValue(newValue: number): void {
this.value.prop.inner = newValue;
}
const getValue(): number {
// This is an error because the method is marked as const so this.value has type { readonly prop: { readonly inner: number } }.
// Similar to how `as const` acts on object literals.
this.value.prop.inner = 15;
return this.value.prop.inner;
}
}
While I like this idea, it would break backwards compatibility pretty badly. Currently Readonly
does not exclude any methods, the proposal as stated would exclude all of them by default.
A backwards-compatible alternative is that instead of marking non-mutating methods as readonly
, you would mark mutating methods as mutable
. Only methods so marked would be excluded. This would allow existing codebases to continue to compile, and the mutable
modifier could be added incrementally as desired.
My proposal is backwards compatible because marking a method as const
is opt-in. Once you do, you will break any code that was assuming it could call any method on a Readonly<T>
. If your code was a library this change would require a major version bump since it'd be a semver breaking change. If you're code is an application then you just have to refactor all the use-sites.
What you've proposed wouldn't be backwards compatible since it would make all methods const
by default. It's probably not too crazy to say the majority of methods on a class in any given codebase are mutable. Marking mutable methods would mean that these codebases would have to annotate a large portion of their methods. With the change I'm proposing mutable would be the default as it is now.
Your proposed change would make the following an error:
class Something {
private counter: number;
public doThing(): void {
this.counter += 1;
// ^^^^^^^^^^^^^^^^^
// Error: Can't mutable `this.counter` in non-mutable method `doThing`.
// Mutable methods must be marked as `mutable`.
}
}
The above would have to be the case otherwise there would be no guarantee that a plain method (i.e. one not marked as mutable
) doesn't actually mutate the class. This guarantee is required since you want to remove mutable
methods when using Readonly<T>
. Consider the following:
const a: Readonly<Something> = new Something();
a.doThing();
// There would be no error here, even though there should be, since `doThing` is not
// marked `mutable` and therefore not remove from `Readonly<Something>`.
Personally, I would prefer immutable by default, but it would cause a lot of headaches to change retroactively.
Search Terms
readonly, as const, Readonly,
Suggestion
Readonly to work the way Readonly<T[]> does without having to create something like ReadonlyArray, ReadonlySet, etc...
I would love it if the
Readonly<T>
mapped type could exclude any method marked asreadonly
or maybeas cosnt
or something similar.Use Cases
At the moment I have to do this:
This gets very boring with a lot of types, also it's error prone. I could forget to add something to the interface, or worse I could accidentally add something that is mutable.
I can currently do
method(this: Readonly<this>) {...}
, which is helpful, but doesn't stop me calling mutable methods on the type.Inside a marked method you would be unable to mutate the state of member variables and you would unable to call other unmarked methods.
This would also allow you to remove the special case of
Readonly<T[]>
. The mutable methods of arrays could be marked with whatever syntax is decided and thenReadonly<T[]>
would just work like any otherReadonly<T>
. Currently I don't bother usingReadonly<T>
since it doesn't really help with anything except C style POD types.Examples
Checklist
My suggestion meets these guidelines: