Skip to content

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

Closed

Description

🔍 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.

Activity

  1. MartinJohns commented on Mar 3, 2026

    @MartinJohns
    Contributor

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

    I haven't seen any mention of this. Especially not considering the duplicated issues are still open.

  2. denis-migdal commented on Mar 3, 2026

    @denis-migdal
    Author

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

    I haven't seen any mention of this. Especially not considering the duplicated issues are still open.

    I though I saw a pinned issue few weeks/months (?) ago by Ryan.
    But maybe this was only for closed issue ?

    However, I'm not sure if old opened issues are still watched by the team ?

  3. MartinJohns commented on Mar 3, 2026

    @MartinJohns
    Contributor

    There's #62827, which refers to LS bug issues (which was mentioned in the blog).

    I'm not sure if old opened issues are still watched by the team

    Generally they are. Last word is with the team of course.

  4. denis-migdal commented on Mar 3, 2026

    @denis-migdal
    Author

    It was something else.
    A little like the beginning of https://github.com/microsoft/TypeScript/blob/main/CONTRIBUTING.md (also modified 3 month ago).

    I remember something like:

    • "In case of regression, do not post on the closed issue, but open a new one for the triage".
    • "If you like an issue, do not post "me too", use the emote to show support"

    Now I'm confused, did I dream it ? I can have quite realistic dreams sometimes.

  5. nmain commented on Mar 3, 2026

    @nmain

    Are you thinking of #62827? That was specific to language server issues.

    Edit: Whoops, didn't see the response right above. My bad.

  6. RyanCavanaugh commented on Mar 3, 2026

    @RyanCavanaugh
    Member

    If this is the same as #43553, no, we don't want new duplicates of issues, please close this. Age of existing open suggestion is not a relevant factor to anything.

    If this is different, please explain exactly how.

  7. denis-migdal commented on Mar 3, 2026

    @denis-migdal
    Author

    Ok.

    Are the old opened issues still read by the team ?

  8. RyanCavanaugh commented on Mar 3, 2026

    @RyanCavanaugh
    Member

    Are the old opened issues still read by the team?

    Yes

  9. locked as resolved and limited conversation to collaborators on May 4, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

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