Skip to content

Commit

Permalink
feat: oneof=unions-value to use the same field name for oneof cases (#…
Browse files Browse the repository at this point in the history
…1062)

Fixes #1060.

This adds a new `oneof=unions-value` option that changes how union oneof
code is generated. Instead of each case having a field named the same as
the `$case`, each of them has the same field, called `value`. This
should simplify writing generic code that can handle multiple cases at
once.

I chose to make it another option for `oneof=` instead of a separate
option since it's just another way of handling oneofs. I also updated
the README but it may be a bit much, I'm happy to remove some stuff. I
also presumptively suggest that this be the new recommended oneof
option.
  • Loading branch information
bhollis authored Jun 15, 2024
1 parent 227ae46 commit 7493090
Show file tree
Hide file tree
Showing 13 changed files with 1,418 additions and 18 deletions.
50 changes: 43 additions & 7 deletions README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -754,7 +754,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
- Probably change `forceLong` default in 2.0, should default to `forceLong=long`
- Make `esModuleInterop=true` the default in 2.0

Expand All @@ -772,23 +772,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 @@ -799,19 +809,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

0 comments on commit 7493090

Please sign in to comment.