Potential Issue: TypeScript appears to have a bug involving the typing of overridden methods called by inherited methods

I recently came across a puzzling situation:

class A {
  public method1(x: string | string[]): string | string[] {
    return this.method2(x);
  }
  protected method2(x: string | string[]): string | string[] {
    return x;
  }
}
class B extends A {
  protected method2(x: string[]): string { // no compiler warning
    return x.join(',');
  }
}
const b = new B();
console.log(b.method1(['a', 'b', 'c'])); // ok
console.log(b.method1('d')); // runtime error

Is this an issue with TypeScript's typing system or intentional behavior? If the latter, what adjustments can be made to the typing to catch these errors during compilation?

Answer №1

In TypeScript, methods have the property of being bivariant, while function properties exhibit contravariance when used with --strictFunctionTypes. It is important to note that they have different syntax:

class MyClass {
  public fn(a: string): string { ... } // this represents a method
  public fn = (a: string): string => { ... } // in contrast, this signifies a function as a property
}

To enforce stricter typing for m2, it is essential to have --strictFunctionTypes enabled (default with strict) and utilize a function property:

protected m2 = (x: string | string[]): string | string[] => {
  return x;
}

Subsequently, m2 will now generate errors appropriately, prompting you to distinguish between string and string[]:

protected m2 = (x: string | string[]): string => { 
  return Array.isArray(x) ? x.join(',') : `${x},`;
}

Check out live code example on Playground

Related: Understanding covariance and contravariance

Related: Explore JS class fields

Answer №2

This is the expected behavior in TypeScript. When you override a method, you are narrowing down the specificity of the method rather than widening it as in some other programming languages.

Regarding the explanation...

class A {
  public m1(x: string | string[]): string | string[] {
    /**
     * In this case, variable 'x' can be either a string or a string[], 
     * which aligns perfectly with the arguments for the method m2. This is why there is no compile-time error.
     */
    return this.m2(x);
  }
  protected m2(x: string | string[]): string | string[] {
    return x;
  }
}
class B extends A {
  /**
   * Here, the method m2 is overridden with a method that only accepts a string[], 
   * which falls within the range accepted by the parent's method. Hence, no compile-time error occurs. Even if the argument type were changed to 'string', it would still be accepted.
   */
  protected m2(x: string[]): string { // no compiler warning
    return x.join(',');
  }
}
const b = new B();
console.log(b.m1(['a', 'b', 'c'])); // ok
/**
 * The type specified when calling method m1 matches what was defined, so there should be no issue here as well
 */
console.log(b.m1('d')); // runtime error

In conclusion, this is more of a logical error rather than a bug in the 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

Searching for a specific document using AngularFirestore - what's the best method?

