Need at least one of two methods, or both, in an abstract class

Consider the following scenario:

export abstract class AbstractButton {
    // Must always provide this method
    abstract someRequiredMethod(): void;

    // The successor must implement one of these (or both)
    abstract setInnerText?(): void;
    abstract setInnerHTML?(): void;
}

It is necessary for a successor to implement either setInnerText() or setInnerHTML(), or even both.

Is there a way to achieve this utilizing the most powerful type system created by humanity?

Answer №1

If you want to distinguish between different implementations, you can create a type alias that utilizes a generic type parameter:

For a practical example and more detailed exploration, head over to the TS Playground

type AbstractButton<T extends 'both' |  'html' | 'text' = 'both'> =
  & { someRequiredMethod(): void }
  & (
    T extends 'html' ? { setInnerHTML(): void }
    : T extends 'text' ? { setInnerText(): void }
    :  {
      setInnerText(): void;
      setInnerHTML(): void;
    }
  );

To ensure adherence to the specified implementation, utilize the implements clause. Here are a few illustrative examples from the TypeScript playground link above - for a comprehensive overview, be sure to check out the provided resources:

class B5 implements AbstractButton<'html'> { /* Error
      ~~
Class 'B5' incorrectly implements interface 'AbstractButton<"html">'.
  Type 'B5' is not assignable to type '{ setInnerHTML(): void; }'.
    Property 'setInnerHTML' is missing in type 'B5' but required in type '{ setInnerHTML(): void; }'.(2420) */
  someRequiredMethod(): void {}
}

class B6 implements AbstractButton<'html'> { // ok
  someRequiredMethod(): void {}
  setInnerHTML(): void {}
}

class B7 implements AbstractButton<'text'> { /* Error
      ~~
Class 'B7' incorrectly implements interface 'AbstractButton<"text">'.
  Type 'B7' is not assignable to type '{ setInnerText(): void; }'.
    Property 'setInnerText' is missing in type 'B7' but required in type '{ setInnerText(): void; }'.(2420) */
  someRequiredMethod(): void {}
}

class B10 implements AbstractButton { /* Error
      ~~
Class 'B10' incorrectly implements interface 'AbstractButton<"both">'.
  Type 'B10' is not assignable to type '{ setInnerText(): void; setInnerHTML(): void; }'.
    Property 'setInnerText' is missing in type 'B10' but required in type '{ setInnerText(): void; setInnerHTML(): void; }'.(2420)
input.tsx(7, 7): 'setInnerText' is declared here. */
  someRequiredMethod(): void {}
  setInnerHTML(): void {}
}

class B12 implements AbstractButton { // ok
  someRequiredMethod(): void {}
  setInnerHTML(): void {}
  setInnerText(): void {}
}

Answer №2

Discovering a method to trigger a compile-time error is possible, but the approach may not be ideal due to its complexity. It might be better to opt for requiring a mixin or accept only a runtime test (triggered from the AbstractButton constructor), which would flag the issue very early on in the subclass development process.

I will explain how I achieved the compile-time error and why defining optional abstract methods in a certain way won't work as expected. The workaround involves setting up the base class without these optional methods and then ensuring that subclasses adhere to at least one of them.

abstract class AbstractButton {
    // This method must always be implemented
    abstract someRequiredMethod(): void;
}

Implementing this requirement solely using `class ___ extends AbstractButton` proved challenging. However, a strategy involving an empty function can generate a type error during compilation if the subclass doesn't implement one of the required methods.

... (remaining text remains unchanged)

Finally: Why do we check [Ctor] against [{ prototype: { ___ } }] instead of directly testing Ctor against { prototype: ____ }? To prevent the generic type argument from being distributed, which would disrupt the tests. Wrapping it in a tuple disables distribution.

In conclusion, although the elaborate method with mapped types serves its purpose, a simpler runtime check within the AbstractButton constructor could serve as a more straightforward alternative, catching issues early in subclass development.

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

Unraveling the intricacies of the relationship between `extends` and function types in TypeScript

Example 1 seems clear to me type A = (1 | 2 | 3) extends (infer I) ? [I] : never; // A = [1 | 2 | 3] In Example 2, the type variables are being intersected but I'm unsure why type A = ( ((_: 1) => void) | ((_: 2) => void) | ((_: 3) => ...

How can methods from another class be accessed in a TypeScript constructor?

