ts-proto icon indicating copy to clipboard operation
ts-proto copied to clipboard

Flat oneofs with type union

Open deser opened this issue 1 year ago • 10 comments

Hey currently oneof have 3 variants and none of them produce correct type for me. I would need the true union to be generated. See the example below: Having

message Some {
  oneof doesntmatter {
    SomeStructureA fielda = 1;
    SomeStructureB fieldb = 2;
  }
}

Generated TS will be:

type Some = {fielda: SomeStructureA} | {fieldb: SomeStructureB}

deser avatar Dec 04 '24 20:12 deser

Hi @deser ; at first I thought that was our default oneof output, in that the fields stay top-level, but, right, we don't technically do the |...

That might be easy-ish to add, since it is similar to the default/flat fields output, if you'd like to attempt a PR. Thanks!

stephenh avatar Dec 04 '24 21:12 stephenh

I'm afraid it's not that simple, consider nested oneof...

syntax = "proto3";

message OuterMessage {
  oneof outer_field {
    InnerMessage inner_message = 1;
  }
}

message InnerMessage {
  oneof inner_field {
    string string_value = 1;
    int32 int_value = 2;
  }
}

I peeked at the code, seems a new chunk of code will need to be written there (I wouldn't be able to re-use existing logic handling oneof as it just generates property in specific way, when with new union type it is required to generate new type for every oneof value), without knowing the code base, it will take considerable amount of time from myself :)

deser avatar Dec 05 '24 00:12 deser

What do you expect the type for your OuterMessage to look like?

it will take considerable amount of time from myself

Yep, that's how open source goes. :-)

stephenh avatar Dec 05 '24 03:12 stephenh

I think somehow like below:

type OuterMessage = {} | {inner_message : {string_value: string}}  | {inner_message : {int_value : number}}

deser avatar Dec 05 '24 15:12 deser

I was thinking it'd be something like:

type OuterMessage = {} | {inner_message : InnerMessage }

type InnerMessage = { string_value: string } | { int_value: number }

Which I believe is "the same type" but would be a lot easier to output, given ts-proto's current "message oriented" output.

stephenh avatar Dec 05 '24 16:12 stephenh

Yes, there's also possibility to put shared props into its own common interface, but that might affect performance, so duplicating is probably ok

deser avatar Dec 05 '24 16:12 deser

FYI: For anyone landing here looking for a workaround, you can use this type to break N optional properties on an interface into a union of required (mutually exclusive) properties.

type ExclusiveUnion<T> = NonNullable<{
  [K in keyof T]: {
    [P in K]-?: T[P];
  }
}[keyof T]>


type Test = ExclusiveUnion<{ foo?: number, bar?: string }>; // { foo: number } | { bar: string }

Here's a TS Playground: https://www.typescriptlang.org/play/?#code/JYOwLgpgTgZghgYwgAgApQPZgwWQgZ3zgHMUBvAKGWuRgwwH4AuZAMXoG4qaAjOKZsgBC-LjWR8AXoJGSuAXwoVQkWIhTsMySuLoYAInDBwWIAK4BbHtADaAXQVKV0eEmH9t3anygA5OBYQLPhgUKDEYrwQ0OjASKaW1lCOyuAu6u6SnuJSkgCeACoAFuH4waHh9hSKFGB5AA4oAKIAHggANmb4wABuEACqIMAYIAA8BQB8yAC8yL4jvmbt7XA87RCjOsg2ANLIoMgA1hB5GDDIBXYsW9Q2qPsgyDt2ALSCBXcO3IryNsen50uEyUtQaKAAgggwGY4O10FhcAQiKQZshWh0ur0BkMRqN4dg8IQSBAJkA

benlesh avatar Dec 11 '24 22:12 benlesh

Yes, there's also possibility to put shared props into its own common interface, but that might affect performance, so duplicating is probably ok

I'm not sure why you wouldn't put the InnerMessage fields in their own type; it's very idiomatic for each message Whatever in a *.proto file to have a type Whatever in the TS code, and InnerMessage being used within a OuterMessage oneof imo shouldn't change that.

Fwiw I would probably try to get the types to be:

type OuterMessage = OuterMessageRegularFields & OuterMessage_OuterFieldA & OuterMessage_OuterFieldB;

type OuterMessageFieldA = { fielda: ... } | { fieldb: ... }

type OtherMessageFieldB = { fieldc: ... | | { fieldd: InnerMessage }

Just to break things down a little bit, for when OuterMessage invariably has multiple oneofs. :shrug:

Fwiw @benlesh I don't totally follow your example, as I believe it assumes that OuterMessage only has a single oneof; as soon as there are multiple oneofs, that approach will break, right?

stephenh avatar Dec 13 '24 01:12 stephenh

For

type OuterMessage = OuterMessageRegularFields & OuterMessage_OuterFieldA & OuterMessage_OuterFieldB;

Wouldn't OuterMessage stay intersection of fields instead of union then, eventually, which will be closer to all optional values, which we don't want here?

deser avatar Dec 16 '24 15:12 deser

Fwiw @benlesh I don't totally follow your example, as I believe it assumes that OuterMessage only has a single oneof; as soon as there are multiple oneofs, that approach will break, right?

@stephenh That's correct. I had a use case at work where there was a message with one oneof which was pretty painful to deal with in TypeScript. I left that comment for anyone else in a similar situation.

benlesh avatar Feb 07 '25 15:02 benlesh