When attempting to apply generic Specification and Visitor patterns, the type does not meet the constraint and improperly extends the interface

I am currently working on incorporating a generic Specification pattern and a generic Visitor pattern simultaneously. Below are the base interfaces I have developed for this implementation.

export interface Specification<T, TVisitor extends SpecificationVisitor<TVisitor, T>> {
  accept(visitor: TVisitor): void;
  isSatisfiedBy(candidate: T): boolean;
  and(other: Specification<T, TVisitor>): Specification<T, TVisitor>;
  andNot(other: Specification<T, TVisitor>): Specification<T, TVisitor>;
  or(other: Specification<T, TVisitor>): Specification<T, TVisitor>;
  orNot(other: Specification<T, TVisitor>): Specification<T, TVisitor>;
  not(): Specification<T, TVisitor>;
}

export interface SpecificationVisitor<TVisitor extends SpecificationVisitor<TVisitor, T>, T> {
  visit(specification: AndSpecification<T, TVisitor>): void;
  visit(specification: AndNotSpecification<T, TVisitor>): void;
  visit(specification: OrSpecification<T, TVisitor>): void;
  visit(specification: OrNotSpecification<T, TVisitor>): void;
  visit(specification: NotSpecification<T, TVisitor>): void;
}

To facilitate this setup, I have implemented some base classes along with an abstract class for the fundamental boolean operators.

export abstract class CompositeSpecification<T, TVisitor extends SpecificationVisitor<TVisitor, T>> implements Specification<T, TVisitor> {
  abstract isSatisfiedBy(candidate: T): boolean;
  abstract accept(visitor: TVisitor): void;

  and(other: Specification<T, TVisitor>): Specification<T, TVisitor> {
    return new AndSpecification<T, TVisitor>(this, other);
  }
  andNot(other: Specification<T, TVisitor>): Specification<T, TVisitor> {
    return new AndNotSpecification<T, TVisitor>(this, other);
  }
  or(other: Specification<T, TVisitor>): Specification<T, TVisitor> {
    return new OrSpecification<T, TVisitor>(this, other);
  }
  orNot(other: Specification<T, TVisitor>): Specification<T, TVisitor> {
    return new OrNotSpecification<T, TVisitor>(this, other);
  }
  not(): Specification<T, TVisitor> {
    return new NotSpecification<T, TVisitor>(this);
  }
}

export class AndSpecification<T, TVisitor extends SpecificationVisitor<TVisitor, T>> extends CompositeSpecification<
  T,
  TVisitor
> {
  constructor(readonly left: Specification<T, TVisitor>, readonly right: Specification<T, TVisitor>) {
    super();
  }

  accept(visitor: TVisitor): void {
    visitor.visit(this);
  }

  isSatisfiedBy(candidate: T): boolean {
    return this.left.isSatisfiedBy(candidate) && this.right.isSatisfiedBy(candidate);
  }
}

export class AndNotSpecification<T, TVisitor extends SpecificationVisitor<TVisitor, T>> extends CompositeSpecification<T, TVisitor> {
  constructor(readonly left: Specification<T, TVisitor>, readonly right: Specification<T, TVisitor>) {
    super();
  }

  accept(visitor: TVisitor): void {
    visitor.visit(this);
  }

  isSatisfiedBy(candidate: T): boolean {
    return this.left.isSatisfiedBy(candidate) && !this.right.isSatisfiedBy(candidate);
  }
}

export class OrSpecification<T, TVisitor extends SpecificationVisitor<TVisitor, T>> extends CompositeSpecification<
  T,
  TVisitor
> {
  constructor(readonly left: Specification<T, TVisitor>, readonly right: Specification<T, TVisitor>) {
    super();
  }

  accept(visitor: TVisitor): void {
    visitor.visit(this);
  }

  isSatisfiedBy(candidate: T): boolean {
    return this.left.isSatisfiedBy(candidate) || this.right.isSatisfiedBy(candidate);
  }
}