I need to access a method from UserModel within the constructor of my UserLogic class. How can I achieve this? import { UserModel, ItUser } from '../../models/user.model'; export class UserLogic { public user: ItUser; constructor() { ...

Using a Component as a Property in Angular

There is a small gridComponent: @Component({ selector: 'moving-grid', templateUrl: './grid.component.html', styleUrls: ['./grid.component.css'] }) export class GridComponent { @Input('widgets') ext ...

Is it possible to pass additional arguments to setState other than prevState and props?

I'm currently facing an issue with my component that involves calling a function called addOption, which is defined on its parent component. This function takes a parameter 'option' from a form field and concatenates it with an array of opti ...

Empty initial value of a number type input element in React using TSX

In the process of developing a react POS app using Typescript, I encountered an issue with calculating change when entering the amount of money received from a buyer. The problem arises when the first value passed to the change calculation logic is empty, ...

How can a child component trigger an event in its parent component?

Currently, I have tabs implemented with a form and a button in tab1. In the parent component, there is an event that deactivates the current tab and activates the next one. Can anyone advise on how to call this event from the child component? gotosecond ...

Passing by value doesn't result in the tree being deleted

Similar Question: Java, pass-by-value, reference variables I am finding it a bit perplexing to understand how JAVA's pass by value mechanism works with objects. When an object is passed as a parameter to a method, I know that its address is passe ...

Caution in NEXTJS: Make sure the server HTML includes a corresponding <div> within a <div> tag

Struggling with a warning while rendering pages in my Next.js and MUI project. Here's the code, any insights on how to resolve this would be greatly appreciated! import "../styles/globals.scss"; import { AppProps } from "next/app"; ...

Is there a way to identify legitimate contacts and phone numbers within an Android application using Javascript or Typescript?

I am developing an Android app where I need to show a list of contacts and specify if they are part of the app's network. However, my goal is to only display valid contacts while excluding unwanted ones such as toll-free numbers or data balance check ...

Definition of generic with recursive immutability

I created a type definition to ensure immutability of types and their properties all the way down. However, when I attempt to use this with a generic type, the compiler claims that the types do not overlap, preventing me from casting an object as immutable ...

The C# private property is inaccessible during a TypeScript callback as it is not contained within the

I'm encountering an issue with TypeScript where the callback function is only returning _proto in the response's .data property when I set private properties in C# and instantiate an object filled with constructed properties. Strangely, if the pr ...

SolidJS directives utilizing use:___ result in TypeScript errors when used in JSX

As I work on converting the forms example from JS to TS, I came across a typescript error related to directives in HTML: It appears that validate and formSubmit are being recognized as unused variables by typescript, resulting in the following jsx error: ...

Tips for managing server data and dynamically binding values in Ionic 3

I am struggling with handling data retrieved from the server. I have a provider that fetches the data through HTTP, and I want to ensure the data is loaded before the page loads. However, there is a delay in reflecting the data on the page. Can someone pro ...

"Experiencing issues retrieving user-modified values in NativeScript's TimePicker component

I'm having trouble retrieving the user-modified value from a TimePicker. Instead of getting the modified value, I keep receiving the original value that was initially set in the control. Below is my component template: <TabView [(ngModel)]="tabSe ...

Is it recommended for TypeScript to automatically resolve the index.ts file as the default module file?

Struggling with getting the module resolution to work in TypeScript. Consider the following file structure: /modulename/index.ts Should it be resolved like this? import * as modulename from "modulename" I can't seem to make it work. However, imp ...

What impact does nesting components have on performance and rendering capabilities?

Although this question may appear simple on the surface, it delves into a deeper understanding of the fundamentals of react. This scenario arose during a project discussion with some coworkers: Let's consider a straightforward situation (as illustrat ...

Tips for correctly specifying the theme as a prop in the styled() function of Material UI using TypeScript

Currently, I am utilizing Material UI along with its styled function to customize components like so: const MyThemeComponent = styled("div")(({ theme }) => ` color: ${theme.palette.primary.contrastText}; background-color: ${theme.palette.primary.mai ...

Converting Abstract Type Members from Scala to TypeScript: A Handy Snippet

I have a brief example of a value level and type level list in Scala sealed trait RowSet { type Append[That <: RowSet] <: RowSet def with[That <: RowSet](that: That): Append[That] } object RowSet { case object Empty extends RowSet { t ...

How to Retrieve Observable<Record<string, boolean>> Value with the Help of AsyncPipe

My service retrieves permissions for the currently logged-in user as a record. The necessary observable is declared in my TypeScript file as a member variable: permissionsRecord$: Observable<Record<string, boolean>>; When I call the service, ...

Conceal the header on signup and login pages using Material UI

I am working on a project with three pages: SignUp, Login, and Lists, along with a header component. My goal is to hide the header on the SignUp and Login pages, and only show it on the List page. Any suggestions on how I can achieve this? Here is my cod ...