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

Is it possible to create a distinctive property value within an ngFor loop that operates autonomously across two child components?

I am working on a functionality where, upon clicking on a specific car brand, the name of the person and their favorite car will be displayed at the bottom. However, I'm facing an issue where the car brand is being repeated among all items in the ngFo ...

The class-transformer malfunctioning: The transformation function is not being executed

I am facing an issue with implementing class-transformer in my codebase, despite using type-graphql and @typegoose/typegoose libraries. Below is the snippet of my code: Custom Decorator import { Transform } from 'class-transformer'; export func ...

Can I assign a value from the tagModel to ngx-chips in an Angular project?

HTML code: <tag-input class="martop20 tag-adder width100 heightauto" [onAdding]="onAdding" (onAdd)="addInternalDomain($event)" type="text" Ts code: addInternalDomain(tagTex ...

Why is it necessary to merge an interface with a function when using the new keyword?

Assuming that the setting noImplicityAny is enabled. Consider the following: function Bar() { this.f = 100; } The following code snippet will not function as expected: let x = new Bar(); An error message will be displayed: 'new' expre ...

Navigating Angular: Uncover the Secrets of Unwrapping Json Data

I need some help with parsing Json data in Angular TypeScript. The data is structured as follows: https://i.sstatic.net/H7s8q.png When calling a REST API, I want to convert a Java class object into an array of model classes. How can this be achieved? "g ...

Issue with manipulating element styles using jQuery in Angular2

My method of assigning IDs to elements dynamically using *ngFor looks like this: <div *ngFor="let section of questionsBySubCat" class="col-md-12"> <div class="subcat-container"> <h4 class="sub-cat">{{ section?.subcategory }}& ...

Using variables within the useEffect hook in ReactJS

I am currently working on a project using Reactjs with Nextjs. I am facing an issue where I need to retrieve the value of "Editor" and alert it inside the handleSubmit function. Can anyone help me with how to achieve this? Here is my code snippet, any as ...

Implementing individual NGRX Selectors for each child component to enable independent firing

My component serves as a widget on a dashboard, and I am using *ngFor to render multiple widgets based on the dashboard's data. Each WidgetComponent receives some of its data via @Input() from the parent. parent <app-widget *ngFor="let widget ...

Leveraging Java and TypeScript code for specific functionalities within an Ionic 2 Android application

When it comes to creating hybrid apps that support Windows, iOS, and Android simultaneously using web technologies such as Html, CSS, and Js, Ionic is the go-to choice. However, there may be certain features not supported by the Ionic 2 framework. Is it ...

Combine a pair of select statements to utilize the RxJS store within an Angular Guard

When working on an Angular Guard, I encountered a challenge where I needed to select two fields from the ngrx store. Here is the code snippet for reference: @Injectable() export class RoleGuard implements CanActivate { constructor( public router: A ...

Does the React memo function modify the component's prop type?

I've come across a strange issue where defining two components causes compilation errors when written separately but not when written in the same file. test3.tsx import React from "react"; type ValueType = number[] | string[] | number | st ...

What is the best way to call a method within a TypeScript class using its name as a string while already inside the class itself?

Currently, I am developing a class that automates the creation of routes for Express and invokes a function in a controller. However, upon trying to execute this[methodName](req, res), I come across an error message stating: 'Element implicitly has an ...

Combining platform-express and platform-fastify for optimal performance

I am currently working on a NestJS application and my goal is to upload files using the package @types/multer. However, I encountered an issue while following the guidelines from the official documentation: Upon starting my application development, I dec ...

Improving the management of user input in Lit components

I am seeking a more efficient method to manage all inputs within my lit component while minimizing the code usage. I am also employing Typescript in this process, utilizing @change=${this.handleInput} for all input fields. Below is an example of how the ha ...

Issue with rendering images retrieved from JSON data

Struggling with displaying images in my Ionic and Angular pokedex app. The JSON file data service pulls the image paths, but only displays the file path instead of the actual image. Any ideas on what might be causing this issue? Sample snippet from the JS ...

How is it possible to utilize type assertions with literals like `false`?

When working in TypeScript, I came across an interesting observation when compiling the following code: const x = true as false; Surprisingly, this direct assertion is valid, creating a constant x with the value true and type false. This differs from the ...

Transforming this JavaScript function using Template Strings into TypeScript

Is there anyone out there who can assist with the following query? I have a functional .js file that I need to integrate into a TypeScript program. import React from "react"; import styled, { css } from "styled-components"; const style = ({ theme, ...res ...

incomplete constructor for a generic class

I have multiple classes that I would like to initialize using the following syntax: class A { b: number = 1 constructor(initializer?: Partial<A>) { Object.assign(this, initializer) } } new A({b: 2}) It seems to me that this ini ...

Tips for extracting information from a TypeScript JSON document

Hey there, I'm currently having trouble understanding how to retrieve data from a JSON file. environment.ts: export const environment = { production: false, urlListBooks: "/assets/list-books.json", urlGetBooks: "/assets/edit- ...

Can an Angular 2 module export an interface?

While attempting to export an interface in a NgModule-declaration, I encountered an error message in my editor (Visual Studio Code) stating: [ts] 'MyInterface' only refers to a type, but is being used as a value here. Below is the code snippet c ...