Creating a contravariant function member in TypeScript?

I am facing a challenge with a class that contains a member which is a function taking an instance of the same class:

class Super {
    public member: (x: Super) => void = function(){}
    use() {const f = this.member; f(this)}
}

However, I need the member to be contravariant so that subclass instances can accept member values that are functions taking that specific subclass, like this:

class Sub extends Super {
    method() {}
}

const sub = new Sub();
sub.member = function(x: Sub) {x.method()};

Yet, TypeScript rightfully raises an error:

Type '(x: Sub) => void' is not assignable to type '(x: Super) => void'.
  Types of parameters 'x' and 'x' are incompatible.
    Property 'method' is missing in type 'Super' but required in type 'Sub'.

Is there a way to declare member such that it allows a covariant parameter type in subclasses?


My attempts to solve this have included:

  • I know that using method syntax to declare member (member(s: Super) {/* ... */}) would make it bivariant, but this doesn't work when member may be a collection of functions (e.g., my actual code involves a dictionary of such functions:

    {[name: string]: (/*...*/, s: Super) => /*...*/}
    ).

  • I tried redeclaring member in Sub with a more restrictive signature:

    class Sub extends Super {
        public member: (x: Sub) => void = function(x){x.method()};
        method() {}
    }
    

    But TypeScript still rejects it:

    Property 'member' in type 'Sub' is not assignable to the same property in base type 
    'Super'.
      Type '(x: Sub) => void' is not assignable to type '(x: Super) => void'.
        Types of parameters 'x' and 'x' are incompatible.
          Property 'method' is missing in type 'Super' but required in type 'Sub'.
    
  • I am aware that TypeScript now supports the in and out modifiers on templates for co/contravariance, but I am unsure if they apply here and how to modify the declaration of Super accordingly.

  • I prefer not to disable strictFunctionTypes as it is generally helpful and I don't want users of this library to adjust their settings just to assign to .member on subclass instances.

  • If all else fails, I can resort to casting the assigned values as (x: Super) => void, but this removes safeguards against incorrect assignments to different subclasses, resulting in runtime failures like this:

    class Sub1 extends Super {
        method1() {}
    }
    class Sub2 extends Super {
        method2() {}
    }
    
    const sub1 = new Sub1();
    sub1.member = function(x: Sub2) {x.method2()} as (x: Super) => void;
    

    This code is accepted by TypeScript but fails at runtime.

  • Browsing through related questions, I came across a similar query involving interfaces rather than subclasses, but the answers provided are not formal yet. Furthermore, the linked snippets seem to rely on explicitly listing all subtypes, which is impractical for situations where there are numerous subclass variations.

Answer №1

If you're looking for a way to utilize the polymorphic this type, which essentially functions as an implicit generic type parameter always constrained to the "current" class, then it's like this: within the body of the Super class, the this type represents "some subtype of Super", while inside the Sub class body it represents "some subtype of Sub". When it comes to instances of Super, the this type will be associated with Super, and for Sub instances, it'll be linked with Sub.

In essence, within the class body, this behaves akin to a generic parameter; outside of the class body, it functions as though that parameter has been explicitly assigned with a type argument corresponding to the present object type.


This realization leads to the desired outcome with the sample code provided:

class Super {
    public member: (x: this) => void = function () { } 
    use() { const f = this.member; f(this) }
}

class Sub extends Super {
    method() { }
}

const sub = new Sub();
sub.member = function (x: Sub) { x.method() }; // works fine

Looks solid.


Keep in mind that a similar behavior can be achieved by using generics more directly (employing a recursive, F-bounded constraint reminiscent of Java):

class Super<T extends Super<T>> { 
    public member: (x: T) => void = function () { }
    use(this: T) { const f = this.member; f(this) } 
}

class Sub extends Super<Sub> {
    method() { }
}

const sub = new Sub();
sub.member = function (x: Sub) { x.method() };

Although somewhat less elegant, this approach offers additional flexibility if standalone this types alone don't align with your requirements.

Playground link to 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

Managing Events in Angular 2 and Creating Custom Event Handlers

Currently, I am in the process of developing a component library in Angular 2 for our app teams to utilize. One of the components I recently created is a modal, but I am encountering some accessibility challenges. Specifically, I want the modal to close wh ...

AngularFire2 Firestore Custom Query: retrieve documents based on current date and time.startTime

Welcome to the world of AngularFire2 and Firestore! My objective is clear: Query data from Firestore where startTime matches currentDateRange. I am facing some challenges with creating a dynamic query in Firestore. After going through the official docume ...

What is the process for including a new attribute to the Component in the _app.tsx file when using Next.js along with Typescript?

Currently, I am in the process of developing some secure pages using Next.js, NextAuth.js, and Typescript. While referencing code from my _app.tsx, which was sourced from the official NextAuth.js website, I encountered an issue where a property named auth ...

Struggling with the @typescript-eslint/no-var-requires error when trying to include @axe-core/react? Here's a step-by

I have integrated axe-core/react into my project by: npm install --save-dev @axe-core/react Now, to make it work, I included the following code snippet in my index.tsx file: if (process.env.NODE_ENV !== 'production') { const axe = require(&a ...

Check out the attributes of a class

I have a TypeScript class that is defined like this: export class MyModel { ID: number; TYPE_ID: number; RECOMMENDED_HOURS: number; UNASSIGNED_HOURS: number; } In a different .ts file, I instantiate this class within a component: export class My ...

Using Array.push within a promise chain can produce unexpected results

I have developed a method that is supposed to retrieve a list of devices connected to the network that the client has access to. export const connectedDevicesCore = (vpnId: string, vpnAuthToken: string) => Service.listVPNConnectionsCore ...

Setting the desired configuration for launching an Aurelia application

After creating a new Aurelia Typescript application using the au new command from the Aurelia CLI, I noticed that there is a config directory at the root of the project. Inside this directory, there are two files: environment.json and environment.productio ...

Angular 2's ng-required directive is used to specify that

I have created a model-driven form in Angular 2, and I need one of the input fields to only show up if a specific checkbox is unchecked. I was able to achieve this using *ngIf directive. Now, my question is how can I make that input field required only whe ...

Maximize the benefits of using React with Typescript by utilizing assignable type for RefObject

I am currently working with React, Typescript, and Material UI. My goal is to pass a ref as a prop. Within WidgetDialog, I have the following: export interface WidgetDialogProps { .... ref?: React.RefObject<HTMLDivElement>; } .... <div d ...

My Nextjs project is encountering deployment issues with both Netlify and Heroku

Whenever I attempt to deploy my application on Heroku or Netlify, I encounter an ERROR related to an incorrect import path. It's perplexing because the import is accurate and functions properly locally. Log ./pages/_app.tsx:7:27 6:31:19 PM: Type err ...

What is the best way to invoke a method in a child component from its parent, before the child component has been rendered?

Within my application, I have a parent component and a child component responsible for adding and updating tiles using a pop-up component. The "Add" button is located in the parent component, while the update functionality is in the child component. When ...

TSLint Alert: Excessive white space detected before 'from' keyword (import-spacing)

I'm currently using WebStorm and working to maintain a specific code style: However, I encounter an issue where TSLint highlights my spacing and provides the following hint: "Too many spaces before 'from' (import-spacing)". My main ques ...

Unable to locate the module styled-components/native in React Native

When adding types in tsconfig.json to remove TypeScript complaints and enable navigation to a package, the code looks like this: import styled, {ThemeProvider} from 'styled-components/native'; The package needed is: @types/styled-components-re ...

Error: Model function not defined as a constructor in TypeScript, mongoose, and express

Can anyone help me with this error message "TypeError: PartyModel is not a constructor"? I've tried some solutions, but now I'm getting another error as well. After using const { ... } = require("./model/..."), I'm seeing "TypeError: C ...

Unleashing the potential of an endless animation by incorporating pauses between each iteration

I am trying to create an infinite animation using animate css but I want to add a delay between each iteration. After exploring various options, I first attempted to achieve this using plain JavaScript. Here is the HTML snippet: <div id="item" class= ...

A guide to adding a delay in the RxJS retry function

I am currently implementing the retry function with a delay in my code. My expectation is that the function will be called after a 1000ms delay, but for some reason it's not happening. Can anyone help me identify the error here? When I check the conso ...

Why does the ReactJS MaterialUI Modal fail to update properly?

Recently, I encountered a curious issue with my Modal component: https://i.stack.imgur.com/dkj4Q.png When I open the dropdown and select a field, it updates the state of the Object but fails to render it in the UI. Strangely, if I perform the same action ...

`Why TypeScript in React may throw an error when using a setter`

As I work on building a small todo app in TypeScript with React, I encountered an issue when attempting to add a new todo item to my list. It seems like the problem may lie in how I am using the setter function and that I need to incorporate Dispatch and s ...

Simple and quickest method for incorporating jQuery into Angular 2/4

Effective ways to combine jQuery and Angular? Simple steps for integrating jQuery in Angular2 TypeScript apps? Not sure if this approach is secure, but it can definitely be beneficial. Quite intriguing. ...

How can you prevent the keys from being read-only when mapping onto a type?

Here's a common query: How can I change keys from readonly to writable when using a type that is Readonly? For example: type Foo = Readonly<{ foo: number bar: number }> type Bar = /* What's the method to duplicate the Foo type, but w ...