Typescript utilizes structural compatibility for determining type compatibility. When evaluating whether the class Incorrect
is compatible with Failure
, it compares the structure of Incorrect
with the members of the Failure
union. This analysis reveals that FileNotFound
shares the same structure as Incorrect
, making them compatible.
To address this issue, you can add a member (preferably private
) to all classes in the union. For instance, the following code snippet demonstrates a scenario where failure occurs:
class Success<T> { constructor(public value: T) { } isSuccess: true = true }
type Try<T, E> = Success<T> | E;
class BaseFailure {
isSuccess: false
}
type Failure = FileNotFound | NameNotFound;
class FileNotFound extends BaseFailure { private _type: "filenotfound" }
class NameNotFound extends BaseFailure { private _type: "namenotfound" }
class Incorrect extends BaseFailure { }
type Result<T> = Try<T, Failure>
function y() {
let x1 = x(1);
if (x1.isSuccess) {
} else {
}
}
function x(a: number): Result<String> {
if (a > 3) {
return new Success("Hallo");
} else {
return new Incorrect(); // error now
}
}
You can also include the private field in BaseFailure
to mandate implementers to specify it:
class BaseFailure<T> {
isSuccess: false
private __type: T
}
class FileNotFound extends BaseFailure<"filenotfound"> { }
class NameNotFound extends BaseFailure<"namenotfound"> { }
class Incorrect extends BaseFailure<"incorrect"> { }
The concept of allowing nominal typing has been a subject of ongoing discussion (refer to the issue). For now, implementing the aforementioned approach represents the most effective solution.