-
-
Notifications
You must be signed in to change notification settings - Fork 3.7k
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
[TS] Let's discuss how to use readonly
and protected
#12457
Comments
readonlyThe problem:In JsDoc readonly tag is used as an indication, that the annotated property shouldn't be modified by the client code. The most natural representation of this in TypeScript is
Workarounds:Here are some ideas of what we can do about it. We can have different solutions for different cases. Not using
|
protectedThe meaning of protected in JsDoc is different from TypeScript doesn't have any Two options that came to my mind:Keep members public and annotate with internal tagclass Test {
/** @internal */
public method(): void { ... }
}
testObj.method(); We can not only mark it internal in API docs, but also can exclude it from generated .d.ts files using However, skipping it from documentation introduces a small inconsistency: // test.ts
class Test {
/** @internal */
public method1(): void { ... }
private method2(): void { ... }
}
// test.d.ts
class Test {
// It doesn't have the type, because it's an implementation detail. But it is still there, so the name
// can't be accidentally used in derived class.
private method2;
// But... there is not method1.
} Same of internal properties are not used outside the class, but overridden in derived ones. So, we can use class Test {
/** @internal */
protected method(): void { ... }
} Cast to 'any'We can make the property private and cast it to any if we want to use it: class Test {
private method(): void { ... }
}
( testObj as any ).method(); Because this is used in the same package, we don't force users to do ugly casts. It's incompatible with overriding. We would have to stick with |
If you don't mind, I will add my thoughts. Have you thought about using interfaces and following "interface segregation principle"? You could operate on interfaces instead of class and thanks to this, you could have to interfaces: public and protected. Public interfaces might be exposed, and protection will be only visible in the specific package. Moreover, we had many problems in our code base if we used specific classes instead of interfaces - problems with types, mocking, etc. So we try to use interfaces everywhere. |
@bczerwonka-cksource, yes, we have. We used that pattern in a few places, like Emitter. However, in this case, the usage of this internal part is in the same file. If you have a method/function in one file (
|
Thanks for the explanation 👍 I don't know totally how you generate docs, so probably my comments are worthless ;) However, I will share my experience/thoughts based on our project. So, in our project, we decided to export from the module only things that can be used from other modules. For this, we decided to use `index.ts` files that contain all "public" exports - this file only re-export dependencies from the module. Of course, you still have access to specific files when you provide the correct path ... For limiting this, we use ESLint rule https://eslint.org/docs/latest/rules/no-restricted-imports Example: file a.ts
file b.ts
file index.ts
However, in your project is a case with "protected" methods/properties that is tricky. In our project, we don't allow for it - something is public and generally visible or should be hidden in class. Probably we do not have your cases ;) For me, using `any` is totally bad... IMO there is no sense using TS and types when in the end, you can use `any` ;) Maybe using some naming convention will be good enough? In our project for private things, we use Sorry for spamming ;) |
My proposal: protected:
readonly:
|
I feel like we should focus on the integrator/plugin developer experience so maybe we should make the API as clean as possible for such cases. protected
readonly
|
@niegowski, @bczerwonka-cksource , I agree with the interface approach. But keep in mind, that: // a.ts
/** @internal */
export interface TestInternal {
_fn();
}
// Can't implement TestInternal,
export class Test /* implements TestInternal */ {
// because this is not public,
private _fn(): void { ... }
}
// b.ts
import {Test, TestInternal } from 'a';
export function helperFunc( test: Test ) {
// so this cast is still required here.
( test as TestInternal )._fn();
} Is that acceptable for you? |
🤔 Why is it not public? If you have an interface, it means that expected that someone will use those methods. It is a contract for others. If you create a private function, you should not add them to the interface. I assume that // a.ts
export interface TestInternal {
_fn();
}
export interface TestPublic {
hello();
}
// Can't implement TestInternal,
export class Test implements TestPublic, TestInternal {
public hello(): void {}
public _fn(): void {}
}
export function helperFunc( test: TestInternal ) {
test._fn();
}
// another module should use TestPublic interface only
export function hi( test: TestPublic ) {
test.hello();
} I don't know how you exactly use "protected" methods, but IMO code that is exposed publicly only because of testing should be hidden, and in tests, you should test everything only through public contracts. But maybe I don't know the use case. Maybe it is a good question. When/why do you use "protected" (package private) methods/fields? |
Meeting notesInternalLet's remove @Protected tags from members that are only exposed to tests. For other cases - change @Protected to @internal and make methods public (protected). ReadonlyIf property is not meant to be changed after construction, but is updated because of performance/hack: add internal setter (may have different name or be regular method, so we can avoid indirection in getter). Unreleted thoughts:
Thanks @niegowski, @bczerwonka-cksource |
readonly
While working on #11708 we came across an difference in how we used JSDoc's
@readonly
and the semantics of TS'sreadonly
keyword.In TS's world, a readonly property cannot be modified after being initialized in the constructor (similar to
const
). So far, when migrating code we were defining properties asreadonly
if they were@readonly
in JSDoc.However, before the migration we allowed changing this property if the change was happening inside the class. This was sometimes used and so we were using workaround such as: https://github.com/ckeditor/ckeditor5/blob/master/packages/ckeditor5-engine/src/model/range.ts#L795-L796
We need to revisit how we port the JSDoc's
@readonly
from the perspective of:The output of this work should be:
readonly
now.protected
Similar to
readonly
– our semantics differs from the one used by TS. We need to clarify the change and come up with migration tips.The output of this work should be:
protected
now.Notes
The text was updated successfully, but these errors were encountered: