bisque
bisque

Reputation: 85

User-Defined Type Guard with Generics may not work

I implemented a Result type like Either in Scala, but User-Defined Type Guards may not work properly depending on the combination of E and T.

type Result<T, E> = Success<T, E> | Failure<T, E>

class Success<T, E> {
  constructor(readonly value: T) {}
  isSuccess(): this is Success<T, E> {
    return true
  }
  isFailure(): this is Failure<T, E> {
    return false
  }
}

class Failure<T, E> {
  constructor(readonly value: E) {}
  isSuccess(): this is Success<T, E> {
    return false
  }
  isFailure(): this is Failure<T, E> {
    return true
  }
}

In the first example, isSuccess () is recognized as Result. isFailure () is never.

function example1(): Result<string, string> {
  return new Success('success')
}

const result1 = example1()

if (result1.isSuccess()) {
  result1 // Result<string, string>
}

if (result1.isFailure()) {
  result1 // never
}

In the second example, isSuccess () is recognized correctly, but isFailure () is recognized as Result.

function example2(): Result<string, unknown> {
  return new Success('success')
}

const result2 = example2()

if (result2.isSuccess()) {
  result2 // Success<string, string>
}

if (result2.isFailure()) {
  result2 // Result<string, string>
}

In both cases, using instanceof is correctly recognized. How do you use User-Defined Type Guard to be recognized correctly?

Upvotes: 0

Views: 1264

Answers (1)

Tiberiu Maran
Tiberiu Maran

Reputation: 2173

Unfortunately your two types Success and Failure have the same structure and as such are identical and interchangeable from the point of view of typescript.

To clarify because of the constructor signature typeof Success and typeof Failure may be different (when T and E are different), but as no instance related members or methods are different, instances are interchangeable. I mean event this is legal:

// this succeeds with original definition
const f: Failure<string, string> = new Success<string, string>("");

Try something like this:

type Result<T, E> = Success<T, E> | Failure<T, E>

class Success<T, E> {
  constructor(readonly value: T) { this.success = value }
  private success: T;
  isSuccess(): this is Success<T, E> {
    return true
  }
  isFailure(): this is Failure<T, E> {
    return false
  }
}

class Failure<T, E> {
  constructor(readonly value: E) { this.failure = value }
  private failure: E;
  isSuccess(): this is Success<T, E> {
    return false
  }
  isFailure(): this is Failure<T, E> {
    return true
  }
}

function example(): Result<string, string> {
  return new Success('success')
}
const result = example();

if (result.isSuccess()) {
   result // Success<string, string>
}

if (result.isFailure()) {
   result // Failure<string, string>
}

Notice the different property for holding the value.

// now this assignment is no longer legal
const f: Failure<string, string> = new Success<string, string>("");

More information on type compatibility here.

Playground here

Upvotes: 1

Related Questions