From 9c53d7efb0252c7ea0af85a5d161ff94bcd69760 Mon Sep 17 00:00:00 2001 From: Daniel Lytkin Date: Thu, 30 Dec 2021 11:05:59 +0700 Subject: [PATCH] feat: add an option to disable Exact types (#456) --- README.markdown | 4 + integration/use-exact-types-false/foo.bin | Bin 0 -> 256 bytes integration/use-exact-types-false/foo.proto | 8 ++ integration/use-exact-types-false/foo.ts | 87 ++++++++++++++++++ .../use-exact-types-false/parameters.txt | 1 + src/main.ts | 17 +++- src/options.ts | 2 + tests/options-test.ts | 1 + 8 files changed, 115 insertions(+), 5 deletions(-) create mode 100644 integration/use-exact-types-false/foo.bin create mode 100644 integration/use-exact-types-false/foo.proto create mode 100644 integration/use-exact-types-false/foo.ts create mode 100755 integration/use-exact-types-false/parameters.txt diff --git a/README.markdown b/README.markdown index 9f72ffa8e..2703dff59 100644 --- a/README.markdown +++ b/README.markdown @@ -350,6 +350,10 @@ Generated code will be placed in the Gradle build directory. - With `--ts_proto_opt=enumsAsLiterals=true`, the generated enum types will be enum-ish object with `as const`. +- With `--ts_proto_opt=useExactTypes=false`, the generated `fromPartial` method will not use Exact types. + + The default behavior is `useExactTypes=true`, which makes `fromPartial` use Exact type for its argument to make TypeScript reject any unknown properties. + ### Only Types If you're looking for `ts-proto` to generate only types for your Protobuf types then passing all three of `outputEncodeMethods`, `outputJsonMethods`, and `outputClientImpl` as `false` is probably what you want, i.e.: diff --git a/integration/use-exact-types-false/foo.bin b/integration/use-exact-types-false/foo.bin new file mode 100644 index 0000000000000000000000000000000000000000..b98932d4bcbb845eb9f7c8d9a7734fe233a59d53 GIT binary patch literal 256 zcmYL^F%H5o5CrdhwsAI=5{@RKLaCsk;SUs)`~V7CqlhZc;dML%8%XGGc6W+HbxUb~ zAJQYOLy&gWaZlrm@y}KAq~Ae~DQOG2UWSf0ysggV{3c^(rer)l8D1^Xf)T(`g-FX~ zR18G`#*$iFdO$3(iVZbt>> 3) { + case 1: + message.bar = reader.string(); + break; + case 2: + message.baz = reader.string(); + break; + default: + reader.skipType(tag & 7); + break; + } + } + return message; + }, + + fromJSON(object: any): Foo { + const message = createBaseFoo(); + message.bar = object.bar !== undefined && object.bar !== null ? String(object.bar) : ''; + message.baz = object.baz !== undefined && object.baz !== null ? String(object.baz) : ''; + return message; + }, + + toJSON(message: Foo): unknown { + const obj: any = {}; + message.bar !== undefined && (obj.bar = message.bar); + message.baz !== undefined && (obj.baz = message.baz); + return obj; + }, + + fromPartial(object: DeepPartial): Foo { + const message = createBaseFoo(); + message.bar = object.bar ?? ''; + message.baz = object.baz ?? ''; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin + ? T + : T extends Array + ? Array> + : T extends ReadonlyArray + ? ReadonlyArray> + : T extends {} + ? { [K in keyof T]?: DeepPartial } + : Partial; + +// If you get a compile-error about 'Constructor and ... have no overlap', +// add '--ts_proto_opt=esModuleInterop=true' as a flag when calling 'protoc'. +if (util.Long !== Long) { + util.Long = Long as any; + configure(); +} diff --git a/integration/use-exact-types-false/parameters.txt b/integration/use-exact-types-false/parameters.txt new file mode 100755 index 000000000..97bb08ab5 --- /dev/null +++ b/integration/use-exact-types-false/parameters.txt @@ -0,0 +1 @@ +useExactTypes=false diff --git a/src/main.ts b/src/main.ts index d82aa592c..6dfe84e89 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1249,14 +1249,21 @@ function generateToJson(ctx: Context, fullName: string, messageDesc: DescriptorP function generateFromPartial(ctx: Context, fullName: string, messageDesc: DescriptorProto): Code { const { options, utils, typeMap } = ctx; const chunks: Code[] = []; - const Timestamp = impProto(options, 'google/protobuf/timestamp', 'Timestamp'); // create the basic function declaration const paramName = messageDesc.field.length > 0 ? 'object' : '_'; - chunks.push(code` - fromPartial, I>>(${paramName}: I): ${fullName} { - const message = createBase${fullName}(); - `); + + if (ctx.options.useExactTypes) { + chunks.push(code` + fromPartial, I>>(${paramName}: I): ${fullName} { + `); + } else { + chunks.push(code` + fromPartial(${paramName}: ${utils.DeepPartial}<${fullName}>): ${fullName} { + `); + } + + chunks.push(code`const message = createBase${fullName}();`); // add a check for each incoming field messageDesc.field.forEach((field) => { diff --git a/src/options.ts b/src/options.ts index f696c452b..380718f13 100644 --- a/src/options.ts +++ b/src/options.ts @@ -58,6 +58,7 @@ export type Options = { // An alias of !output onlyTypes: boolean; emitImportedFiles: boolean; + useExactTypes: boolean; }; export function defaultOptions(): Options { @@ -90,6 +91,7 @@ export function defaultOptions(): Options { outputSchema: false, onlyTypes: false, emitImportedFiles: true, + useExactTypes: true, }; } diff --git a/tests/options-test.ts b/tests/options-test.ts index 2d35bd25c..6f82a0818 100644 --- a/tests/options-test.ts +++ b/tests/options-test.ts @@ -34,6 +34,7 @@ describe('options', () => { "stringEnums": false, "unrecognizedEnum": true, "useDate": "timestamp", + "useExactTypes": true, "useOptionals": "none", } `);