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

Incorporate a generic type into a React Functional Component

I have developed the following component: import { FC } from "react"; export interface Option<T> { value: T; label: string; } interface TestComponentProps { name: string; options: Option<string>[]; value: string; onChang ...

What is the best way to transpile TypeScript within the Astro framework?

Recently, I decided to dive into exploring Astro for a couple of upcoming projects. In my research, I delved into the script and typescript sections of the documentation (), as well as (). However, I found the workflow somewhat counterintuitive and struggl ...

Getting started with installing Bootstrap for your Next.Js Typescript application

I have been trying to set up Bootstrap for a Next.js Typescript app, but I'm having trouble figuring out the proper installation process. This is my first time using Bootstrap with Typescript and I could use some guidance. I've come across these ...

Creating an interceptor to customize default repository methods in loopback4

Whenever I attempt to access the default repository code, I need to manipulate certain values before triggering the default crud function in the repository. How can I accomplish this? For example: ... @repository.getter('PersonRepository') priva ...

Transforming Excel data into JSON format using ReactJS

Currently, I am in the process of converting an imported Excel file to JSON within ReactJS. While attempting to achieve this task, I have encountered some challenges using the npm XLSX package to convert the Excel data into the required JSON format. Any as ...

Error in hook order occurs when rendering various components

A discrepancy was encountered in React when attempting to render different components Warning: React has detected a change in the order of Hooks called by GenericDialog. This can result in bugs and errors if left unresolved. Previous render Next ren ...

The challenges of dealing with duplicate identifiers caused by nesting npm packages in TypeScript

I am facing an issue with my project structure where I have a node_modules folder at the root level and another one within a subfolder named functions. The directory layout looks like this, ├── functions │   ├── index.js │   ├── ...

Implementing validation logic on button click using TypeScript

I'm struggling to get my validation to work for empty fields using the method below. Can anyone provide some guidance or suggestions? Thanks. Here is my template: <form [formGroup]="form" (ngSubmit)="onSubmit(form.value)" class="nobottommargin ad ...

Having trouble implementing types with typescript in Vue-toastification for vuejs 3

Having trouble incorporating vue-toast-notification into my vue 3 project. The issue seems to be with vue Augmenting Types. I've tried other solutions without success, encountering the following error: TS2339: Property '$toast' does not exis ...

What is the best way to trim a string property of an object within an array?

I am seeking a solution to access the "description" property of objects within an array and utilize a string cutting method, such as slice, in order to return an array of modified objects. I have attempted using for loops but have been unsuccessful. Here ...

Guide on deactivating the div in angular using ngClass based on a boolean value

displayData = [ { status: 'CLOSED', ack: false }, { status: 'ESCALATED', ack: false }, { status: 'ACK', ack: false }, { status: 'ACK', ack: true }, { status: 'NEW', ack ...

React's memo and/or useCallback functions are not functioning as anticipated

Within my Home Component, there is a state called records, which I utilize to execute a records.map() and display individual RecordItem components within a table. function Home() { const [records, setRecords] = useState<Array<RecordType>>(l ...

How can I utilize Angular services to transfer a count value to the Component?

I've been working on creating a coin counter for my application by developing a service specifically for counting coins. However, when I tried to use this service in one of my components where the count function is triggered, I encountered some diffic ...

Navigate to a different page using the A-g Grid router when a row is

Having trouble making the router link interact with Ag grid. When I use the router link ="url", it always takes me to a different page every time I click on anything in the grid. What I really want is for clicking on an individual row to redirect me to an ...

If every single item in an array satisfies a specific condition

I am working with a structure that looks like this: { documentGroup: { Id: 000 Children: [ { Id: 000 Status: 1 }, { Id: 000 Status: 2 ...

Disable all typings within a specified namespace for a specific file

I need to disable typechecking for a specific namespace called MyNamespace in a Typescript file. Is there a way to achieve this without affecting other files? ...

What is the role of the "prepare" function in AWS CDK constructs?

TL;DR: What is the role and purpose of the prepare(): void method in AWS CDK's Construct class? When and how should it be utilized or avoided? The information provided about prepare() states: prepare() function is called after child constructs have ...

Angular Project: Exploring Classes and Interfaces with and without the Export Keyword

Currently, I am delving into the world of Angular. I have taken up a video course and also referred to a PDF book, but I find myself perplexed when it comes to understanding the usage of the "export" keyword... The PDF course focuses on Angular 5 and util ...

Tips for creating a sequelize transaction in TypeScript

I am currently working with sequelize, node js, and TypeScript. I am looking to convert the following command into TypeScript. return sequelize.transaction().then(function (t) { return User.create({ firstName: 'Homer', lastName: ' ...

The number entered will be incorporated into the API URL key value by passing the variable from page.html to services.ts

Recently diving into the world of Ionic, Angular, and Typescript, I've had a burning question. Can the number inputted be added to the API URL as one of the key values? I came across this helpful guide, specifically focusing on key event filtering (wi ...