Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

C++-style const modifier on class members #58236

Open
6 tasks done
OldStarchy opened this issue Apr 18, 2024 · 4 comments
Open
6 tasks done

C++-style const modifier on class members #58236

OldStarchy opened this issue Apr 18, 2024 · 4 comments
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript

Comments

@OldStarchy
Copy link

OldStarchy commented Apr 18, 2024

πŸ” Search Terms

const method parameter keyword readonly class
C++ like const methods
class method with const keyword
"as const" for class type

βœ… Viability Checklist

⭐ Suggestion

I'm sure I've seen this suggestion somewhere before but I couldn't find it #35313

Allow using as const on class objects by annotating methods as const (syntax tbd).

A const method would (like in C++) not allow modifying properties of this or calling any method not also marked as const.

An object marked with as const would similarly be readonly and only methods marked as const would be callable.

πŸ“ƒ Motivating Example

Keeping track of mutable vs immutable can be tricky when you're not used to the library you're using (or if its poorly written). It's very important to get it right in many libraries (react state, vue refs (shallow vs deep), preact/signals) where modifying values may or may not be required.

Debugging mutability bugs can be a pain sometimes too

const start = new Point(0, 0);
const line: Point[] = [];

for (let i = 0; i < 10; i++) {
  line.push(start.add(i * 10, Math.random()));
}

// equivalent to
const line = new Array(10).fill(new Point(450, Math.random()));

Declaration syntax 1 using a const decoration

class Point {
	constructor(public x: number, public y: number) { }
	const clone(): Point { ... }

	add(other: PointLike): this { ... }
}

const p = new Point(0, 0) as const; // type is { readonly x: number, readonly y: number, readonly clone(): Point } (`add` has been removed)

Declaration syntax 2 using this: const parameter

class Point {
	constructor(public x: number, public y: number) { }
	clone(this: const): Point { ... }

	add(other: PointLike): this { ... }
}

const p = new Point(0, 0) as const; // type is { readonly x: number, readonly y: number, readonly clone(): Point } (`add` has been removed)

I personally much prefer the first syntax where const is a decoration.

Parameter syntax 1

function noModifyPoint(p: const Point) {
	p.clone().add(1, 1);
}

function modifyPoint(p: Point) {
	p.add(1, 1);
}

const p = new Point(0, 0) as const;

noModifyPoint(p); // ok
modifyPoint(p); // Argument of type `const Point` is not assignable to parameter of type `Point`. Type `const Point` is missing the following properties from type `Point`: add

Not to be confused with

function foo() {
	const p = arguments[0];
	//...
}

(as an aside a more appropriate syntax for that would be foo(const p: Point) or foo(const p: const Point) but that's OoS)

Parameter syntax 2

function noModifyPoint(p: Readonly<Point>) {
	p.clone().add(1, 1);
}

function modifyPoint(p: Point) {
	p.add(1, 1);
}

const p = new Point(0, 0) as const;

noModifyPoint(p); // ok
modifyPoint(p); // Argument of type `Readonly<Point>` is not assignable to parameter of type `Point`. Type `Readonly<Point>` is missing the following properties from type `Point`: add

Again I prefer syntax 1. The const type inference needs to happen in the compiler, making it look like a utility type would be confusing. The use of const also mirrors the declaration syntax 1.

The inference of the new readonly type should ℒ️ be simple enough, convert any r/w properties to readonly, and remove any functions not marked as const.

For simplicity arrow method properties should be handled as any other property would.

class Point {
	constructor(public x: number, public y: number) { }
	clone(this: const): Point { ... }

	unsafelyModifyThis: () => { this.x = 10; }
}

const p = new Point(0, 0) as const;

p.unsafelyModifyThis(); // OK

The reason I suggest this is only because there needs to be a line drawn somewhere as to how complex this feature becomes. As mentioned in TypeScripts third non-goal this is a tradeoff between usefulness and simplicity.

Its already possible to break the type system and its up to the programmers to not do stupid things.

Other types

const a1 = [1, 2, 3] as const;
const a2 = [1, 2, 3] as Readonly<[number, number, number]>;

function aFunc1<T>(p: const Array<T>) {
	// syntactically valid but is probably useful since Array doesn't have any built-in const decorations
	// the resulting type would be `{ readonly [index: number]: T; readonly length: number }`
	// also not sure how it would work with the `ReadonlyArray<T>` built-in type
	...
}

function prim(n: const number) { ... } //error

πŸ’» Use Cases

  1. What do you want to use this for?
    This would be most useful for data classes (aka structs) that represent non-primative data structures.

  2. What shortcomings exist with current approaches?
    Workarounds are not DRY, requiring manual definition for const variants of all types

  3. What workarounds are you using in the meantime?
    Manually defining a ReadonlyPoint variation and casting to it.

    interface ReadonlyPoint {
    	readonly x: number;
    	readonly y: number;
    	readonly xy: [number, number];
    
    	clone(): Point;
    }
    
    class Point implements ReadonlyPoint {
    	constructor(public x: number, public y: number) { }
    
    	clone(): Point {
    		return new Point(this.x, this.y);
    	}
    
    	add(other: PointLike): this {
    		this.x += other.x;
    		this.y += other.y;
    		return this;
    	}
    
    	//...
    }
    
    const p = new Point(0, 0) as ReadonlyPoint;
    
    p.add(p); // error
@RyanCavanaugh RyanCavanaugh added Suggestion An idea for TypeScript Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature labels Apr 18, 2024
@RyanCavanaugh RyanCavanaugh changed the title Define "as const" for class types C++-style const modifier on class members Apr 18, 2024
@RyanCavanaugh
Copy link
Member

We're considering closing the { readonly x: number } -> { x: number } soundness hole under a flag, at which point you'd be able to express this solely in terms of existing operations, e.g.

class Point
  readonly x: number;
  readonly y: number;
  modify(this: Mutable<Point>, x: number, y: number) {

  }
}

@MartinJohns
Copy link
Contributor

Related: #35313

@OldStarchy
Copy link
Author

class Point {
	readonly x: number;
	readonly y: number;

	modify(this: Mutable<Point>, x: number, y: number) {
	  this.x = x;
      this.y = y;
    }
}

const p = new Point(0, 0);

p.modify(3, 4); // error? (argument of type Point is not assignable to type Mutable<Point>)

const q = p as Mutable<Point>; // then wouldn't this also error with the same type conversion?
q.modify(3, 4);

// and if not, then i could also do this

function badFunc(point: Point) {
  (point as Mutable<Point>).modify(3, 4);
}

Is this what you're intending?

I was thinking recently that I do prefer rust's style of const-by-default, but with this syntax I'm not sure the correct way to create something mutable.

@OldStarchy
Copy link
Author

I just realized I didn't specify this explicitly, but ideally the "const" flag would work for any parameter of any function

function createConvexHull(const poly: Polygon): Polygon {
	//...
}

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Awaiting More Feedback This means we'd like to hear from more people who would be helped by this feature Suggestion An idea for TypeScript
Projects
None yet
Development

No branches or pull requests

3 participants