TypeScript icon indicating copy to clipboard operation
TypeScript copied to clipboard

TypeScript does not recognize unreachable code after a method returning never is called outside of its defining scope

Open dkfdkdodkms opened this issue 1 year ago • 2 comments

Does this issue occur when all extensions are disabled?: Yes

  • VS Code Version: 1.89.1
  • OS Version: All

To Reproduce:

class SimpleThrower {
    public throwError(): never {
        throw new Error('');
    }

    public doSomething(): string {
        this.throwError(); // Directly call the never-returning function
        
        return 'Success'; // TypeScript recognizes that this line is unreachable
    }
}

() => {
    const example = new SimpleThrower();
    console.log(example.doSomething()); // This will throw an error
    example.throwError(); // Throws an error and stops execution
    console.log('foobar'); // TypeScript does not recognize that this line is unreachable
}

Playground Link

Expected behavior:

TypeScript should recognize that the line console.log('foobar'); is unreachable after example.throwError(); is called, similar to how it recognizes the unreachable code within the doSomething method.

Actual behavior:

TypeScript does not mark the line console.log('foobar'); as unreachable after the example.throwError(); call in the global scope or within a different function scope.

dkfdkdodkms avatar May 24 '24 14:05 dkfdkdodkms

I ran into this exact same issue today. (See playground)

const process = {
  foo(): never {
    throw new Error('bar');
  },
} as const;

function foo(): never {
  throw new Error('foo');
}

process.foo();

console.log('hello');

foo();

// from this point on, the unreachable code is detected

console.log('world');

However, if I use an interface to define process, it does work correctly. (See playground)

interface IProcess {
  foo(): never;
}

const process: IProcess = {
  foo(): never {
    throw new Error('bar');
  },
} as const;

function foo(): never {
  throw new Error('foo');
}

process.foo();

// from this point on, the unreachable code is detected

console.log('hello');

foo();

console.log('world');

But then again, it doesn't work if you use [key: string] as the key in the interface. (See playground)

interface IProcess {
  [key: string]: () => never;
}

const process: IProcess = {
  foo(): never {
    throw new Error('bar');
  },
} as const;

function foo(): never {
  throw new Error('foo');
}

process.foo();

console.log('hello');

foo();

// from this point on, the unreachable code is detected

console.log('world');

SamVerschueren avatar Aug 22 '24 14:08 SamVerschueren

I ran into this problem too:

function getThrowError() {
  return function throwError() {
    throw new Error();
  };
}

const throwError = getThrowError();

throwError();

// Should be unreachable
console.log("test");

Playground

I couldn't get it to work with an interface either.

amannn avatar Oct 18 '24 08:10 amannn

Actually I think it is a bug, it is not a just suggestion.

interface Throwable {
    throw(): never;
}

function fn(value: string | Throwable): string {
    if (typeof value !== "string") value.throw();
    return value;
//  ^^^^^^ - Type 'string | Throwable' is not assignable to type 'string'.
//           Type 'Throwable' is not assignable to type 'string'.
}

Playground

But this code works well:

interface Throwable {
    throw(): never;
}

declare function fail(): never;

function fn(value: string | Throwable): string {
    if (typeof value !== "string") fail();
    return value;
}

Playground

As well as the following one:

declare const error: {
  throw(): never;
}

declare const value: string | undefined;

if (value == null) error.throw();
const result: string = value;

Playground

This code doesn't work again:

interface Throwable {
    throw(): never;
}

declare function fail(): never;

const context = { fail };

function fn(value: string | Throwable): string {
    if (typeof value !== "string") context.fail();
    return value;
//  ^^^^^^ - Type 'string | Throwable' is not assignable to type 'string'.
//           Type 'Throwable' is not assignable to type 'string'.
}

Playground

So, currently we have the patch with adding return before throw():

function fn(value: string | Throwable): string {
    if (typeof value !== "string") return value.throw();
    return value;
}

Playground

But it is definitelly better to fix the inconsistent behaviour.

@RyanCavanaugh, could you please help with raising the priority of this issue?

DScheglov avatar Oct 22 '24 11:10 DScheglov

Duplicate of https://github.com/microsoft/TypeScript/issues/60368, I think?

alecmev avatar Jul 21 '25 15:07 alecmev