Home > Net >  Type guard for union type generic notification (Notifier | Success) does not work
Type guard for union type generic notification (Notifier | Success) does not work

Time:03-27

I have the following structure:

type Result<TNotification, TSuccess> = Success<TNotification, TSuccess> | Notifier<TNotification, TSuccess>;

abstract class ResultAbstract<TNotification, TSuccess> {
protected constructor(protected readonly result: TNotification | TSuccess) {}

abstract isSuccess(): this is Success<TNotification, TSuccess>;

abstract isFailure(): this is Notifier<TNotification, TSuccess>;

abstract getData(): TNotification | TSuccess;
}

class Success<F, S> extends ResultAbstract<F, S> {
constructor(readonly data: S) {
  super(data);
}

isSuccess(): this is Success<F, S> {
  return true;
}

isFailure(): this is Notifier<F, S> {
  return false;
}

getData(): S {
  return this.result as S;
}

}

class Notifier<E, _> extends ResultAbstract<E, _> {
constructor(readonly data: E) {
  super(data);
}

isSuccess(): this is Success<E, _> {
  return false;
}

isFailure(): this is Notifier<E, _> {
  return true;
}

getData(): E {
  return this.result as E;
}
}

class Fail {}

class User {
  constructor(private name: string){}
}


I am basically trying to build a notification pattern for handling exceptions on my code flow. I have the following functions:

const findUser = (): Result<Fail, User> => {
    if(Math.random() * 0.5 < 0.2)
        return new Success(new User('Tony stark'))
    
    return new Notifier(new Error('Usuário não existe'))
}


const userExists = () => {
    const result = findUser()

    if(result.isFailure()) return 'Not found'

    const user = result.getData() // Property 'getData' does not exist on type 'never'.(2339)


    return user.name
}

console.log(userExists())

The Result type is a union of either Notifier (For errors) or Success, but if I call one of the type guards on the union it does not narrow the remainder (the "else" statement) to the other half of the union.

If I explicitly call the second type guard, it works as expected:

const userExists = () => {
    const result = findUser()

    if(!result.isSuccess()) return 'Not found'

    const user = result.getData() 

    return user.name
}

console.log(userExists()) // it works!

The sample code is here in this playground

Can anyone help me understand this?

CodePudding user response:

The core of the problem here is that Success and Notifier are both structurally the same, so TypeScript thinks that if the following will return true for one of them, it should return true for both: if(result.isFailure())

Hence, the result value type after the if block is automatically inferred as never. I have created an example to illustrate this here.

This problem can easily be fixed by introducing a unique field/method within one of the two classes or alternatively adding a private field/method.

The explanation above will fix the issue you are having and can be used to describe the behaviour of the if(result.isFailure()) statement. However, if(result.isSuccess()) on the other hand still appears to work with your original code. The reason for that is because you have introduced dynamicity in the order of how the nested generic types are assigned to the extending classes:

extends ResultAbstract<F, S> vs extends ResultAbstract<E, _>.

Given the above, the classes can be considered to be different, however TypeScript will still think that Success and Notifier are the same as the is predicates tell it that Success can be Notifier and Notifier can be Success. Hence, all you need to do is remove either the isSuccess or the isFailure methods and the code will start to work. Because your code only uses two classes, this would be the most suitable solution.

  • Related