Skip to content

Commit

Permalink
feat: option to declare schema as const (#1096)
Browse files Browse the repository at this point in the history
This PR adds a new option that allows us to get additional type
information for the `protoMetadata` export when using
`outputSchema=true`.

## Background

Using the `outputSchema=true` option, ts-proto outputs a `protoMetadata`
export for all proto files.

This metadata object can be useful for rudimentary reflection and
required if we want to access value options.
Unfortunately there is no compile time type safety for these values,
because the export is declared as `ProtoMetadata`, losing a lot of
useful type information.

## Proposed Solution

When setting the option `outputSchemaAsConst=true`, do the following:

Instead of doing `export const protoMetadata: ProtoMetadata = { ... }`,
export it as `export const protoMetadata = { ... } as const satisfies
ProtoMetadata`.

This retains the full value of `protoMetadata` on the type level.
This additional type information can then be used to write type safe
reflection methods, like for accessing the values of value options
declared in proto files (see the included tests for some basic
examples).

In order to provide type safety for the `fileDescriptor` field, I had to
remove the call to `FileDescriptorProto.fromPartial(...)`. As far as I
can tell this call doesn't do much in practice, because the data passed
to it at code generation time was already passed through
`FileDescriptorProto.fromPartial(fileDesc)` and I _think_ this results
in the exact same value, making the `fromPartial` call in the generated
code basically a no-op. You might want to double-check this before
merging because this change isn't hidden behind the
`outputSchemaAsConst` option.

## Considerations

### `satisfies` operator

The `satisfies ProtoMetadata` part of the declaration isn't strictly
necessary as the exported value should match the `ProtoMetadata`
interface anyway.
However, it can act as a hint to developers that the export conforms to
the `ProtoMetadata` type instead of being completely independent.
It also acts as a kind of sanity check when type checking, making sure
the generated type matches our expectations.

The `satisfies` operator is only supported since TypeScript 4.9, so code
generated using this option will not work on older TS versions.

### type complexity

When declaring `protoMetadata` as const, the type definition blows up _a
lot_, because the whole value is now also encoded on the type level.
This allows for advanced use cases (like typesafe reflecton), but could
potentially slow down the compiler when dealing with large schemas.
It also increases type declaration file sizes when the typescript files
when the resulting `.ts` files are compiled to `.js` and `.d.ts` files.
  • Loading branch information
cmd-johnson authored Sep 4, 2024
1 parent 8ccb459 commit 4cc1a1e
Show file tree
Hide file tree
Showing 19 changed files with 6,158 additions and 36 deletions.
6 changes: 5 additions & 1 deletion README.markdown
Original file line number Diff line number Diff line change
Expand Up @@ -454,7 +454,11 @@ Generated code will be placed in the Gradle build directory.

- With `--ts_proto_opt=annotateFilesWithVersion=false`, the generated files will not contain the versions of `protoc` and `ts-proto` used to generate the file. This option is normally set to `true`, such that files list the versions used.

- With `--ts_proto_opt=outputSchema=true`, meta typings will be generated that can later be used in other code generators. If outputSchema is instead specified to be `no-file-descriptor` then we do not include the file descriptor in the generated schema. This is useful if you are trying to minimize the size of the generated schema.
- With `--ts_proto_opt=outputSchema=true`, meta typings will be generated that can later be used in other code generators.

- With `--ts_proto_opt=outputSchema=no-file-descriptor`, meta typings will be generated, but we do not include the file descriptor in the generated schema. This is useful if you are trying to minimize the size of the generated schema.

- With `--ts_proto_opt=outputSchema=const`, meta typings will be generated `as const`, allowing type-safe access to all its properties. (only works with TypeScript 4.9 and up, because it also uses the [`satisfies`](https://www.typescriptlang.org/docs/handbook/release-notes/typescript-4-9.html#the-satisfies-operator) operator). Can be combined with the `no-file-descriptor` option (`outputSchema=const,outputSchema=no-file-descriptor`) to not include the file descriptor in the generated schema.

- With `--ts_proto_opt=outputTypeAnnotations=true`, each message will be given a `$type` field containing its fully-qualified name. You can use `--ts_proto_opt=outputTypeAnnotations=static-only` to omit it from the `interface` declaration, or `--ts_proto_opt=outputTypeAnnotations=optional` to make it an optional property on the `interface` definition. The latter option may be useful if you want to use the `$type` field for runtime type checking on responses from a server.

Expand Down
Loading

0 comments on commit 4cc1a1e

Please sign in to comment.