New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
proto-loader-gen-types: Support nominal typing with type branding #2281
Conversation
The runtime library doesn't output objects with these For example, the example interface definitions would seem to imply that the following function would work: function isFooRequest(obj: object): obj is FooRequest {
return '__type' in obj && obj.__type === 'FooRequest';
} but that function will usually not work because the |
Thanks for the reply!
Yes, this is indeed true because the purpose of type branding is to provide compile-time type compatibility checking, not for the runtime. And this can also be seen as an advantage, depending on the perspective, because it doesn't imply any runtime overhead.
Thus, keeping this special To recap, this is because the goal of branded type is to detect abnormal data flow in compile time, not for runtime type checking.
I agree with your idea. Normally I also do not prefer type branding, but I recently encountered with some requirements which need type-level metadata to make use of type manipulation. In this case, I had no option but to give up 100% reflective typing in order to get type-level metadata. For such use cases where you have no other options, leaving type branding at least as an option might be the last hope. And the last word if you allow, to relief the uncomfortableness of type assertion for users to make objects, I guess we can exclude permissive interfaces from type branding.
Because objects that the library will output can be the automated starting point for the type branding. |
The problem I see is that users may incorrectly believe that the property is usable at runtime. I believe this can be resolved with a documentation comment in the generated code for each
I think a better approach here would be to have two separate options, one for permissive types and another for restrictive types. Maybe |
update readme update readme
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Appreciate for your review!
I updated the two points you mentioned.
While adding the boolean option with default, I got excessive type instantiation error.
Seems like we are already right under the maximum number of inferable options due to the limitation of tsc, if I am not doing something wrong :(
I demonstrated two workarounds.
But indeed, requiring workaround doesn't make you pleasant 😭
.option('inputBranded', { | ||
boolean: true, | ||
default: false, | ||
}) | ||
.option('outputBranded', { | ||
boolean: true, | ||
default: false, | ||
}) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This is another option we can take, apart from just leaving them optional.
By using option
method, we can avoid excessive type instantiation error, still providing the same parsing behavior.
Unfortunately, this changes the order of help descriptions, lifting inputBranded
and outputBranded
to the top.
Since these options are not significantly important options, I guess they should remain at the bottom.
This can be fixed if all the other options are declared using option
as well, but I am not sure if this breaks the current style convention.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Changing everything else to use option
seems like a good way to handle this.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Fixed it.
Since the printed order has not been exactly aligned with declaration order in the code and I don't know how exactly they were decided,
I adjusted declaration order a bit to preserve the original printed order.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This looks good to me. Thank you for your contribution. There's just one more thing: please send another PR to document these options in this file similar to grpc/proposal#326 that documented another recent update
Great! |
I published this in version 0.7.4 |
TL;DR
This PR introduces a boolean option
branded
forproto-loader-gen-types
.If the branded option is on,
interface
representing a Message has additional__type
property, that has thefullName
of originatingMessage
as its value.turns into
Details
Since typescript only supports structural typing, if any two types has the exact same structure, they can be used interchangeably.
This makes codes like below totally valid.
Note : since method signature generated by proto-loader-gen-types is too complicated, I will use much intuitive version(i.e., requestFoo, requestBar) here.
If this behavior is not what you want, typically you can fix this in typescript by using technique called type branding.
https://github.com/microsoft/TypeScript/blob/7b48a182c05ea4dea81bab73ecbbe9e013a79e99/src/compiler/types.ts#L693-L698
Restricted vs Permissive
It is often cumbersome to use type assertion when you are providing the branded type.
For example, there is no additional effort required for using branded type returned from gRPC method, but creating a branded object requires type assertion.
I wonder if we need to include restricted interface only, or should we separate options for restricted and permissive interface respectively?
Motivating materials