Leverage classes as interfaces in Typescript

Trying to implement a Class as Interface for another class in order to create an updated mockClass for Testing.

According to https://angular.io/guide/styleguide#interfaces, this approach should be feasible and beneficial.

Referenced example: Export class as interface in Angular2

Encountering an error when using Typescript 2.5.3 in VSCode 1.17.2

Error appears in class SpecialTest

[ts]
Class 'SpecialTest' incorrectly implements interface 'Test'.
  Types have separate declarations of a private property 'name'.

Sample code:

class Test {
    private name: string;

    constructor(name: string) {
        this.name = name;
    }

    getName() {
        return this.name;
    }
    setName(name: string): void {
        this.name = name;
}
}

class SpecialTest implements Test {
    private name: string;

    getName(): string {
        return '';
    }
    setName(name: string): void {
    }
}
 

What am I missing?

EDIT: use string instead of String, as @fenton suggested

Answer №1

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.

Answer №2

To transform a class into an interface, focusing only on the public properties and excluding implementation details, you can utilize Pick<Type, Keys> in combination with the keyof operator.

class Example {
  property1: any;
  private property2: any;
}

class SpecialExample implements Pick<Example, keyof Example> {
  property1: any;
}

Pick<Type, Keys> Creates a type by selecting the properties specified in Keys from Type.

[...] keyof T, which is the index type query operator. For any type T, keyof T represents the union of known, public property names of T.

In this specific instance,

Pick<Example, keyof Example>
translates to Pick<Example, 'property1'> resulting in { property1: any }.

Answer №3

In the provided example, the parent class is intended to be abstract and function as an interface. It's important to note that TypeScript interfaces do not support access modifiers, meaning all properties within an interface are inherently public.

However, a challenge arises when utilizing a concrete class like Test as an interface because private properties cannot be implemented or overridden in this scenario.

To address this issue, both Test and SpecialTest should either implement an abstract class acting as an interface (with only public members) or inherit from it (allowing for protected members within the abstract class).

Answer №4

In reference to @Fenton's explanation on the topic of Utilizing a Class as an Interface

An alternative, clearer method of declaring

class SpecialTest implements Test
is by utilizing the InstanceType<T> utility type. For example, you can write it as
class SpecialTest implements InstanceType<typeof Test>
. This approach makes the code easier to comprehend.

It is recommended to use interfaces in your own code, reserving this particular technique for situations where it has proven valuable or necessary, such as when creating a decorator for a 3rd-party class.

Answer №5

within the article you referenced here, did you carry out the final segment of it in your own implementation:

It is important to note that any class can serve as an interface in >TypeScript. Therefore, if there is no distinct necessity to separate between interface and >implementation, it could simply be a single concrete class:

export class Foo {
    bar() { ... }

    baz() { ... }
 }

...
provider: [Foo]
...

This can then be used later as an interface if needed:

 export class SomeFoo implements Foo { ... }

 ...
 provider: [{ provide: Foo, useClass: SomeFoo }]
 ...

Similar questions

If you have not found the answer to your question or you are interested in this topic, then look at other similar questions below or use the search

angulartechnology: Error 62: Property 'resolveAndCreate' is not found in 'Injector' type

