1

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?

  • 2
    Your example seems to work. Here is a playground: https://tsplay.dev/N7oJ4N . Please modify to show your concerns – Svetoslav Petkov Nov 17 '22 at 16:27
  • I found this thread with a similar answer. I hope it helps :) https://stackoverflow.com/questions/3330193/early-exit-from-function – Hugo Badenhorst Nov 17 '22 at 16:52
  • If your initial question was incorrect, don't leave the incorrect information up. Your question is too busy and hard to understand. Also, please try to avoid shortening words (Upd). Let's make it as easy as possible for all to understand. – Ruan Mendes Nov 17 '22 at 17:18
  • Ah, TypeScript 3.7 introduced some support for [treating `never`-returning functions like inline `throw` for control flow](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-3-7.html#better-support-for-never-returning-functions), in the same pull request [ms/TS#32695](https://github.com/microsoft/TypeScript/pull/32695) that introduced assertion functions. ... – jcalz Nov 18 '22 at 02:56
  • ... And like assertion functions, you only get the new control flow behavior if the function has an explicit type annotation; but your `logger` is *inferred* to be `Logger`, so there is no type annotation, and so the compiler fails to use the function for control flow. If you add one, like [this playground link](https://tsplay.dev/NnbpBN) shows, it starts working the way you want. Does this fully address your question? If so I will write up an answer explaining; if not, what am I missing? (Pls ping me via @jcalz if you reply) – jcalz Nov 18 '22 at 02:56
  • @jcalz Yes, it is. Please write the answer and I will mark it as correct. Thank you. – Andrey Bokhan Nov 18 '22 at 08:26

1 Answers1

1

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

jcalz
  • 264,269
  • 27
  • 359
  • 360