How can you refer to the implementing class from an interface in TypeScript?

Delving into the Typescript type system, I am currently working on implementing the Fantasy Land Spec and encountered a hurdle while trying to implement the specification for Semigroup.

The spec dictates that a Semigroup must follow the type definition outlined below:

concat :: Semigroup a => a ~> a -> a

Interpreting this, I understand that a type a, which implements Semigroup, should have a concat method that takes a parameter of type a and returns a value of type a.

The way I attempted to express this type definition in TypeScript is as follows:

interface Semigroup {
    concat(other: this): this;
}

However, when trying to implement this interface in a class, like so:

class Sum implements Semigroup {
    constructor(readonly num: number) {}

    concat(other: Sum): Sum {
        return new Sum(this.num + other.num);
    }
}

I encountered a compiler error stating:

Property 'concat' in type 'Sum' is not assignable to the same property in base type 'Semigroup'.
  Type '(other: Sum) => Sum' is not assignable to type '(other: this) => this'.
    Type 'Sum' is not assignable to type 'this'.
      'Sum' is assignable to the constraint of type 'this', but 'this' could be instantiated with a different subtype of constraint 'Sum'.(2416)

Upon reading this answer on Stack Overflow, I gained insight into the issue.

It seems the compiler is essentially informing me that while the interface specifies taking a parameter of the concrete type this (Sum in this case), it could also accept a class extending Sum.

Yet, I am uncertain about how to address this issue. Specifically, I am unsure about how to articulate the type definition for Semigroup in TypeScript. How can the implementing class be referenced from an interface?

For a demonstration, please visit the TS Playground.

Update

Reflecting on @Guerric P's solution, I believe it offers a partial remedy. Guerric suggested utilizing a generic within the interface. While this solution facilitates the implementation of the Semigroup specification, as evidenced here, the interface itself does not strictly enforce it.

Moreover, the fantasy land specification elaborates on the criteria as follows:

s.concat(b)

/** 
 * `b` must be a value of the same `Semigroup`
 *
 * If `b` is not the same semigroup, behaviour of `concat` is 
 * unspecified.
 *
 * `concat` must return a value of the same `Semigroup`.
 */

Considering making b a generic, my proposal moves towards restricting the type to Semigroup. This adjustment enforces the constraint that b must be of type Semigroup, as showcased here:

interface Semigroup {
    concat(other: Semigroup): Semigroup;
}

However, the specification still falls short of mandating that b must belong to the SAME Semigroup. I am still seeking a solution within the TypeScript type system to achieve this level of specificity.

Answer №1

I won't question your understanding of that fantastical spec, which admittedly I don't fully grasp, so I'll take your interpretation as correct 😉.

The issue here is that your class could be extended, causing this to reference that extended class. TypeScript does not have a concept of a final class or a similar construct.

Let's say you have an ExtendedSum class that extends Sum. Your equals implementation will still function because (other: Sum) => boolean can be assigned to

(other: ExtendedSum) => boolean
. Essentially, a function that accepts a Sum as an argument can also accept an ExtendedSum due to structural typing.

However, your concat implementation will not work since (other: Sum) => Sum cannot be assigned to

(other: ExtendedSum) => ExtendedSum
. This is because a function that returns a Sum cannot be assigned to a function that returns an ExtendedSum, as a Sum is not always an ExtendedSum.

This issue can be resolved by using a generic typed interface:

interface Semigroup<T> {
    concat(other: T): T;
}

class Sum implements Setoid, Semigroup<Sum> {
    constructor(readonly num: number) {}

    equals(other: Sum): boolean {
        return this.num === other.num;
    }

    concat(other: Sum): Sum {
        return new Sum(this.num + other.num);
    }
}

TypeScript playground

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

What enables typescript to be eligible for transpiling is its equivalent level of abstraction to javascript?

Transpilation is the act of converting code from one language to another with a similar level of abstraction. Can you point out some distinctive characteristics that indicate TypeScript transpires into JavaScript? ...

Error: There was a problem trying to import the `.d.ts` file

