Home > Software engineering >  How to tell Typescript to abort a function?
How to tell Typescript to abort a function?

Time:11-20

The typescript for some reason cannot determine that the code is not reachable at runtime and shows an error that cannot happen.

I have code like this:

Live example

class ApplicationError extends Error {}

class Company {
  public ownerId: number = 123;
}

// This is a mock to avoid business logic in example
function getCompany() {
  return Math.random() ? new Company() : null;
}

class Logger {
  private logAndThrow(method: 'error' | 'warning' | 'info' | 'debug', messageOrError: string | ApplicationError) {
    if (typeof messageOrError === 'string') {
      // ...
      console.log(this);
      return;
    }

    throw messageOrError;
  }

  public debug(message: string): void;
  public debug(error: ApplicationError): never;
  public debug(messageOrError: string | ApplicationError) {
    this.logAndThrow('debug', messageOrError);
  }
}

const logger = new Logger();

const company = getCompany();

logger.debug(new ApplicationError());

if (company.ownerId === 123) {
  // ...
}

Why Typescript shows an error TS2531: Object is possibly 'null'. at the last condition line?

CodePudding user response:

TypeScript 3.7 introduced support for treating never-returning functions like inline throw in terms of control flow analysis and reachability. Before that there was no way to get the behavior you're looking for. This was implemented in microsoft/TypeScript#32695 in order to make assertion functions work.

And for both assertion functions and never-returning functions, this behavior only works if the function has an explicit type annotation, so that "control flow analysis of potential assertion calls doesn't circularly trigger further analysis" (see microsoft/TypeScript#45385 for more information). Observe:

const assertsAnnotated: (x: any) => asserts x is string = () => { };

declare const x: unknown;
assertsAnnotated(x);
x.toString(); // okay

const assertsUnannotated = assertsAnnotated;

declare const y: unknown;
assertsUnannotated(y); // error,
// Assertions require every name in the call target
// to be declared with an explicit type annotation
y.toString(); // error, unknown

const throwsAnnotated: () => never = () => { throw new Error(); }
const throwsUnannotated = neverAnnotated;

if (Math.random() < 0.5) {
  throwsAnnotated();
  console.log("abc"); // error, unreachable
} else {
  throwsUnannotated();
  console.log("abc"); // no error
}

Both assertsAnnotated and throwsAnnotated affect control flow in the desired way, since they are explicitly annotated (note that function statements also count as explicit annotations), while assertsUnannotated and throwsUnannotated do not, since their types are inferred.

The assertsUnannotated() even causes a compiler error to warn you that the asserts has no effect; this was implemented in microsoft/TypeScript#33622. Originally this error would also have warned about throwsUnannotated, but that functionality was removed as per this comment. So a never-returning function will just silently fail to affect reachability if the function is not explicitly annotated.


And that's why this code does not affect reachability:

const logger = new Logger();
logger.debug(new ApplicationError());
console.log("apparently reachable"); // no error here

The never-returning function is logger.debug, but the type of logger is inferred to be Logger by the assignment. So logger.debug's type is not explicitly annotated, and so no special control flow analysis occurs (and no error warns you about that).

The workaround/fix here is to explicitly annotate logger:

const logger: Logger = new Logger();
logger.debug(new ApplicationError());
console.log("apparently reachable"); // error!  unreachable code

Playground link to code

  • Related