Prior to diving in, it's important to note that the extends
keyword is used when extending a class. You extend a class and implement an interface. More details on this can be found later in this post.
class SpecialTest extends Test {
Be cautious of mixing up string
with String
, as it can lead to confusion. Type annotations should typically use string
(all lowercase).
Furthermore, there's no need to manually assign constructor parameters. The original code:
class Test {
private name: string;
constructor(name: string) {
this.name = name;
}
// ...
}
Can be simplified to:
class Test {
constructor(private name: string) {
}
// ...
}
Now you have various options to tackle your issue.
Protected Member
If you make the name
member protected, it can be accessed within subclasses:
class Test {
protected name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return this.name;
}
setName(name: string): void {
this.name = name;
}
}
class SpecialTest extends Test {
getName(): string {
return '';
}
setName(name: string): void {
}
}
Interface
This solution aligns well with your requirements.
If you extract the public members into an interface, both classes can be treated as that interface (even if the implements
keyword isn't explicitly used - TypeScript is structurally typed).
interface SomeTest {
getName(): string;
setName(name: string): void;
}
You could opt for explicit implementation:
class SpecialTest implements SomeTest {
private name: string;
getName(): string {
return '';
}
setName(name: string): void {
}
}
Your code can now rely on the interface rather than a concrete class.
Using a Class as an Interface
While technically possible to reference a class as an interface using implements MyClass
, this approach has its own set of pitfalls.
Firstly, it adds unnecessary complexity for anyone reviewing the code later on, including future maintenance tasks. Additionally, mixing up extends
and implements
may introduce subtle bugs in the future when the inherited class is modified. Maintainers will need to be vigilant about which keyword is utilized. All of this just to maintain nominal habits in a structural language.
Interfaces are abstract and more stable, while classes are concrete and subject to change. Using a class as an interface undermines the concept of relying on consistent abstractions and instead forces dependence on unstable concrete classes.
Consider the repercussions of "classes as interfaces" scattered throughout a program. A modification to a class, like adding a method, might inadvertently trigger changes across various parts of the program... how many sections of the program would reject input because the input doesn't contain an unused method?
The preferable alternatives (when access modifier compatibility isn't an issue)...
Create an interface based on the class:
interface MyInterface extends MyClass {
}
Or simply refrain from referencing the original class entirely in the second class and let the structural type system validate compatibility.
On a side note, depending on your TSLint settings, weak types (e.g., an interface with only optional types) may invoke the no-empty-interface
rule.
Specific Case of Private Members
None of these approaches (using a class as an interface, generating an interface from a class, or structural typing) address the issue of private members. Hence, creating an interface with the public members is the most effective solution.
In scenarios involving private members, such as the one described, contemplate the consequences of continuing with the initial pattern. Preserving the practice of treating the class as an interface would likely involve altering the visibility of members, like so:
class Test {
public name: string;
constructor(name: string) {
this.name = name;
}
getName() {
return this.name;
}
setName(name: string): void {
this.name = name;
}
}
Ultimately, this deviates from solid object-oriented principles.