-
Notifications
You must be signed in to change notification settings - Fork 13.3k
Description
🔍 Search Terms
Related issues:
- Different types based on visibility #43553
- [feature] class properties that are "readonly in public, writable in private" or other permutations #37487
- Declare properties with a separate public and private types. #51597
- Class property
readonly?for external consumers #53707
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
- This wouldn't be a breaking change in existing TypeScript/JavaScript code
- This wouldn't change the runtime behavior of existing JavaScript code
- This could be implemented without emitting different JS based on the types of the expressions
- This isn't a runtime feature (e.g. library functionality, non-ECMAScript syntax with JavaScript output, new syntax sugar for JS, etc.)
- This isn't a request to add a new utility type: https://github.com/microsoft/TypeScript/wiki/No-New-Utility-Types
- This feature would agree with the rest of our Design Goals: https://github.com/Microsoft/TypeScript/wiki/TypeScript-Design-Goals
⭐ 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:
Wneeds to be assignable toR, i.e. the protected value needs to be compatible with the public type.- the public type needs to be
readonlyas we can't allow the outside to set aRtofooas it might be incompatible withW. - the public type needs to be
declareas 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".
- if a public and a protected definition are given without
- when subclassing it:
- check if
foohas a protected definition. - if
declare [public] readonly foois 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] foois used (i.e. withoutreadonly, check the type with the protected type definition, the whole property becomes public.
- check if
- when accessing it:
- internally/in a method, i.e.
this: Foo, use the protected definition. - from outside, use the public definition.
- internally/in a method, i.e.
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
- What do you want to use this for?
Having attributes with internal types.
- 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;
}- 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.