#Interceptor misses Type Information

78 messages · Page 1 of 1 (latest)

river laurel
#

Dear all,

in my application i have implemented neverthrow to indicate, whether a result is Ok or an Err. This result is passed back to the controller, where it can be properly handled (i.e., show error messages). Checking for an error may look like this:

async getUserById(id: string) {
  const result = await this.userService.getById(id);
    
  // console.log(typeof result); // --> may be of type "Err"
  // console.log(result.constructor.name); // --> may be "Err"
    
  if (result.isErr()) {
    throw result.error
  }
  return result;
}

As i do not want to have those checks (or try/catch) in my controller methods, i thought about handling this in an Interceptor and apply the latter to all routes globally.

However, when i created a minimal interceptor, i noticed, that the type information (i.e., the result from the controller method is a type Result<User, Error>) is lost. The information, however, is present in the controller.

My interceptor looks like this:

@Injectable()
export class ExceptionInterceptor<T> implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<T> {
    return next.handle().pipe(
      map((data) => {
        console.log(typeof data); // --> returns "object"
        console.log(data.constructor.name); // --> returns "Object"
        // ...
      }),
    );
  }
}

It seems that the type information (i.e., the type) is lost when it is passed into the pipe() method from the Observable. What i would actually like to have is:

@Injectable()
export class ExceptionInterceptor<T> implements NestInterceptor {
  intercept(context: ExecutionContext, next: CallHandler): Observable<T> {
    return next.handle().pipe(
      map((data) => {
        if (data instanceof Result) {
          if(data.isErr() {
            throw data.error;
          }
          // ...
        }
      }),
    );
  }
}

Am I missing something crucial here?

All the best and thanks for your help!

dusty vector
#

why not use exception filters?

river laurel
#

because the ExceptionFilter catches the exeption at the time it is thrown (i.e., down in the service)

dusty vector
#

also, instead of map, I believe you should use catchError like the docs shows

river laurel
#

the map() was just to see whether i can get the proper type

#

my main issue is, that there is no proper way to indicate whether a method throws an exception or not (without looking at the code). so, if i call a method, i would need to know, if i need to try/catch and so on.. therefore, i thought about "wrapping" the result in a Result type that indicates whether it is ok or err (i.e., see neverthrow library)

dusty vector
#

yeah, I like that approach

river laurel
#

this would allow me to simple generate an exception (i.e., "user not found" or whatever) and pass the result back up to the callee up to the controller..

dusty vector
#

so you want to return an object instead of raising an exception?

river laurel
#

and then handle the error at the controller level..

#

if it is not properly handled before (i.e., via a try/catch)

#

or whatever

#

to do so, however, i would need to add a

#

if(result.isErr() { throw result.error;} check in all my controller methods

#

which is quite stupid, i guess, haha,..

#

and therefore, i thought, to just simply return the result (which is either Ok or still an Error) and handle this in an Interceptor globally.

quaint urchin
#

for this last case you could just use a basic imported method

#

I don't find it terrible to handle this at controller level it makes it explicit

river laurel
river laurel
#

oh.. you mean an imported method for the check? yeah, that would be possible, but you would still need to import it into every method..

#

for me, the issue is, that the type information (i.e., the result from the controller method is of type X) is lost.. so i cannot say, whether it is an Ok, Err or WhateverObject type.. and therefore, i cannot check via instanceof

#

data that is passed into the interceptor is - at least what it looks for me - of type Object

quaint urchin
river laurel
quaint urchin
#

Yes I understand the base problem and I don't have an answer

river laurel
#
app.useGlobalInterceptor(new ExceptionInterceptor(), ... );
quaint urchin
#

yes that feels like a more nesty solution

river laurel
#

so i don't need to add custom decorators to every method..

quaint urchin
#

console.log(typeof data); // --> returns "object"

#

what are you expecting here?

river laurel
#

if the result in the controller is an Err object, i would expect it to be an Err object in the Interceptor as well.

#

see the code for the controller. when i run console.log(typeof result) i get (for example) Err so i can properly work with this information

quaint urchin
#

did you check that the type you expect are the right ones in the controller?

#

This doesn't look like a TS problem

river laurel
#

yeah, types in the controller are correct

#

i can also get the result.constructor.name (i.e., "Err")

quaint urchin
#

Try with data instanceof Error but it shall be the same

#

You could rely on duck typing

river laurel
#

duck typing would be a very hacky approach and may not be suitable (i.e., if i dont have fields where i can properly distinguish between my types)

#

fun fact: neverthrow would provide type guards, however, i would need the information that the object is a proper Result object

#

the Result.isErr() or Result.isOk() methods are custom type guards.

#

but - again - i cannot use it, because the type information is lost somewhere along the way 😦

quaint urchin
#

Err is a custom type I suppose?

#

Try with standard Error just to be sure

river laurel
#

Err is a class from neverthrow, exactly

quaint urchin
#

I would try to make it basically work without neverthrow first

#

then you can debug more serenely

#

just a random thought make sure you don't have class-transformer infer in some way (like transform through validationPipe)

river laurel
#

thank you very much @quaint urchin for helping and providing so many ideas..

#

i also tried to return a custom class (and not a Response<Data, Error> class from neverthrow

#

but got the same result

#

i use class-transformer, however, only for input validation (and not for returning data)

#

i have also removed all other interceptors

#

hmmmmmmmmm.. well, i guess, i will have to figure some other ways out 🙂

#

i will try to create a minimal example and figure out a solution a

#

and then move everything back to my main api

#

thank you so much for helping me out!

quaint urchin
#

Give feedback if you manage to make it work anyway

#

thanks for neverthrow I had forgotten it will look in details

river laurel
#

i can highly suggest neverthrow.. it is really conventient to work with it, because it can provide you with "type safety" with respect to errors that happened in your application

#

otherwise you need to check the method to see if exceptions are thrown that must be catched or whatever..

feral mango
#

Do you have a reproduction showing that the instances from neverthrow get lost upon use of an interceptor?

#

To me, that sounds like another interceptor is in place and serializing the result. Nest shouldn't be modifying the returned data or instance of the data unless specified to do so or the response is about tobe sent (has to serialize before sending the data of course)

#

Also, typeof only brings back JS primitives, right? number, string, object, etc. I've never seen typeof return the instance type of an object

river laurel
#

i will check.. i thought that i have removed all other interceptors.. i will check and give feedback later..

#

thank you so much for providing all those ideas and information

charred halo
#

Any idea why instanceof is not working with Mongoose returned type? (the type is model)

feral mango
#

As in something like data instanceOf Cat where Cat extends Model?

#

Mongoose is honestly pretty awful about instanceof because it was never written to be class based in the first place. It'll return objects that satisfy the class type (yay structural typing) but aren't actually instances of the class itself. Most of the time you have to take the mongoose return and create the instance yourself

charred halo
#

yeah

#

tbh trying to do the instanceof on nested type (and using discrimination) .. so how could I differ between different types?

feral mango
#

Hmm, type guard via checking for the presence of a key?

charred halo
#

I used the discrimination key with if statement, but doesn't give me the correct type (the check works, but struggling with the typescript typing)

feral mango
#

Can you show what you currently have? May be easier to help if I can see what's going on