Is subtyping causing issues in TypeScript's inheritance model?

I am currently utilizing TypeScript for my coding projects, and I have observed that it can allow the production of non-type-safe code. Despite implementing all the "strict" options available to me, the behavior I am experiencing goes against the principle of "Inheritance implies subtyping". This issue has been discussed in various platforms, such as:

https://stackoverflow.com/questions/50729485/override-method-with-different-argument-types-in-extended-class-typescript

One specific piece of code that does not raise any type errors is as follows:

abstract class A {
    abstract do(x: number | string): number;
}

class B extends A {
    override do(x: number): number {
        return x;
    }
}

const a: A = new B();

const x: number = a.do("dupa");

console.log(x);

Instead of receiving an expected error message like:

Error:(7, 14) TS2416: Property 'do' in type 'B' is not assignable to the same property in base type 'A'.
  Type '(x: number) => number' is not assignable to type '(x: string | number) => number'.
    Types of parameters 'x' and 'x' are incompatible.
      Type 'string | number' is not assignable to type 'number'.
        Type 'string' is not assignable to type 'number'.

The output actually displays "dupa" in the console.

In attempt to troubleshoot this issue, I tried switching types (X, Y) from (number, string) to different pairs under the assumption that there might be implicit casting involved. However, even with pairs of arbitrary or non-assignable types X and Y, or with some combination where X and Y=null (while working with strictNullChecks), I encountered the same outcome.

Furthermore, I was able to deliberately create a type error

Type 'string | number' is not assignable to type 'number'.   Type 'string' is not assignable to type 'number'.
. Hence, in general, such assignments should not be permitted.

It appears that this behavior could potentially be considered as a "feature rather than a bug", as indicated in: https://github.com/microsoft/TypeScript/issues/22156

Thus, I am now seeking to rephrase the inquiry:

Is there a workaround available that would prompt the TypeScript type checker to detect the absence of contravariance in parameter types within overridden methods?

Answer №1

To achieve true soundness and type-safety in TypeScript, it would be necessary for the language to compare function and method parameters contravariantly. This would ensure that method overrides only widen parameter types without narrowing them. However, TypeScript is not designed to be truly sound, as stated in the TypeScipt design goals. The goal is to balance correctness and productivity rather than aim for a provably correct type system.

In TypeScript, method parameters are compared bivariantly, allowing both widening and narrowing of parameter types when overriding methods. Even though non-method function types became strict with the introduction of the --strictFunctionTypes compiler option, method parameters remain bivariant regardless of this setting.

There are reasons behind this decision, one being that developers often want to treat arrays covariantly, leading to potential unsound behavior. Similar unsoundness exists with properties in TypeScript, where covariance can lead to unexpected runtime errors.

Enforcing soundness for methods while properties remain unsound creates inconsistencies in the language. Therefore, TypeScript continues to allow unsafe method overrides but restricts standalone functions and callback function types.


If you need a workaround, consider using function-valued properties instead of methods in your code. This can help avoid conflicts with method overrides by defining these functions syntactically within your type definitions.

interface A {
    do: (x: number | string) => number;
}

class B implements A {
    do(x: number): number { // error!
        return x;
    }
}

You could also use abstract classes with overridden function properties instead of methods to maintain the structure required by your code:

abstract class A {
    abstract do: (x: number | string) => number;
}

class B extends A {
    override do = function (x: number) { // error            
        return x;
    }
}

For more information on working around TypeScript limitations, check out the provided Playground link which showcases different approaches to handling type-related issues.

Answer №2

There appears to be a common saying in the coding world, that it's more of a feature than a bug, as detailed here:

https://github.com/microsoft/TypeScript/issues/22156

This revelation is disheartening: even with strict flags enabled (such as strict and strictFunctionTypes), you may end up with error-free code that simply does not function properly due to a type error.

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

Issue: Unable to locate a matching object '[object Object]' of type 'object'. NgFor can solely bind to data structures like Arrays and Iterables

I am facing an error that says "Cannot find a differ supporting object '[object Object]' of type 'object'. NgFor only supports binding to Iterables such as Arrays." I am trying to create a Notification list but I can't figure out w ...

Tips for resolving this unhandled error in React TypeScript

After creating a program in React TypeScript, I encountered an uncaught error. Despite running and debugging tests and conducting extensive research on Google, I have been unable to resolve this issue on my own. Therefore, I am reaching out for assistance ...

Enhancing Angular input validators with updates

Working on a project with Angular 6, I have set up an input field using mat-input from the Angular Material framework and assigned it an id for FormGroup validation. However, when I initialize my TypeScript class and update the input value, the validator d ...

How can you enhance a component by including additional props alongside an existing onClick function?

