Skip to content

Enable declare public readonly + protected declaration merge in classes #63209

@denis-migdal

Description

@denis-migdal

🔍 Search Terms

Related issues:

This issue is a duplicate of some of the issues above.
However, I add 2 more workarounds, and more technical details.

AFAIK, you are now asking to open new issue in order to do the triage instead of digging up old issues.

✅ Viability Checklist

⭐ Suggestion

Being able to provide a declare public readonly and a protected declaration in classes :

// [] means the keyword is optional.
interface R                     { get(): void; }
interface W extends R { set(): void; }

class Foo {
       declare [public] readonly foo: R;
       [declare] protected [readonly] foo: W; 
}

Then, when Foo.foo is used outside of the class, the public interface is used. When it is used inside the class or inside the method, i.e. a function that has a this: Foo, the protected interface is used.

The following constraint would be required:

  • W needs to be assignable to R, i.e. the protected value needs to be compatible with the public type.
  • the public type needs to be readonly as we can't allow the outside to set a R to foo as it might be incompatible with W.
  • the public type needs to be declare as we can't provide a value to the public interface, even when subclassing it.

I'll suggest doing the following:

  • when declaring the class, performs the check above, and register the 2 definitions.
    • if a public and a protected definition are given without declare readonly, raise an error message "Attribute declaration merging are only possible with declare public readonly".
  • when subclassing it:
    • check if foo has a protected definition.
    • if declare [public] readonly foo is used, check the type with the public type definition.
    • if an affectation is made to public foo, check the type with the protected type definition, the whole property becomes public.
    • if declare [public] foo is used (i.e. without readonly, check the type with the protected type definition, the whole property becomes public.
  • when accessing it:
    • internally/in a method, i.e. this: Foo, use the protected definition.
    • from outside, use the public definition.

Note that a method might expose the protected type if it returns the attribute:

foo(this: Foo) {
    return this.foo;
}

This might be voluntary, the user has to perform the cast himself:

foo(this: Foo): R {
    return this.foo;
}

📃 Motivating Example

interface WEvent extends REvent {
      trigger(): void;
}

class Signal {
      declare public readonly event: REvent;
      protected readonly event: WEvent;

      trigger() {
           this.event.trigger();
      }
}

💻 Use Cases

  1. What do you want to use this for?

Having attributes with internal types.

  1. What shortcomings exist with current approaches?

We need to adapt the JS code to TS by using a protected member and a getter:

class Foo {
      get foo(): R { return this._foo; }
      declare _foo: W;
}
  1. What workarounds are you using in the meantime?

Workaround 1: use an identity function.

function asInternal(a: R): W { return a as any; }

class Foo {
      readonly foo: R = ...;
      faa() {
            asInternal(foo).doStuff
      }
}

However, this is kind of fragile if the attribute is redefined in the subclasses.

Depending on the case, we can use a asMutable() function (removes readonly), or a asRW() :

import "asRW"
declare module "asRW" {
    export interface TasRW {
        <T>(ro: REvent<T>): REvent<T>&WEvent<T>
    }
}

// in asRW
export interface TasRW {}
function asRW(a: unknown) { return a; }

export default asRW as TasRW;

Workaround 2:

Add an internal interface:

interface Foo {
    foo: R;
}
interface InternalFoo extends Foo {
    foo: W;
}

class ImplFoo implements InternalFoo { ... }

export function createFoo(): Foo { return new ImplFoo(); }
// or
const XFoo = ImplFoo as {new(): Foo}
export default XFoo;

This requires to declare interfaces (not a bad thing in itself).

This makes subclassing a little more difficult as we have to remove the internal properties before exposing the new class.

However, when using helpers using internal features, e.g. :

function helper(f: Foo) {
      (f as InternalFoo).DO_STUFF()
}

This might cause issues as we could give a class implementing Foo, but not InternalFoo.

Making Foo and InternalFoo as abstract classes, with an abstract property in Foo, only defined in InternalFoo could work, but this starts to be quite hacky.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions