Search code examples
javascripttypescriptabstract-class

Allowing a class that inherits from an abstract class to return undefined


I have an abstract class that looks like this:

// step.ts

import { AllowedStepNames } from "./interfaces"; 

abstract class Step {
  abstract nextStep(): AllowedStepNames | undefined
}

Which in turn uses a type in a seperate file:

// interfaces/index.ts

const stepList = {
  foo: "bar",
  bar: "baz"
};

export type AllowedStepNames = keyof typeof stepList;

I have a class that extends from it that looks like this:

// referral-reason.step.ts

export default class ReferralReasonStep extends Step {
  nextStep() {
    return this.params.reason === 'no-reason' ? 'foo' : undefined
  }
}

However, the compiler throws an error:

Property 'nextStep' in type 'NotEligibleStep' is not assignable to the same property in base type 'Step'.

However, if I add the return type in my inherited class like so:

export default class ReferralReasonStep extends Step {
  nextStep(): AllowedStepNames | undefined {
    return this.params.reason === 'no-reason' ? 'foo' : undefined
  }
}

This seems strange to me, because I'd expect the extended class to inherit the return type from the abstract class. Can someone tell me what's going on? Or am I doing something wrong?

In addition, if I put all my classes and types in the same file, the problem goes away.

Reproducable example here


Solution

  • I think I got the issue now:

    TypeScript is automatically inferring the type string instead of foo | undefined to your function based on the return type and string is not assignable to the union AllowedStepNames.

    What can you do to fix this?

    Option 1

    As you already said in the post, you can explicitly set the return type of the function to foo | undefined.

    Option 2

    The reason there was no error in the TypeScript playground is a difference in the tsconfig rules. I identified strictNullChecks to be the rule that changes this behavior. So depending on your project, you could enable this rule.

    compilerOptions: {
      strictNullChecks: "true"
    }
    

    Option 3

    By using as const you can tell the compiler to use the string literal value as the type instead of string.

    nextStep() {
      return this.params.reason === "no-reason" ? "foo" as const : undefined;
    }