As a newcomer to React and TypeScript, I've created a simple component that looks like this: const CloseButton = ({ onClick }: { onClick: MouseEventHandler }) => { const classes = useStyles(); return <CloseIcon className={classes.closeButto ...

Is it necessary to manually unsubscribe from observables in the main Angular component?

I'm facing a dilemma with my Observable in the root Angular (6.x) component, AppComponent. Typically, I would unsubscribe from any open Subscription when calling destroy() using the lifecycle hook, ngOnDestroy. However, since the AppComponent serv ...

Ensuring Commitments in the ForEach Cycle (Typescript 2)

Having trouble with promise chains after uploading images to the server in Angular 2 and Typescript. Let's come up with some pseudo-code: uploadImages(images):Promise<any> { return new Promise((resolve, reject) => { for (let imag ...

Ways to redirect to a different page following a successful execution of a mutation in React-query

I am facing an issue where a memory leak warning appears when I redirect to another page after a mutation. Despite trying various methods, I have not been able to find a solution. The specific warning message is: Warning: Can't perform a React state ...

Tips for refining TypeScript discriminated unions by using discriminators that are only partially known?

Currently in the process of developing a React hook to abstract state for different features sharing common function arguments, while also having specific feature-related arguments that should be required or disallowed based on the enabled features. The ho ...

Refreshing the cache in SWR, but the user interface remains unchanged inexplicably - SWR hook in Next.js with TypeScript

I am currently working on a project that resembles Facebook, and I am facing an issue with the like button functionality. Whenever I press the like button, I expect to see the change immediately, but unfortunately, SWR only updates after a delay of 4-8 sec ...

What are some effective ways to exclude multiple spec files in playwright?

Within my configuration, I have three distinct projects. One project is responsible for running tests for a specific account type using one login, while another project runs tests for a different login. Recently, I added a third project that needs to run t ...

The custom native date adapter is facing compatibility issues following the upgrade of Angular/Material from version 5 to 6

In my Angular 5 application, I had implemented a custom date adapter as follows: import {NativeDateAdapter} from "@angular/material"; import {Injectable} from "@angular/core"; @Injectable() export class CustomDateAdapter extends NativeDateAdapter { ...

Encountering an Unexpected Index Error with ngFor in Angular 4/5

I am struggling to create a list of inputs and I can't seem to get ngFor to work properly. <div *ngFor="let q of questions; let i = index" class="col-3"> <div class="group"> <input [(ngModel)]="q" [class.ng-not-empty]="q.length & ...

What is the expected return type in TypeScript of a function that returns a void function?

I recently received feedback during a code review suggesting that I add return type values to my functions. However, I am unsure of what return type to assign to this particular function: function mysteryTypeFunction(): mysteryType { return function() ...

Exploring Opencascade.js: Uncovering the Real Text within a TCollection_ExtendedString

I am currently attempting to retrieve the name of an assembly part that I have extracted from a .step file. My method is inspired by a blog post found at , however, I am implementing it using javascript. I have managed to extract the TDataStd_Name attribut ...

Using React with an Array of Promises in Typescript

I have a function that looks like this: function queryProposals(hash:string) { let result = api?.query.backgroundCouncil.proposalOf( hash,(data1:any)=>{ let injectedData = data1.toPrimitive().args.account as InjectedAccou ...

The server's response is unpredictable, causing Json.Parse to fail intermittently

I have encountered a strange issue that is really frustrating. It all started when I noticed that my Json.Parse function failed intermittently. Here is the code snippet in question: const Info = JSON.parse(response); this.onInfoUpdate(Info.InfoConfig[0]); ...

Angular2: Once user is authenticated, navigate to specific routes

I've developed an admin panel with a router for page navigation. The layout of the admin panel includes a sidebar (Component), header (Component), and content (Component). Within the content, I have inserted <router-outlet></router-outlet> ...

The Ion-item-option button requires two clicks to activate

Within my ion-list, I have sliding items that are dynamically created using a for loop. Interestingly, when clicking on an item to navigate to another page, everything works fine. However, upon sliding an item, a button is revealed but it requires two clic ...

Is it possible to showcase a variety of values in mat-select?

Is it possible to pass different values to the .ts file in each function? For example, can I emit a String with (selectionChange)="onChangeLogr($event)" and an object with (onSelectionChange)="onChangeLogr_($event)"? How would I go about doing this? ...

Create a functioning implementation for retrieving a list of objects from a REST API

I am looking to incorporate an Angular example that retrieves a list from a REST API. Here is what I have attempted: SQL query: @Override public Iterable<Merchants> findAll() { String hql = "select e from " + Merchants.class.getName ...