export class OrNotSpecification<T, TVisitor extends SpecificationVisitor<TVisitor, T>> extends CompositeSpecification<
  T,
  TVisitor
> {
  constructor(readonly left: Specification<T, TVisitor>, readonly right: Specification<T, TVisitor>) {
    super();
  }

  accept(visitor: TVisitor): void {
    visitor.visit(this);
  }

  isSatisfiedBy(candidate: T): boolean {
    return this.left.isSatisfiedBy(candidate) || !this.right.isSatisfiedBy(candidate);
  }
}

export class NotSpecification<T, TVisitor extends SpecificationVisitor<TVisitor, T>> extends CompositeSpecification<
  T,
  TVisitor
> {
  constructor(readonly other: Specification<T, TVisitor>) {
    super();
  }

  accept(visitor: TVisitor): void {
    visitor.visit(this);
  }

  isSatisfiedBy(candidate: T): boolean {
    return !this.other.isSatisfiedBy(candidate);
  }
}

The code above compiles successfully without any errors. However, issues arise when attempting to create an interface that extends the base SpecificationVisitor interface and implementing a class that extends the abstract CompositeSpecification.

export interface NumberComparatorVisitor extends SpecificationVisitor<NumberComparatorVisitor, number> {
  visit(specification: GreaterThan): void;
}

export class GreaterThan extends CompositeSpecification<number, NumberComparatorVisitor> {
  constructor(readonly value: number) {
    super();
  }

  accept(visitor: NumberComparatorVisitor): void {
    visitor.visit(this);
  }

  isSatisfiedBy(candidate: number): boolean {
    return candidate > this.value;
  }
}

The following errors occur:

Type 'NumberComparatorVisitor' does not satisfy the constraint 'SpecificationVisitor<NumberComparatorVisitor, number>'.ts(2344)

Interface 'NumberComparatorVisitor' incorrectly extends interface 'SpecificationVisitor<NumberComparatorVisitor, number>'.
  Types of property 'visit' are incompatible.
    Type '(specification: GreaterThan) => void' is not assignable to type '{ (specification: AndSpecification<number, NumberComparatorVisitor>): void; (specification: AndNotSpecification<number, NumberComparatorVisitor>): void; (specification: OrSpecification<...>): void; (specification: OrNotSpecification<...>): void; (specification: NotSpecification<...>): void; }'.
      Types of parameters 'specification' and 'specification' are incompatible.
        Type 'AndSpecification<number, NumberComparatorVisitor>' is not assignable to type 'GreaterThan'.ts(2430)

Type 'NumberComparatorVisitor' does not satisfy the constraint 'SpecificationVisitor<NumberComparatorVisitor, number>'.
  Types of property 'visit' are incompatible.
    Type '(specification: GreaterThan) => void' is not assignable to type '{ (specification: AndSpecification<number, NumberComparatorVisitor>): void; (specification: AndNotSpecification<number, NumberComparatorVisitor>): void; (specification: OrSpecification<...>): void; (specification: OrNotSpecification<...>): void; (specification: NotSpecification<...>): void; }'.
      Types of parameters 'specification' and 'specification' are incompatible.
        Property 'value' is missing in type 'AndSpecification<number, NumberComparatorVisitor>' but required in type 'GreaterThan'.ts(2344)

I am unclear about the reason for these complaints. What modifications should be made to achieve the desired outcome?

Answer №1

My goodness, that's a hefty chunk of code right there. Let's condense it into a minimal reproducible example to highlight the issue:

interface Foo {
  ovld(x: string): number;
  ovld(x: number): boolean;
}

interface BadBar extends Foo {  // error!
  ovld(x: boolean): string;
}

Here, we have an interface named Foo with an overloaded method called ovld. This method has two different call signatures; one for strings and another for numbers. Now we try to create the BadBar interface that extends from it. The goal is to introduce a third overload to ovld which takes a boolean. However, this approach fails! But why?


