supermacro/neverthrow

Error type is not checked

rudolfbyker opened this issue · 3 comments

I would expect this not to compile, because I'm using the wrong error type in functions test1 and test2:

import { BaseError } from "make-error";

class FooError extends BaseError {}

class BarError extends BaseError {}

function test1(): ResultAsync<string, FooError> {
  return errAsync(new BarError("bar"));
}

function test2(): Result<string, FooError> {
  return err(new BarError("bar"));
}

However, this seems to go unchecked.

The OK types are checked properly:

function test3(): ResultAsync<string, FooError> {
  return okAsync(123); // Type 'number' is not assignable to type 'string'.
}

function test4(): Result<string, FooError> {
  return ok(new BarError("bar")); // Type 'number' is not assignable to type 'string'.
}

I wonder if this has to do with the fact that typescript uses structural typing as opposed to nominal typing?

In this case FooError and BarError are structurally indistinguishable. If you, say, add distinct attributes to the two types do you get the expected type error?

Interesting, I never thought of that! I will test it ASAP (next week)

This is definitely because of structural vs nominal typing. Here's my recommendation, use symbol to distinguish between two structurally equivalent types, or some other way to explicitly make the structure distinct.

class BaseError {
    readonly prop: string

    constructor(val: string) {
        this.prop = val
    }
}

class FooError extends BaseError {
    public is_foo_error: true = true;
}

class BarError extends BaseError {
    public is_bar_error: true = true;
}

function test1(): FooError {
  return new BarError("bar");
}

You could also use newtype-ts for nominal typing: https://gcanti.github.io/newtype-ts/

More info: https://stackoverflow.com/questions/58454935/how-to-distinguish-structurally-identical-types-in-typescript