TypeScript
TypeScript copied to clipboard
TypeScript does not recognize unreachable code after a method returning never is called outside of its defining scope
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
}
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.
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');
I ran into this problem too:
function getThrowError() {
return function throwError() {
throw new Error();
};
}
const throwError = getThrowError();
throwError();
// Should be unreachable
console.log("test");
I couldn't get it to work with an interface either.
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'.
}
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;
}
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;
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'.
}
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;
}
But it is definitelly better to fix the inconsistent behaviour.
@RyanCavanaugh, could you please help with raising the priority of this issue?
Duplicate of https://github.com/microsoft/TypeScript/issues/60368, I think?