The reason behind this is that you can't simply tack on additional overloads when extending interfaces. Interface extension does not equate to merging interfaces. By redefining ovld in the extended interface, you're instructing the compiler to entirely replace the type of ovld in Foo with the version in BadBar. Since the single call signature in BadBar doesn't match those in Foo, it results in an error, similar to this situation:

interface XXX {
  prop: string;
}
interface YYY extends XXX { // error!
  prop: boolean; 
}

In both scenarios, you're improperly extending an interface by modifying an existing property or method in an invalid manner. The only acceptable alterations are narrowing changes, like switching a string to "a" | "b".


If adding an overload by re-declaring ovld() in

BadBar</code isn't viable, how can we achieve it then? One method involves utilizing an <a href="https://www.typescriptlang.org/docs/handbook/2/indexed-access-types.html" rel="noreferrer">indexed access type</a> to extract the existing call signatures from the parent interface and then <a href="https://www.typescriptlang.org/docs/handbook/2/objects.html#intersection-types" rel="noreferrer">intersecting</a> it with the new call signature:</p>
<pre><code>interface GoodBar extends Foo {
  ovld: ((x: boolean) => string) & Foo["ovld"]
} 
/* (property) GoodBar.ovld: ((x: boolean) => string) & {
  (x: string): number;
  (x: number): boolean;
} */

The intersection X & Y serves as a valid way to narrow down X, resolving the compiler error. Furthermore, TypeScript treats function intersections as equivalent to overloads. Hence, the above construct provides the new (x: boolean) => string call signature as the initial overload, followed by the existing two from Foo:

declare const goodBar: GoodBar;
goodBar.ovld(false).toUpperCase();
goodBar.ovld("hello").toFixed();
goodBar.ovld(123) === true;

Success!


This implies that your NumberComparatorVisitor might require a tweak along these lines:

export interface NumberComparatorVisitor extends
  SpecificationVisitor<NumberComparatorVisitor, number> {
  visit: (
    ((specification: GreaterThan) => void) &
    SpecificationVisitor<NumberComparatorVisitor, number>["visit"]
  );
}

Implementing this adjustment should resolve the errors since now NumberComparatorVisitor genuinely extends its parent interface.

Link to Playground containing the revised code

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

Having difficulty transforming ".jsx" to ".tsx" in a Next.js application while working with "getStaticProps"

