diff --git a/README.markdown b/README.markdown index a6834d21a..21a53dd14 100644 --- a/README.markdown +++ b/README.markdown @@ -432,7 +432,7 @@ Generated code will be placed in the Gradle build directory. - With `--ts_proto_opt=outputSchema=true`, meta typings will be generated that can later be used in other code generators. -- 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. +- 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. - With `--ts_proto_opt=outputTypeRegistry=true`, the type registry will be generated that can be used to resolve message types by fully-qualified name. Also, each message will be given a `$type` field containing its fully-qualified name. diff --git a/integration/optional-type-definitions/parameters.txt b/integration/optional-type-definitions/parameters.txt new file mode 100644 index 000000000..c2e04e81c --- /dev/null +++ b/integration/optional-type-definitions/parameters.txt @@ -0,0 +1 @@ +outputTypeAnnotations=optional diff --git a/integration/optional-type-definitions/simple.bin b/integration/optional-type-definitions/simple.bin new file mode 100644 index 000000000..02c48b052 Binary files /dev/null and b/integration/optional-type-definitions/simple.bin differ diff --git a/integration/optional-type-definitions/simple.proto b/integration/optional-type-definitions/simple.proto new file mode 100644 index 000000000..3d8dbdf88 --- /dev/null +++ b/integration/optional-type-definitions/simple.proto @@ -0,0 +1,23 @@ +// Adding a comment to the syntax will become the first +// comment in the output source file. +syntax = "proto3"; + +package simple; + +// This comment is separated by a blank non-comment line, and will detach from +// the following comment on the message Simple. + +/** Example comment on the Simple message */ +message Simple { + // Name field + string name = 1 [deprecated = true]; + /** Age field */ + int32 age = 2 [deprecated = true]; + Child child = 3 [deprecated = true]; // This comment will also attach; + string test_field = 4 [deprecated = true]; + string test_not_deprecated = 5 [deprecated = false]; +} + +message Child { + string name = 1; +} diff --git a/integration/optional-type-definitions/simple.ts b/integration/optional-type-definitions/simple.ts new file mode 100644 index 000000000..9e5c489be --- /dev/null +++ b/integration/optional-type-definitions/simple.ts @@ -0,0 +1,239 @@ +/* eslint-disable */ +import * as _m0 from "protobufjs/minimal"; + +export const protobufPackage = "simple"; + +/** + * Adding a comment to the syntax will become the first + * comment in the output source file. + */ + +/** Example comment on the Simple message */ +export interface Simple { + $type?: "simple.Simple"; + /** + * Name field + * + * @deprecated + */ + name: string; + /** + * Age field + * + * @deprecated + */ + age: number; + /** + * This comment will also attach; + * + * @deprecated + */ + child: + | Child + | undefined; + /** @deprecated */ + testField: string; + testNotDeprecated: string; +} + +export interface Child { + $type?: "simple.Child"; + name: string; +} + +function createBaseSimple(): Simple { + return { $type: "simple.Simple", name: "", age: 0, child: undefined, testField: "", testNotDeprecated: "" }; +} + +export const Simple = { + $type: "simple.Simple" as const, + + encode(message: Simple, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name); + } + if (message.age !== 0) { + writer.uint32(16).int32(message.age); + } + if (message.child !== undefined) { + Child.encode(message.child, writer.uint32(26).fork()).ldelim(); + } + if (message.testField !== "") { + writer.uint32(34).string(message.testField); + } + if (message.testNotDeprecated !== "") { + writer.uint32(42).string(message.testNotDeprecated); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): Simple { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseSimple(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.name = reader.string(); + continue; + case 2: + if (tag !== 16) { + break; + } + + message.age = reader.int32(); + continue; + case 3: + if (tag !== 26) { + break; + } + + message.child = Child.decode(reader, reader.uint32()); + continue; + case 4: + if (tag !== 34) { + break; + } + + message.testField = reader.string(); + continue; + case 5: + if (tag !== 42) { + break; + } + + message.testNotDeprecated = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): Simple { + return { + $type: Simple.$type, + name: isSet(object.name) ? globalThis.String(object.name) : "", + age: isSet(object.age) ? globalThis.Number(object.age) : 0, + child: isSet(object.child) ? Child.fromJSON(object.child) : undefined, + testField: isSet(object.testField) ? globalThis.String(object.testField) : "", + testNotDeprecated: isSet(object.testNotDeprecated) ? globalThis.String(object.testNotDeprecated) : "", + }; + }, + + toJSON(message: Simple): unknown { + const obj: any = {}; + if (message.name !== "") { + obj.name = message.name; + } + if (message.age !== 0) { + obj.age = Math.round(message.age); + } + if (message.child !== undefined) { + obj.child = Child.toJSON(message.child); + } + if (message.testField !== "") { + obj.testField = message.testField; + } + if (message.testNotDeprecated !== "") { + obj.testNotDeprecated = message.testNotDeprecated; + } + return obj; + }, + + create, I>>(base?: I): Simple { + return Simple.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Simple { + const message = createBaseSimple(); + message.name = object.name ?? ""; + message.age = object.age ?? 0; + message.child = (object.child !== undefined && object.child !== null) ? Child.fromPartial(object.child) : undefined; + message.testField = object.testField ?? ""; + message.testNotDeprecated = object.testNotDeprecated ?? ""; + return message; + }, +}; + +function createBaseChild(): Child { + return { $type: "simple.Child", name: "" }; +} + +export const Child = { + $type: "simple.Child" as const, + + encode(message: Child, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { + if (message.name !== "") { + writer.uint32(10).string(message.name); + } + return writer; + }, + + decode(input: _m0.Reader | Uint8Array, length?: number): Child { + const reader = input instanceof _m0.Reader ? input : _m0.Reader.create(input); + let end = length === undefined ? reader.len : reader.pos + length; + const message = createBaseChild(); + while (reader.pos < end) { + const tag = reader.uint32(); + switch (tag >>> 3) { + case 1: + if (tag !== 10) { + break; + } + + message.name = reader.string(); + continue; + } + if ((tag & 7) === 4 || tag === 0) { + break; + } + reader.skipType(tag & 7); + } + return message; + }, + + fromJSON(object: any): Child { + return { $type: Child.$type, name: isSet(object.name) ? globalThis.String(object.name) : "" }; + }, + + toJSON(message: Child): unknown { + const obj: any = {}; + if (message.name !== "") { + obj.name = message.name; + } + return obj; + }, + + create, I>>(base?: I): Child { + return Child.fromPartial(base ?? ({} as any)); + }, + fromPartial, I>>(object: I): Child { + const message = createBaseChild(); + message.name = object.name ?? ""; + return message; + }, +}; + +type Builtin = Date | Function | Uint8Array | string | number | boolean | undefined; + +export type DeepPartial = T extends Builtin ? T + : T extends globalThis.Array ? globalThis.Array> + : T extends ReadonlyArray ? ReadonlyArray> + : T extends {} ? { [K in Exclude]?: DeepPartial } + : Partial; + +type KeysOfUnion = T extends T ? keyof T : never; +export type Exact = P extends Builtin ? P + : P & { [K in keyof P]: Exact } & { [K in Exclude | "$type">]: never }; + +function isSet(value: any): boolean { + return value !== null && value !== undefined; +} diff --git a/src/main.ts b/src/main.ts index cd333e1f0..85cbd16f9 100644 --- a/src/main.ts +++ b/src/main.ts @@ -956,7 +956,7 @@ function generateInterfaceDeclaration( chunks.push(code`export interface ${def(fullName)} {`); if (addTypeToMessages(options)) { - chunks.push(code`$type: '${fullTypeName}',`); + chunks.push(code`$type${options.outputTypeAnnotations === "optional" ? "?" : ""}: '${fullTypeName}',`); } // When oneof=unions, we generate a single property with an ADT per `oneof` clause. diff --git a/src/options.ts b/src/options.ts index 11d849d3c..1bf426bbd 100644 --- a/src/options.ts +++ b/src/options.ts @@ -55,7 +55,7 @@ export type Options = { outputEncodeMethods: true | false | "encode-only" | "decode-only" | "encode-no-creation"; outputJsonMethods: true | false | "to-only" | "from-only"; outputPartialMethods: boolean; - outputTypeAnnotations: boolean | "static-only"; + outputTypeAnnotations: boolean | "static-only" | "optional"; outputTypeRegistry: boolean; stringEnums: boolean; constEnums: boolean;