Typescript: Feature Request: Readonly<T> should remove mutable methods

Created on 23 Nov 2019  Â·  8Comments  Â·  Source: microsoft/TypeScript

Search Terms

readonly, as const, Readonly,

Suggestion

Readonly to work the way Readonly, ReadonlySet, etc...

I would love it if the Readonly<T> mapped type could exclude any method marked as readonly or maybe as cosnt or something similar.

Use Cases

At the moment I have to do this:

class Vector2 {
    public constructor(
        public x: number,
        public x: number,
    ) {}

    public add(other: ReadonlyVector2): Vector2 {
        return this.clone().addInPlace(other);
    }

    public addInPlace(other: ReadonlyVector2): this {
        this.x += other.x;
        this.y += other.y;
        return this;
    }

    public clone(): Vector2 {
        return new Vector2(this.x, this.y);
    }
}

interface ReadonlyVector2 {
    readonly x: number;
    readonly y: number;
    add(other: ReadonlyVector2): Vector2;
    clone(): Vector2;
}

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 then Readonly<T[]> would just work like any other Readonly<T>. Currently I don't bother using Readonly<T> since it doesn't really help with anything except C style POD types.

Examples

class Vector2 {
    public constructor(
        public x: number,
        public x: number,
    ) {}

    public readonly add(other: Readonly<Vector2>): Vector2 {
        return this.clone().addInPlace(other);
    }

    public addInPlace(other: Readonly<Vector2>): this {
        this.x += other.x;
        this.y += other.y;
        return this;
    }

    public const clone(): Vector2 {
        return new Vector2(this.x, this.y);
    }
}

Checklist

My suggestion meets these guidelines:

  • [ ] This wouldn't be a breaking change in existing TypeScript/JavaScript code
  • [x] This wouldn't change the runtime behavior of existing JavaScript code
  • [x] This could be implemented without emitting different JS based on the types of the expressions
  • [x] This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, etc.)
  • [x] This feature would agree with the rest of TypeScript's Design Goals.
Awaiting More Feedback Suggestion

Most helpful comment

How does the compiler know which methods are mutative and which ones aren’t?

All 8 comments

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.

  1. You have to create multiple interfaces if you want to have readonly private members.
  2. It's a lot of boiler plate to write for every type.

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:

  1. Should the readonly be deep? Personally I think yes. Like the as const.
  2. With the current code, if something is only read in a readonly context, then TypeScript complains that the value is never read. This is caused by using this: ReadonlyFoo in the readonly methods. TypeScript doesn't know that they're the same property.

Note method(this: Readonly) won't work across inheritance.

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.
    }
}
Was this page helpful?
0 / 5 - 0 ratings

Related issues

Antony-Jones picture Antony-Jones  Â·  3Comments

remojansen picture remojansen  Â·  3Comments

kyasbal-1994 picture kyasbal-1994  Â·  3Comments

dlaberge picture dlaberge  Â·  3Comments

weswigham picture weswigham  Â·  3Comments