My application utilizes the Printifull API and performs well with .jsx using the code snippet below: import axios from "axios"; export default function ApiTest(props) { console.log(props); return( <></> ( } export async ...

Issue with Moment.js: inability to append hours and minutes to a designated time

I have a starting time and I need to add an ending time to it. For example: start=19:09 end=00:51 // 0 hours and 51 minutes I want to add the 51 minutes to the 19:09 to make it 20:00. I've attempted several different methods as shown below, but none ...

Is there a Typescript function error that occurs when attempting to rename a key, stating it "could potentially be instantiated with a different subtype"?

I am currently working on a Mongoify type that accepts a type T, removes the id key, and replaces it with an _id key. type Mongoify<T extends {id: string}> = Omit<T, "id"> & { _id: ObjectId }; function fromMongo<T extends ...

Preserve the timestamp of when the radio query was chosen

I'm interested in finding a way to save the user's selected answer for a radio button question and track the time they saved it. Is there a way to achieve this using HTML alone? Or would I need to utilize another coding language or package? Just ...

Creating arrays with non-specific elements

Take a look at the code snippet below public class MyClass<S> { private S[] elements; public MyClass(S[] elements) { this.elements = elements; } public void doSomething() { S[] newArray = (S[]) new Object[5]; ...

How can I obtain the rowIndex of an expanded row in Primeng?

<p-dataTable [value]="services" [paginator]="true" expandableRows="true" rowExpandMode="single"> ...</p-dataTable> There is a similar example below: <ng-template let-col let-period="rowData" let-ri="rowIndex" pTemplate="body">...</ ...

Using TypeScript in conjunction with Node.js

I'm currently trying to use typescript with nodejs, but I keep encountering errors. Can anyone help me troubleshoot? Here is the code snippet (assuming all necessary modules are imported): import routes from "./routes/routes"; let app = express(); ap ...

Tips on distinguishing the original Ctrl+C and Ctrl+V commands from the Javascript-added document level listeners

My Clipboard service includes a copy() and paste() method that is triggered by Ctrl+C and Ctrl+V commands. These methods are document-level keyboard listeners added to a component using HostListeners. However, I am facing an issue where the paste() method ...

Incorporating a d3 chart into an Angular 4 project

Currently, I am in the process of developing an Angular application using TypeScript. My aim is to incorporate a force directed network graph from Mike Boston built with d3, which can be viewed here. After successfully translating most of the code to Type ...

Tips for initializing and updating a string array using the useState hook in TypeScript:1. Begin by importing the useState hook from the

Currently, I am working on a React project that involves implementing a multi-select function for avatars. The goal is to allow users to select and deselect multiple avatars simultaneously. Here is what I have so far: export interface IStoreRecommendation ...

How can I best declare a reactive variable without a value in Vue 3 using TypeScript?

Is there a way to initialize a reactive variable without assigning it a value initially? After trying various methods, I found that using null as the initial value doesn't seem to work: const workspaceReact = reactive(null) // incorrect! Cannot pass n ...

How to retrieve the parent activated route in Angular 2

My route structure includes parent and child routes as shown below: { path: 'dashboard', children: [{ path: '', canActivate: [CanActivateAuthGuard], component: DashboardComponent }, { path: & ...

The error code TS2345 indicates that the argument type 'Event' cannot be assigned to a parameter type 'string'

Hello, I'm a newcomer to utilizing Angular and I'm struggling to identify where my mistake lies. Below is the TypeScript code in question: import { Component } from '@angular/core'; @Component({ selector: 'app-root' ...

A guide on including a class to a DOM element in Angular 6 without relying on Jquery

Currently, I have created a component template using Bootstrap that looks like this: <div class="container"> <div class="row my-4"> <div class="col-md-12 d-flex justify-content-center"> <h2> ...

Detecting Typescript linting issues in VSCode using Yarn version 3.2.3

Every time I try to set up a new react or next app using the latest yarn v3.2.3, my VS Code keeps showing linting errors as seen in the screenshot below. The main error it displays is ts(2307), which says Cannot find module 'next' or its correspo ...

What is the best way to differentiate between the content in the 'stories' and '.storybook' directories?

Overview Upon integrating Storybook.js with my existing Create-React-App project, I found that two new directories were created by default: .storybook src/stories This integration seemed to blur the lines between different aspects of my project, which g ...

Create an interface property that can accommodate various disparate types

In a React component, I am looking to use an external type (from react-hook-form) that can accommodate 3 specific types representing an object with form values. I initially thought about using a union type for this purpose, but encountered issues when pas ...

Decide on the chosen option within the select tag

Is there a way to pre-select an option in a combobox and have the ability to change the selection using TypeScript? I only have two options: "yes" or "no", and I would like to determine which one is selected by default. EDIT : This combobox is for allow ...

When deploying, an error is occurring where variables and objects are becoming undefined

I've hit a roadblock while deploying my project on Vercel due to an issue with prerendering. It seems like prerendering is causing my variables/objects to be undefined, which I will later receive from users. Attached below is the screenshot of the bui ...

Exploring Type Definitions in Vue Apollo version 4 and the Composition API

How can TypeScript be informed that Variables is the interface for the arguments of the myMutation function? interface Variables { uuid: string; value: string; } const { mutate: myMutation } = useMutation(myGqlMutation); I am look ...