diff --git a/integration/use-readonly-types/google/protobuf/field_mask.ts b/integration/use-readonly-types/google/protobuf/field_mask.ts index 00619768a..2d09ba284 100644 --- a/integration/use-readonly-types/google/protobuf/field_mask.ts +++ b/integration/use-readonly-types/google/protobuf/field_mask.ts @@ -275,6 +275,8 @@ type Builtin = Date | Function | Uint8Array | string | number | boolean | undefi export type DeepPartial = T extends Builtin ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> + : T extends { readonly $case: string } + ? { [K in keyof Omit]?: DeepPartial } & { readonly $case: T["$case"] } : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; diff --git a/integration/use-readonly-types/google/protobuf/struct.ts b/integration/use-readonly-types/google/protobuf/struct.ts index 4d7127953..aa957f579 100644 --- a/integration/use-readonly-types/google/protobuf/struct.ts +++ b/integration/use-readonly-types/google/protobuf/struct.ts @@ -66,28 +66,13 @@ export interface Struct_FieldsEntry { * The JSON representation for `Value` is JSON value. */ export interface Value { - /** Represents a null value. */ - readonly nullValue: - | NullValue - | undefined; - /** Represents a double value. */ - readonly numberValue: - | number - | undefined; - /** Represents a string value. */ - readonly stringValue: - | string - | undefined; - /** Represents a boolean value. */ - readonly boolValue: - | boolean - | undefined; - /** Represents a structured value. */ - readonly structValue: - | { readonly [key: string]: any } - | undefined; - /** Represents a repeated `Value`. */ - readonly listValue: ReadonlyArray | undefined; + readonly kind?: + | { readonly $case: "nullValue"; readonly nullValue: NullValue } + | { readonly $case: "numberValue"; readonly numberValue: number } + | { readonly $case: "stringValue"; readonly stringValue: string } + | { readonly $case: "boolValue"; readonly boolValue: boolean } + | { readonly $case: "structValue"; readonly structValue: { readonly [key: string]: any } | undefined } + | { readonly $case: "listValue"; readonly listValue: ReadonlyArray | undefined }; } /** @@ -246,35 +231,28 @@ export const Struct_FieldsEntry = { }; function createBaseValue(): Value { - return { - nullValue: undefined, - numberValue: undefined, - stringValue: undefined, - boolValue: undefined, - structValue: undefined, - listValue: undefined, - }; + return { kind: undefined }; } export const Value = { encode(message: Value, writer: _m0.Writer = _m0.Writer.create()): _m0.Writer { - if (message.nullValue !== undefined) { - writer.uint32(8).int32(message.nullValue); + if (message.kind?.$case === "nullValue") { + writer.uint32(8).int32(message.kind.nullValue); } - if (message.numberValue !== undefined) { - writer.uint32(17).double(message.numberValue); + if (message.kind?.$case === "numberValue") { + writer.uint32(17).double(message.kind.numberValue); } - if (message.stringValue !== undefined) { - writer.uint32(26).string(message.stringValue); + if (message.kind?.$case === "stringValue") { + writer.uint32(26).string(message.kind.stringValue); } - if (message.boolValue !== undefined) { - writer.uint32(32).bool(message.boolValue); + if (message.kind?.$case === "boolValue") { + writer.uint32(32).bool(message.kind.boolValue); } - if (message.structValue !== undefined) { - Struct.encode(Struct.wrap(message.structValue), writer.uint32(42).fork()).ldelim(); + if (message.kind?.$case === "structValue") { + Struct.encode(Struct.wrap(message.kind.structValue), writer.uint32(42).fork()).ldelim(); } - if (message.listValue !== undefined) { - ListValue.encode(ListValue.wrap(message.listValue), writer.uint32(50).fork()).ldelim(); + if (message.kind?.$case === "listValue") { + ListValue.encode(ListValue.wrap(message.kind.listValue), writer.uint32(50).fork()).ldelim(); } return writer; }, @@ -287,22 +265,22 @@ export const Value = { const tag = reader.uint32(); switch (tag >>> 3) { case 1: - message.nullValue = reader.int32() as any; + message.kind = { $case: "nullValue", nullValue: reader.int32() as any }; break; case 2: - message.numberValue = reader.double(); + message.kind = { $case: "numberValue", numberValue: reader.double() }; break; case 3: - message.stringValue = reader.string(); + message.kind = { $case: "stringValue", stringValue: reader.string() }; break; case 4: - message.boolValue = reader.bool(); + message.kind = { $case: "boolValue", boolValue: reader.bool() }; break; case 5: - message.structValue = Struct.unwrap(Struct.decode(reader, reader.uint32())); + message.kind = { $case: "structValue", structValue: Struct.unwrap(Struct.decode(reader, reader.uint32())) }; break; case 6: - message.listValue = ListValue.unwrap(ListValue.decode(reader, reader.uint32())); + message.kind = { $case: "listValue", listValue: ListValue.unwrap(ListValue.decode(reader, reader.uint32())) }; break; default: reader.skipType(tag & 7); @@ -314,35 +292,66 @@ export const Value = { fromJSON(object: any): Value { return { - nullValue: isSet(object.nullValue) ? nullValueFromJSON(object.nullValue) : undefined, - numberValue: isSet(object.numberValue) ? Number(object.numberValue) : undefined, - stringValue: isSet(object.stringValue) ? String(object.stringValue) : undefined, - boolValue: isSet(object.boolValue) ? Boolean(object.boolValue) : undefined, - structValue: isObject(object.structValue) ? object.structValue : undefined, - listValue: Array.isArray(object.listValue) ? [...object.listValue] : undefined, + kind: isSet(object.nullValue) + ? { $case: "nullValue", nullValue: nullValueFromJSON(object.nullValue) } + : isSet(object.numberValue) + ? { $case: "numberValue", numberValue: Number(object.numberValue) } + : isSet(object.stringValue) + ? { $case: "stringValue", stringValue: String(object.stringValue) } + : isSet(object.boolValue) + ? { $case: "boolValue", boolValue: Boolean(object.boolValue) } + : isSet(object.structValue) + ? { $case: "structValue", structValue: object.structValue } + : isSet(object.listValue) + ? { $case: "listValue", listValue: [...object.listValue] } + : undefined, }; }, toJSON(message: Value): unknown { const obj: any = {}; - message.nullValue !== undefined && - (obj.nullValue = message.nullValue !== undefined ? nullValueToJSON(message.nullValue) : undefined); - message.numberValue !== undefined && (obj.numberValue = message.numberValue); - message.stringValue !== undefined && (obj.stringValue = message.stringValue); - message.boolValue !== undefined && (obj.boolValue = message.boolValue); - message.structValue !== undefined && (obj.structValue = message.structValue); - message.listValue !== undefined && (obj.listValue = message.listValue); + message.kind?.$case === "nullValue" && + (obj.nullValue = message.kind?.nullValue !== undefined ? nullValueToJSON(message.kind?.nullValue) : undefined); + message.kind?.$case === "numberValue" && (obj.numberValue = message.kind?.numberValue); + message.kind?.$case === "stringValue" && (obj.stringValue = message.kind?.stringValue); + message.kind?.$case === "boolValue" && (obj.boolValue = message.kind?.boolValue); + message.kind?.$case === "structValue" && (obj.structValue = message.kind?.structValue); + message.kind?.$case === "listValue" && (obj.listValue = message.kind?.listValue); return obj; }, fromPartial, I>>(object: I): Value { const message = createBaseValue() as any; - message.nullValue = object.nullValue ?? undefined; - message.numberValue = object.numberValue ?? undefined; - message.stringValue = object.stringValue ?? undefined; - message.boolValue = object.boolValue ?? undefined; - message.structValue = object.structValue ?? undefined; - message.listValue = object.listValue ?? undefined; + if (object.kind?.$case === "nullValue" && object.kind?.nullValue !== undefined && object.kind?.nullValue !== null) { + message.kind = { $case: "nullValue", nullValue: object.kind.nullValue }; + } + if ( + object.kind?.$case === "numberValue" && + object.kind?.numberValue !== undefined && + object.kind?.numberValue !== null + ) { + message.kind = { $case: "numberValue", numberValue: object.kind.numberValue }; + } + if ( + object.kind?.$case === "stringValue" && + object.kind?.stringValue !== undefined && + object.kind?.stringValue !== null + ) { + message.kind = { $case: "stringValue", stringValue: object.kind.stringValue }; + } + if (object.kind?.$case === "boolValue" && object.kind?.boolValue !== undefined && object.kind?.boolValue !== null) { + message.kind = { $case: "boolValue", boolValue: object.kind.boolValue }; + } + if ( + object.kind?.$case === "structValue" && + object.kind?.structValue !== undefined && + object.kind?.structValue !== null + ) { + message.kind = { $case: "structValue", structValue: object.kind.structValue }; + } + if (object.kind?.$case === "listValue" && object.kind?.listValue !== undefined && object.kind?.listValue !== null) { + message.kind = { $case: "listValue", listValue: object.kind.listValue }; + } return message; }, @@ -350,17 +359,17 @@ export const Value = { const result = createBaseValue() as any; if (value === null) { - result.nullValue = NullValue.NULL_VALUE; + result.kind = { $case: "nullValue", nullValue: NullValue.NULL_VALUE }; } else if (typeof value === "boolean") { - result.boolValue = value; + result.kind = { $case: "boolValue", boolValue: value }; } else if (typeof value === "number") { - result.numberValue = value; + result.kind = { $case: "numberValue", numberValue: value }; } else if (typeof value === "string") { - result.stringValue = value; + result.kind = { $case: "stringValue", stringValue: value }; } else if (Array.isArray(value)) { - result.listValue = value; + result.kind = { $case: "listValue", listValue: value }; } else if (typeof value === "object") { - result.structValue = value; + result.kind = { $case: "structValue", structValue: value }; } else if (typeof value !== "undefined") { throw new Error("Unsupported any value type: " + typeof value); } @@ -369,20 +378,21 @@ export const Value = { }, unwrap(message: Value): string | number | boolean | Object | null | Array | undefined { - if (message?.stringValue !== undefined) { - return message.stringValue; - } else if (message?.numberValue !== undefined) { - return message.numberValue; - } else if (message?.boolValue !== undefined) { - return message.boolValue; - } else if (message?.structValue !== undefined) { - return message.structValue; - } else if (message?.listValue !== undefined) { - return message.listValue; - } else if (message?.nullValue !== undefined) { + if (message.kind?.$case === "nullValue") { return null; + } else if (message.kind?.$case === "numberValue") { + return message.kind?.numberValue; + } else if (message.kind?.$case === "stringValue") { + return message.kind?.stringValue; + } else if (message.kind?.$case === "boolValue") { + return message.kind?.boolValue; + } else if (message.kind?.$case === "structValue") { + return message.kind?.structValue; + } else if (message.kind?.$case === "listValue") { + return message.kind?.listValue; + } else { + return undefined; } - return undefined; }, }; @@ -453,6 +463,8 @@ type Builtin = Date | Function | Uint8Array | string | number | boolean | undefi export type DeepPartial = T extends Builtin ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> + : T extends { readonly $case: string } + ? { [K in keyof Omit]?: DeepPartial } & { readonly $case: T["$case"] } : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; diff --git a/integration/use-readonly-types/parameters.txt b/integration/use-readonly-types/parameters.txt index 9cea675ca..419d64c24 100644 --- a/integration/use-readonly-types/parameters.txt +++ b/integration/use-readonly-types/parameters.txt @@ -1 +1 @@ -useReadonlyTypes=true +useReadonlyTypes=true,oneof=unions diff --git a/integration/use-readonly-types/use-readonly-types-test.ts b/integration/use-readonly-types/use-readonly-types-test.ts index cee1ab067..a6e823b1c 100644 --- a/integration/use-readonly-types/use-readonly-types-test.ts +++ b/integration/use-readonly-types/use-readonly-types-test.ts @@ -12,6 +12,7 @@ describe("use-readonly-types", () => { fieldMask: ["the", "mask"], listValue: ["the", "list"], structValue: { the: "struct" }, + oneOfValue: { $case: "theStringValue", theStringValue: "theString" }, }; const jsonFromObject = Entity.toJSON(m); const entityFromJSON = Entity.fromJSON(jsonFromObject); @@ -28,6 +29,7 @@ describe("use-readonly-types", () => { fieldMask: m.fieldMask, listValue: m.listValue, structValue: m.structValue, + oneOfValue: m.oneOfValue, }); const jsonFromDecoded = Entity.toJSON(decoded); expect(jsonFromDecoded).toEqual(jsonFromObject); diff --git a/integration/use-readonly-types/use-readonly-types.bin b/integration/use-readonly-types/use-readonly-types.bin index a37b0b21e..6dbaae962 100644 Binary files a/integration/use-readonly-types/use-readonly-types.bin and b/integration/use-readonly-types/use-readonly-types.bin differ diff --git a/integration/use-readonly-types/use-readonly-types.proto b/integration/use-readonly-types/use-readonly-types.proto index a3a34809c..fdac39962 100644 --- a/integration/use-readonly-types/use-readonly-types.proto +++ b/integration/use-readonly-types/use-readonly-types.proto @@ -14,6 +14,10 @@ message Entity { google.protobuf.FieldMask fieldMask = 8; google.protobuf.ListValue listValue = 9; google.protobuf.Struct structValue = 10; + oneof oneOfValue { + string theStringValue = 11; + int32 theIntValue = 12; + } } message SubEntity { diff --git a/integration/use-readonly-types/use-readonly-types.ts b/integration/use-readonly-types/use-readonly-types.ts index 0a84a927d..7df6f03d1 100644 --- a/integration/use-readonly-types/use-readonly-types.ts +++ b/integration/use-readonly-types/use-readonly-types.ts @@ -16,6 +16,10 @@ export interface Entity { readonly fieldMask: readonly string[] | undefined; readonly listValue: ReadonlyArray | undefined; readonly structValue: { readonly [key: string]: any } | undefined; + readonly oneOfValue?: { readonly $case: "theStringValue"; readonly theStringValue: string } | { + readonly $case: "theIntValue"; + readonly theIntValue: number; + }; } export interface SubEntity { @@ -34,6 +38,7 @@ function createBaseEntity(): Entity { fieldMask: undefined, listValue: undefined, structValue: undefined, + oneOfValue: undefined, }; } @@ -71,6 +76,12 @@ export const Entity = { if (message.structValue !== undefined) { Struct.encode(Struct.wrap(message.structValue), writer.uint32(82).fork()).ldelim(); } + if (message.oneOfValue?.$case === "theStringValue") { + writer.uint32(90).string(message.oneOfValue.theStringValue); + } + if (message.oneOfValue?.$case === "theIntValue") { + writer.uint32(96).int32(message.oneOfValue.theIntValue); + } return writer; }, @@ -118,6 +129,12 @@ export const Entity = { case 10: message.structValue = Struct.unwrap(Struct.decode(reader, reader.uint32())); break; + case 11: + message.oneOfValue = { $case: "theStringValue", theStringValue: reader.string() }; + break; + case 12: + message.oneOfValue = { $case: "theIntValue", theIntValue: reader.int32() }; + break; default: reader.skipType(tag & 7); break; @@ -140,6 +157,11 @@ export const Entity = { fieldMask: isSet(object.fieldMask) ? FieldMask.unwrap(FieldMask.fromJSON(object.fieldMask)) : undefined, listValue: Array.isArray(object.listValue) ? [...object.listValue] : undefined, structValue: isObject(object.structValue) ? object.structValue : undefined, + oneOfValue: isSet(object.theStringValue) + ? { $case: "theStringValue", theStringValue: String(object.theStringValue) } + : isSet(object.theIntValue) + ? { $case: "theIntValue", theIntValue: Number(object.theIntValue) } + : undefined, }; }, @@ -168,6 +190,8 @@ export const Entity = { message.fieldMask !== undefined && (obj.fieldMask = FieldMask.toJSON(FieldMask.wrap(message.fieldMask))); message.listValue !== undefined && (obj.listValue = message.listValue); message.structValue !== undefined && (obj.structValue = message.structValue); + message.oneOfValue?.$case === "theStringValue" && (obj.theStringValue = message.oneOfValue?.theStringValue); + message.oneOfValue?.$case === "theIntValue" && (obj.theIntValue = Math.round(message.oneOfValue?.theIntValue)); return obj; }, @@ -185,6 +209,20 @@ export const Entity = { message.fieldMask = object.fieldMask ?? undefined; message.listValue = object.listValue ?? undefined; message.structValue = object.structValue ?? undefined; + if ( + object.oneOfValue?.$case === "theStringValue" && + object.oneOfValue?.theStringValue !== undefined && + object.oneOfValue?.theStringValue !== null + ) { + message.oneOfValue = { $case: "theStringValue", theStringValue: object.oneOfValue.theStringValue }; + } + if ( + object.oneOfValue?.$case === "theIntValue" && + object.oneOfValue?.theIntValue !== undefined && + object.oneOfValue?.theIntValue !== null + ) { + message.oneOfValue = { $case: "theIntValue", theIntValue: object.oneOfValue.theIntValue }; + } return message; }, }; @@ -240,6 +278,8 @@ type Builtin = Date | Function | Uint8Array | string | number | boolean | undefi export type DeepPartial = T extends Builtin ? T : T extends Array ? Array> : T extends ReadonlyArray ? ReadonlyArray> + : T extends { readonly $case: string } + ? { [K in keyof Omit]?: DeepPartial } & { readonly $case: T["$case"] } : T extends {} ? { [K in keyof T]?: DeepPartial } : Partial; diff --git a/src/main.ts b/src/main.ts index cc4863be7..f1e370660 100644 --- a/src/main.ts +++ b/src/main.ts @@ -489,8 +489,8 @@ function makeDeepPartial(options: Options, longs: ReturnType]?: DeepPartial } & { $case: T['$case'] } + : T extends { ${maybeReadonly(options)}$case: string } + ? { [K in keyof Omit]?: DeepPartial } & { ${maybeReadonly(options)}$case: T['$case'] } `; } @@ -751,7 +751,7 @@ function generateInterfaceDeclaration( const name = maybeSnakeToCamel(fieldDesc.name, options); const type = toTypeName(ctx, messageDesc, fieldDesc); const q = isOptionalProperty(fieldDesc, messageDesc.options, options) ? "?" : ""; - chunks.push(code`${ctx.options.useReadonlyTypes ? "readonly " : ""}${name}${q}: ${type}, `); + chunks.push(code`${maybeReadonly(options)}${name}${q}: ${type}, `); }); chunks.push(code`}`); @@ -770,13 +770,15 @@ function generateOneofProperty( fields.map((f) => { let fieldName = maybeSnakeToCamel(f.name, options); let typeName = toTypeName(ctx, messageDesc, f); - return code`{ $case: '${fieldName}', ${fieldName}: ${typeName} }`; + return code`{ ${maybeReadonly(options)}$case: '${fieldName}', ${maybeReadonly( + options + )}${fieldName}: ${typeName} }`; }), { on: " | " } ); const name = maybeSnakeToCamel(messageDesc.oneofDecl[oneofIndex].name, options); - return code`${name}?: ${unionType},`; + return code`${maybeReadonly(options)}${name}?: ${unionType},`; /* // Ideally we'd put the comments for each oneof field next to the anonymous @@ -876,7 +878,7 @@ function generateDecode(ctx: Context, fullName: string, messageDesc: DescriptorP let end = length === undefined ? reader.len : reader.pos + length; `); - chunks.push(code`const message = ${createBase}${options.useReadonlyTypes ? " as any" : ""};`); + chunks.push(code`const message = ${createBase}${maybeAsAny(options)};`); if (options.unknownFields) { chunks.push(code`(message as any)._unknownFields = {}`); @@ -1593,7 +1595,7 @@ function generateFromPartial(ctx: Context, fullName: string, messageDesc: Descri createBase = code`Object.create(${createBase}) as ${fullName}`; } - chunks.push(code`const message = ${createBase}${options.useReadonlyTypes ? " as any" : ""};`); + chunks.push(code`const message = ${createBase}${maybeAsAny(options)};`); // add a check for each incoming field messageDesc.field.forEach((field) => { @@ -1741,7 +1743,7 @@ function generateWrap(ctx: Context, fullProtoTypeName: string, fieldNames: Struc if (isAnyValueTypeName(fullProtoTypeName)) { if (ctx.options.oneof === OneofOption.UNIONS) { chunks.push(code`wrap(value: any): Value { - const result = createBaseValue()${ctx.options.useReadonlyTypes ? " as any" : ""}; + const result = createBaseValue()${maybeAsAny(ctx.options)}; if (value === null) { result.kind = {$case: '${fieldNames.nullValue}', ${fieldNames.nullValue}: NullValue.NULL_VALUE}; @@ -1763,7 +1765,7 @@ function generateWrap(ctx: Context, fullProtoTypeName: string, fieldNames: Struc }`); } else { chunks.push(code`wrap(value: any): Value { - const result = createBaseValue()${ctx.options.useReadonlyTypes ? " as any" : ""}; + const result = createBaseValue()${maybeAsAny(ctx.options)}; if (value === null) { result.${fieldNames.nullValue} = NullValue.NULL_VALUE; @@ -1790,7 +1792,7 @@ function generateWrap(ctx: Context, fullProtoTypeName: string, fieldNames: Struc chunks.push(code`wrap(value: ${ ctx.options.useReadonlyTypes ? "ReadonlyArray" : "Array" } | undefined): ListValue { - const result = createBaseListValue()${ctx.options.useReadonlyTypes ? " as any" : ""}; + const result = createBaseListValue()${maybeAsAny(ctx.options)}; result.values = value ?? []; @@ -1799,8 +1801,8 @@ function generateWrap(ctx: Context, fullProtoTypeName: string, fieldNames: Struc } if (isFieldMaskTypeName(fullProtoTypeName)) { - chunks.push(code`wrap(paths: ${ctx.options.useReadonlyTypes ? "readonly " : ""} string[]): FieldMask { - const result = createBaseFieldMask()${ctx.options.useReadonlyTypes ? " as any" : ""}; + chunks.push(code`wrap(paths: ${maybeReadonly(ctx.options)} string[]): FieldMask { + const result = createBaseFieldMask()${maybeAsAny(ctx.options)}; result.paths = paths; @@ -1892,3 +1894,11 @@ function maybeCastToNumber( return `Number(${variableName})`; } } + +function maybeReadonly(options: Options): string { + return options.useReadonlyTypes ? "readonly " : ""; +} + +function maybeAsAny(options: Options): string { + return options.useReadonlyTypes ? " as any" : ""; +}