grpc-web icon indicating copy to clipboard operation
grpc-web copied to clipboard

TypeScript fails to infer message response type after `toObject` call

Open rmilejcz opened this issue 6 years ago • 1 comments

I'm currently working with grpc-web and experiencing an issue with the type safety of response message results being inferred as {}:

  onEnd: res => {
    if(res.message) {
      const msg = res.message.toObject() // <- inferred as {}
      this.setState({
        msgRes: msg.getItem() // <- property getItem does not exist on {}
      })
    }
  }

This is fairly trivial to resolve by using the already provided generic arguments for UnaryRpcOptions:

const opts: UnaryRpcOptions<GetMessageRequest, GetMessageResponse>

or passing these parameters to the unary function:

grpc.unary<GetMessageRequest, GetMessageresponse, MessageServiceGetMessage>(
  //...
)

which makes me think this is either supposed to be inferred and isn't or could be made to infer the correct type easily. It definitely makes sense that if TResponse is inappropriately being passed as {} then I would end up with the above result.

In case this is a bug:

TypeScript version is 3.5.2 grpc-web version is 0.9.6 ts-protoc-gen is 0.10.0

my tscongif:

{
  "compilerOptions": {
    "emitDecoratorMetadata": true,
    "experimentalDecorators": true,
    "jsx": "react",
    "module": "es6",
    "moduleResolution": "node",
    "noImplicitAny": true,
    "outDir": "./dist",
    "preserveConstEnums": true,
    "target": "es5",
    "strict": true,
    "esModuleInterop": true,
    "allowJs": true,
    "checkJs": true,
    "plugins": [
      { "name": "typescript-tslint-plugin", "configFile": "./tslint.json" }
    ],
    "lib": ["dom"]
  },
  "exclude": ["node_modules", "dist"]
}

rmilejcz avatar Jul 10 '19 17:07 rmilejcz

I believe toObject is not what you're looking for--it seems like it is for accessing via raw calls. res.message is already the type you want (including with __proto__ fields like getItem()). You need to explicitly cast it for TypeScript to handle:

onEnd: res => {
  if (res.message) {
    const msg = res.message as <ObjectType> // cast here
    this.setState({
      msgRes: msg.getItem() // will be valid
    })
  }
}

I don't like having to provide the cast type and other stuff all the time, so I wrapped the grpc.* calls into a function that can inference the type:

unary<TRequest extends ProtobufMessage, TResponse extends ProtobufMessage, TMethod extends UnaryMethodDefinition<TRequest, TResponse>>(
  methodDescriptor: TMethod,
  request: ProtobufMessage
) {
  grpc.unary(methodDescriptor, {
    request,
    host: GrpcService.ENDPOINT,
    onEnd: (response) => {
        if (response.status !== grpc.Code.OK) {
          // err
        }

      const value = response.message as TResponse;
      // do whatever with `value`
    }
  });
}

and use it like so:

sayHello(name: string) {
  const helloRequest = new HelloRequest();
  helloRequest.setName(name);
  unary(Hello.SayHello, helloRequest);
}

twixthehero avatar Feb 27 '20 02:02 twixthehero