Is it possible to create an Observable that is limited to a single document? While the code provided creates an Observable for querying multiple documents: foo.component.ts import { AngularFirestore } from '@angular/fire/firestore'; import { O ...

How did my attempt to add a length() method to Object end up breaking jQuery?

Here is the code I created: Object.prototype.length = function(){ var count = -1; for(var i in this) count++; return count; } Surprisingly, when viewing my page with Firebug enabled, it gives an error stating that jQuery's .appendTo() is ...

Tips for utilizing an elective conversion by typing

In my opinion, utilizing code is the most effective approach to articulate my intentions: interface Input { in: string } interface Output { out: string } function doStuff(input: Input): Output { return { out: input.in }; } function f<Out>(input ...

How can you ensure a code snippet in JavaScript runs only a single time?

I have a scenario where I need to dynamically save my .env content from the AWS secrets manager, but I only want to do this once when the server starts. What would be the best approach for this situation? My project is utilizing TypeScript: getSecrets(&qu ...

Encountering a Typescript issue with the updated Apollo server version within a NestJS application

After upgrading my nestJS application to use version 3.2 of apollo-server-plugin-base, I encountered two TypeScript errors related to a simple nestJS plugin: import { Plugin } from '@nestjs/graphql' import { ApolloServerPlugin, GraphQLRequest ...

Information sent from TypeScript frontend to Java backend is converted into a LinkedHashMap

I have a situation where my typescript frontend is communicating with my java backend using REST. Recently, I added a new simple rest endpoint but encountered an issue when trying to cast the sent object properly because the body being sent is a LinkedHash ...

Discovering the generic parameter in the return type using TypeScript

I am struggling with a specific issue export type AppThunk<ReturnType> = ThunkAction< ReturnType, RootState, unknown, Action<string> >; After implementing the above code snippet export const loadCourse = (id: string): AppThunk ...

Tips for displaying personalized data with MUI DatePicker

I need to create a React TypeScript component that displays a MUI DatePicker. When a new date is selected, I want a custom component (called <Badge>) to appear in the value field. Previously, I was able to achieve this with MUI Select: return ( ...

Converting an array of objects into a dictionary using TypeScript

I'm attempting to convert an array of objects into a dictionary using TypeScript. Below is the code I have written: let data = [ {id: 1, country: 'Germany', population: 83623528}, {id: 2, country: 'Austria', population: 897555 ...

Avoid accessing members in Vue 3 using TypeScript that may be unsafe

Recently, we initiated the process of upgrading from Quasar v1 to Quasar v2 (moving from Vue 2 to Vue 3). In the past, this code functioned without any issues: // src/pages/myComponent.vue <script lang="ts"> import { defineComponent } from ...

What steps should I take to resolve an unhandled promise error in a React TypeScript application while making an axios POST request?

I am currently working on a .tsx file to implement adding an enterprise feature. Although I can input data, clicking the submit button does not trigger any action. My application includes a page that has a button for adding a new enterprise. Upon clickin ...

Is there a way for me to navigate from one child view to another by clicking on [routerLink]?

Currently, I am facing an issue with switching views on my Angular website. The problem arises when I attempt to navigate from one child view to another within the application. Clicking on a button with the routerlink successfully takes me to the new view, ...

Combining subclasses in TypeScript

Do you need help with a tricky situation? ...

"Error: The method setValue is not found in the AbstractControl type" when trying to clear form in Angular 2

Here is the template code: <form [formGroup]="form" (ngSubmit)="onSubmit(form.value)" novalidate="novalidate"> <textarea [ngClass]="{ 'error': comment }" [formControl]="form.controls['comment']" ...

Ways to retrieve a value from a span using the Document Object Model

sample.html <span id='char'>{{value}}</span> sample.ts console.log((<HTMLInputElement>document.getElementById('char'))) This code snippet will display <span id='char'>ThisIsTheValueupdated</sp ...

Accessing URL fragments in Next JS with context during the execution of getServerSideProps

I am trying to extract a URL fragment using getServerSideProps. The URL is structured like this: http://localhost:3000/some-folder#desiredParam=value Even though I pass the context as an argument to the getServerSideProps function, I am struggling to retr ...

Angular2 - Breaking down applications into reusable components

Utilizing custom properties permits seamless data binding between multiple components. <section id="main"> <app-home [dict]="dict">Hello there!</app-home> </section> In this scenario, dict serves ...

Passing a Typescript object as a parameter in a function call

modifications: { babelSetup?: TransformationModifier<babel.Configuration>, } = {} While examining some code in a React project, I came across the above snippet that is passed as an argument to a function. As far as I can tell, the modifications p ...

Creating a completely dynamic button in Angular 6: A step-by-step guide

I have been working on creating dynamic components for my application, allowing them to be reusable in different parts of the program. At this point, I have successfully developed text inputs that can be dynamically added and customized using the component ...

Send the Children prop to the React Memo component

Currently, I am in the stage of enhancing a set of React SFC components by utilizing React.memo. The majority of these components have children and the project incorporates TypeScript. I had a notion that memo components do not support children when I en ...