After updating to ng2 beta 17, I encountered this error: Error:(62, 33) TS2339: Property 'resolveAndCreate' does not exist on type 'typeof Injector'. var injector = Injector.resolveAndCreate( [ TodoService, ...

How can I assign a specific class to certain elements within an *ngFor loop in Angular?

I have a situation where I am utilizing the *ngFor directive to display table data with the help of *ngFor="let record of records". In this scenario, I am looking to assign a custom CSS class to the 'record' based on specific conditions; for exam ...

Issues with rapid refreshing are arising in Vite while dynamically importing components with React and typescript

In my quest to develop a multistep form, I have organized my data in a separate file for constants as shown below: import { lazy } from 'react'; export const steps = [ { id: 0, name: 'Personal Info', component: lazy(() ...

Encountering a node module issue when implementing graphql in a TypeScript project

I encountered issues when attempting to utilize the @types/graphql package alongside TypeScript Node Starter node_modules//subscription/subscribe.d.ts(17,4): error TS2314: Generic type AsyncIterator<T, E>' requires 2 type argument(s). node_modu ...

Transform a base64 image into a blob format for transmission to the backend via a form

Is there a way to convert a base64 string image to a blob image in order to send it to the backend using a form? I've tried some solutions like this one, but they didn't work for me. function b64toBlob(b64Data, contentType='', sliceSiz ...

Using Angular 2 with Typescript to call a JavaScript function

How can I correctly invoke a JavaScript function from a component in Angular 2 (TypeScript)? Below is the code for my component: import { ElementRef, AfterViewInit } from '@angular/core'; export class AppComponent implements AfterViewIni ...

Prevent Click Event on Angular Mat-Button

One of the challenges I'm facing involves a column with buttons within a mat-table. These buttons need to be enabled or disabled based on a value, which is working as intended. However, a new issue arises when a user clicks on a disabled button, resul ...

The output from the map() function does not accurately represent the data returned by my REST call

Struggling with a REST call in my http.service.ts file while working with Angular version 7 and RxJS. The goal is to extract the "cod" value from a JSON response obtained from a REST call to the openweather API. However, when I add map() to extract the des ...

Using TypeScript: creating functions without defining an interface

Can function props be used without an interface? I have a function with the following properties: from - HTML Element to - HTML Element coords - Array [2, 2] export const adjustElements = ({ from, to, coords }) => { let to_rect = to.getBoundingC ...

What is the process for integrating unit tests from external sources into an Angular project following an upgrade to version 15

As of Angular v15, the require.context function has been removed from the test.ts configuration file. I used to rely on require.context to expose tests outside of the Angular project to Karma. Now that it's no longer available: const contextGlobal = ...

Effective ways to manage extensive forms in Angular 2

I have a complex form in my application. What is the most effective way to collect data from this form and transmit it to the API using Angular 5? ...

I prefer not to permit components to receive undefined values

When using swr, the data type is IAge| undefined. I want to avoid passing undefined to AgeComponent, so I need the age type to be strictly IAge. Since AgeComponent does not allow undefined values, I am facing an error stating that 'IAge | undefined&ap ...

I am unable to process the information and transform it into a straightforward list

When using ngrx, I am able to retrieve two distinct data sets from storage. private getCatalog() { this.catalogStore.select(CatalogStoreSelectors.selectAllCatalogsLoadSuccess) .pipe( takeWhile(() => this.alive), take(1), ...

Exploring how NestJS can serialize bigint parameters within DTOs

I have data transfer objects (DTOs) with parameters that are of type bigint. However, when I receive these DTOs, the parameters always have a type of string. Here is an example: @Get("") async foo(@Query() query: Foo) { console.log(typeof Foo ...

Accessing dynamically generated text from an li element in Angular by transferring it from the HTML to the TypeScript component

Can you assist me with this issue? Below is the code snippet: Xpressions Total ={{response.total}} clear </div> <div class="exResult"> <ul> <li *ngFor='let item of response.data'> {{item.ph ...

Using Typescript to reduce an array of objects results in concatenation

In my quest to calculate the sum of integers from an array of objects stored in a redux slice, I have encountered a challenge. Here is the code snippet in question: export type Expense = { id: string; title: string; amount: number; date: st ...

"Learn how to programmatically close all panels in an ngb-accordion component in Angular 2 Release 6, leaving only

I have recently begun working on an accordion and I am trying to figure out how to make the first panel expand while keeping the others closed. I attempted to use [closeOthers]="true" but it doesn't seem to be effective. Here is my HTML code: <di ...

Switching cell icon when clicked - A step-by-step guide

I have a situation in ag-grid where I need to update the icon of a button in a cell when it is clicked to indicate progress and then revert back to its original state upon completion of the action. Below is the code snippet: my-custom.component.ts < ...

"React Bootstrap column showing gaps on the sides instead of filling the entire

In my current project with React and React Bootstrap 4.0, I am aiming to create a collapsible side-bar of 300px width on the left side of the screen, with the content displayed in the remaining space. The main parent component for the side-bar and content ...

Bring in jspm libraries to your project via typescript

While researching how to import jspm packages into typescript, I noticed that most examples assumed the use of SystemJS for loading and interpreting them in the browser. However, I prefer using tsc to compile commonjs modules and only import the js code, a ...