Software Stack Vue version 3.2.19 @vue/test-utils version 2.0.0-rc.15 Typescript version 4.1.6 Issue Description Encountering an error when running the command vue-cli-service test:unit --no-cache. Please refer to the TypeError screenshot (link to Con ...

Discovering the best approach to utilizing Font Awesome 6 with React

Required Packages "@fortawesome/fontawesome-svg-core": "^6.1.1", "@fortawesome/free-solid-svg-icons": "^6.1.1", "@fortawesome/react-fontawesome": "^0.1.18", "next": " ...

Different varieties of TypeScript's keyof when working with objects

I am grappling with the concept of TypeScript's types when incorporating the keyof type operator on objects. Check out this example: type TypeA = { [k: number]: boolean }; type AKey = keyof TypeA; // ^? type AKey = number type TypeB = { [k: string] ...

Alter the command from 'require' to an 'import'

Utilizing https://www.npmjs.com/package/json-bigint with native BigInt functionality has been a challenge. In the CommonJS environment, the following code is typically used: var JSONbigNative = require('json-bigint')({ useNativeBigInt: true }); ...

Troubleshooting issue of incorporating hintText in a TextField within a create-react-app-with-typescript

After recently downloading, installing, and running the create-react-app-with-typescript, I have been exploring different components. My latest experiment involved adding a TextField with hintText using the following code: import TextField from 'mate ...

Is there a way to turn off eslint's no-promise-executor-return rule?

Can the eslint rule 'no-promise-executor-return' be disabled? my .eslintrc file { "env": { "es6": true, "node": true }, "extends": [ "airbnb-base" ], "globals": { "de ...

Encountering HttpErrorResponse when sending a Delete request in Angular

I need help troubleshooting an issue with sending a Delete request from my movie.component.ts file to delete a review. Unfortunately, I keep receiving the dreaded HttpErrorResponse error. Can anyone pinpoint where I may be making a mistake? Take a look at ...

How can I export custom MUI theme definitions after overriding them?

I have successfully created a MUI theme for my project, and everything is functioning as expected. Now, I want to extract this theme as a separate library (e.g. @myproject/theme) so that I can easily share and redeploy it across multiple applications. This ...

Spartacus has the capability to extend or override the PageType enum within the cms.model framework

I am facing a dilemma similar to the Spartacus situation. In brief, I am required to modify the PageType enum in cms.model by either overriding or extending it. The current enum consists of four values (content, product, category, catalog) and I must incl ...

Encountering the "Not all code paths return a value" TypeScript error when attempting to manipulate a response before returning it, however, returning the response directly works without any issues

Encountering an issue where manipulating/process response and return triggers an error in TypeScript with the message "Not all code paths return a value.". Data is fetched from a backend API using RxJS lastValueFrom operator, along with lodash functions as ...

What is the best way to change an existing boolean value in Prisma using MongoDB?

Exploring prisma with mongoDb for the first time and faced a challenge. I need to update a boolean value in a collection but struggling to find the right query to switch it between true and false... :( const updateUser = await prisma.user.update({ where: ...

Error in TypeScript: The type 'Color' cannot be assigned to the type '"default" | "primary" | "secondary"'

Currently, I am utilizing MUI along with typescript and encountering the following issue. It seems like I may be overlooking a fundamental concept here but unfortunately cannot pinpoint it. Error: Type 'Color' is not assignable to type '&quo ...

Implementing delayed loading of Angular modules without relying on the route prefix

In my application, I am using lazy loading to load a module called lazy. The module is lazily loaded like this: { path:'lazy', loadChildren: './lazy/lazy.module#LazyModule' } Within the lazy module, there are several routes def ...

I'm trying to determine in Stencil JS if a button has been clicked in a separate component within a different class. Can anyone assist

I've created a component named button.tsx, which contains a function that performs specific tasks when the button is clicked. The function this.saveSearch triggers the saveSearch() function. button.tsx {((this.test1) || this.selectedExistingId) && ...

Unable to locate 'reflect-metadata' module within Docker container on Production Server

I encountered an error: module.js:550 throw err; ^ Error: Cannot find module 'reflect-metadata' at Function.Module._resolveFilename (module.js:548:15) at Function.Module._load (module.js:475:25) at Module.require ( ...

Incorporate Material Design Icons into your NPM Electron React application for sleek visuals

I am currently exploring how to incorporate Material Design Icons into an NPM Electron project with Webpack 4. The Google Github page suggests that the icons can be easily installed using npm install material-design-icons. However, based on this discussion ...

Can TypeScript be used to dynamically render elements with props?

After extensive research on SO and the wider web, I'm struggling to find a solution. I have devised two components, Link and Button. In short, these act as wrappers for <a> and <button> elements with additional features like chevrons on t ...

Adal TypeScript Document

Recently, I've been experimenting with the TypeScript version of adal.js. As part of my setup process, I'm referring to this link to install adal.ts. However, after executing the command: npm install adal-typescript --save a new "node_modules" ...

After updating to Angular 7, an error was encountered: "TypeError: Unable to execute map function on ctorParameters"

After updating my Angular project to version 7, I encountered a new issue. When running "ng serve --open" from the CLI, I received the following error message: Uncaught TypeError: ctorParameters.map is not a function at ReflectionCapabilities._own ...