Skip to content
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

feat: oneof=unions-value to use the same field name for oneof cases #1062

Merged
merged 4 commits into from
Jun 15, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 44 additions & 8 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -603,7 +603,7 @@ export interface User {
}
```

- With `--ts_proto_opt=noDefaultsForOptionals=true`, `undefined` primitive values will not be defaulted as per the protobuf spec. Additionally unlike the standard behavior, when a field is set to it's standard default value, it *will* be encoded allowing it to be sent over the wire and distinguished from undefined values. For example if a message does not set a boolean value, ordinarily this would be defaulted to `false` which is different to it being undefined.
- With `--ts_proto_opt=noDefaultsForOptionals=true`, `undefined` primitive values will not be defaulted as per the protobuf spec. Additionally unlike the standard behavior, when a field is set to it's standard default value, it *will* be encoded allowing it to be sent over the wire and distinguished from undefined values. For example if a message does not set a boolean value, ordinarily this would be defaulted to `false` which is different to it being undefined.

This option allows the library to act in a compatible way with the [Wire implementation](https://square.github.io/wire/) maintained and used by Square/Block. Note: this option should only be used in combination with other client/server code generated using Wire or ts-proto with this option enabled.

Expand Down Expand Up @@ -752,7 +752,7 @@ ts-protoc --ts_proto_out=./output -I=./protos ./protoc/*.proto
# Todo

- Support the string-based encoding of duration in `fromJSON`/`toJSON`
- Make `oneof=unions` the default behavior in 2.0
- Make `oneof=unions-value` the default behavior in 2.0
Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ah nice! I definitely agree this value field is better and should be the default.

We're in luck that a 2.x release is likely going to happen in the next ~month or so, via #1058, so we should plan on flipping this default over, and honestly probably just removing the option/old behavior all together.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds great, feel free to ping me if you want help with that.

Copy link
Owner

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great! I'll ping you once Timo's PR lands as an 2.0.0 alpha and would be great to get this flipped over.

- Probably change `forceLong` default in 2.0, should default to `forceLong=long`
- Make `esModuleInterop=true` the default in 2.0

Expand All @@ -770,23 +770,33 @@ Will generate a `Foo` type with two fields: `field_a: string | undefined;` and `

With this output, you'll have to check both `if object.field_a` and `if object.field_b`, and if you set one, you'll have to remember to unset the other.

Instead, we recommend using the `oneof=unions` option, which will change the output to be an Abstract Data Type/ADT like:
Instead, we recommend using the `oneof=unions-value` option, which will change the output to be an Abstract Data Type/ADT like:

```typescript
interface YourMessage {
eitherField?: { $case: "field_a"; field_a: string } | { $case: "field_b"; field_b: string };
eitherField?: { $case: "field_a"; value: string } | { $case: "field_b"; value: string };
}
```

As this will automatically enforce only one of `field_a` or `field_b` "being set" at a time, because the values are stored in the `eitherField` field that can only have a single value at a time.

(Note that `eitherField` is optional b/c `oneof` in Protobuf means "at most one field" is set, and does not mean one of the fields _must_ be set.)

In ts-proto's currently-unscheduled 2.x release, `oneof=unions` will become the default behavior.
In ts-proto's currently-unscheduled 2.x release, `oneof=unions-value` will become the default behavior.

There is also a `oneof=unions` option, which generates a union where the field names are included in each option:

```typescript
interface YourMessage {
eitherField?: { $case: "field_a"; field_a: string } | { $case: "field_b"; field_b: string };
}
```

This is no longer recommended as it can be difficult to write code and types to handle multiple oneof options:

## OneOf Type Helpers

The following helper types may make it easier to work with the types generated from `oneof=unions`:
The following helper types may make it easier to work with the types generated from `oneof=unions`, though they are generally not needed if you use `oneof=unions-value`:

```ts
/** Extracts all the case names from a oneOf field. */
Expand All @@ -797,19 +807,45 @@ type OneOfValues<T> = T extends { $case: infer U extends string; [key: string]:

/** Extracts the specific type of a oneOf case based on its field name */
type OneOfCase<T, K extends OneOfCases<T>> = T extends {
$case: K;
[key: string]: unknown;
}
? T
: never;

/** Extracts the specific type of a value type from a oneOf field */
type OneOfValue<T, K extends OneOfCases<T>> = T extends {
$case: infer U extends K;
[key: string]: unknown;
}
? T[U]
: never;
```

/** Extracts the specific type of a value type from a oneOf field */
export type OneOfValue<T, K extends OneOfCases<T>> = T extends {
For comparison, the equivalents for `oneof=unions-value`:

```ts
/** Extracts all the case names from a oneOf field. */
type OneOfCases<T> = T['$case'];

/** Extracts a union of all the value types from a oneOf field */
type OneOfValues<T> = T['value'];

/** Extracts the specific type of a oneOf case based on its field name */
type OneOfCase<T, K extends OneOfCases<T>> = T extends {
$case: K;
[key: string]: unknown;
}
? T
: never;

/** Extracts the specific type of a value type from a oneOf field */
type OneOfValue<T, K extends OneOfCases<T>> = T extends {
$case: infer U extends K;
value: unknown;
}
? T[U]
: never;
```

# Default values and unset fields
Expand Down
Loading
Loading