protobuf-es
protobuf-es copied to clipboard
Inherit top-level type (e.g. PlainMessage)
I have some user-facing functions that take PlainMessage<T> as input. The problem is that PlainMessage doesn't seem to trickle down to properties that themselves are messages. Take the below snippets as an example:
Protobuf
syntax = "proto3";
option optimize_for = SPEED;
message Op {
string denomination = 1;
}
message Msg {
repeated Op ops = 1;
}
// Type
type x = PlainMessage<Msg>
// Expected
type x = {
ops: PlainMessage<Op>[];
}
// Actual
type x = {
ops: Op[];
}
When using PlainMessage<T> I would expect all properties of T that are also messages to be also referenced as PlainMessage rather than expecting the (full) Message. Is this an intentional design choice and can this be worked around without a custom type outside the library or is this an oversight? Thanks in advanced!
Hey Brian, thanks for this issue!
The behavior is a design choice - the type is an exact representation for a message cloned with the spread operator: let plain: PlainMessage<Example> = {...example};
I'm aware that this is not always helpful in practice. Making PlainMessage<T> recursive is definitely an option, but I'm not quite sure yet that it is the best option. So before we consider this, we will be looking into generating an interface IExample for every message Example alongside the class, which behaves exactly like a recursive PlainMessage<T>.
The rationale for interface IExample is that a simple interface can be a bit more approachable and can provide better TypeScript error messages than a mapped type. It also means we would generate a bit more code, so we obviously want to consider this carefully.
Expect to see some updates regarding this soonish. In the meantime, I don't see any complications from using your own recursive version of PlainMessage<T>. If you do, please do let us know about how it works out for you!
A recursive version of PlainMessage:
import type { Message } from "@bufbuild/protobuf";
export type Plain<T extends Message> = {
[P in keyof T as T[P] extends Function ? never : P]: PlainField<T[P]>;
};
// prettier-ignore
type PlainField<F> =
F extends (Date | Uint8Array | bigint | boolean | string | number) ? F
: F extends Array<infer U> ? Array<PlainField<U>>
: F extends ReadonlyArray<infer U> ? ReadonlyArray<PlainField<U>>
: F extends Message ? Plain<F>
: F extends OneofSelectedMessage<infer C, infer V> ? { case: C; value: Plain<V> }
: F extends { case: string | undefined; value?: unknown } ? F
: F extends { [key: string|number]: Message<infer U> } ? { [key: string|number]: Plain<U> }
: F;
type OneofSelectedMessage<K extends string, M extends Message> = {
case: K;
value: M;
};
@timostamm Thanks! The Plain type does exactly what I needed, a lot nicer to work with without having to plaster my code with @ts-ignore since the underlying code works as expected 😅 Separate interfaces would be nice, could probably also be used to generate mocks from those interfaces if someone had the need for that.
@timostamm that Plain type looks wonderful! Is there any chance of including that next to PlainMessage<> and PartialMessage<> in @bufbuild/protobuf?
Absolutely, @sjbarag. We're just wrapping up some work on the plugin framework, but this issue is up next!
@faustbrian @sjbarag We landed #308 today, which makes the current PlainMessage type recursive. Closing this as a result, so let us know if this change works for you. If you notice any problems, feel free to open another issue.
Excellent, thanks @smaye81 !