protobuf-ts icon indicating copy to clipboard operation
protobuf-ts copied to clipboard

PartialMessage<T> of message with oneof field requires that oneof field be non-partial

Open jcready opened this issue 2 years ago • 2 comments

Calling .create() on a MessageType instance allows partial oneof fields at runtime, but at compile-time they are required to be non-partial. TS Playground

// pulled from https://github.com/timostamm/protobuf-ts/blob/master/packages/runtime/src/message-type-contract.ts
export type PartialMessage<T extends object> = {
    [K in keyof T]?: PartialField<T[K]>
}; type PartialField<T> =
     T extends (Date | Uint8Array | bigint | boolean | string | number) ? T
   : T extends Array<infer U> ? Array<PartialField<U>>
   : T extends ReadonlyArray<infer U> ? ReadonlyArray<PartialField<U>>
   : T extends { oneofKind: string } ? T
   : T extends { oneofKind: undefined } ? T
   : T extends object ? PartialMessage<T>
   : T ;

// example
interface Bar {
    arr: number[];
    other?: number;
}

interface Foo {
    args: {
        oneofKind: "bar";
        bar: Bar;
    } | {
        oneofKind: undefined;
    }
}

function create(i: PartialMessage<Foo>) {}

create({
    args: {
        oneofKind: 'bar',
        bar: { // ts-2741: Property 'arr' is missing in type '{ other: number; }' but required in type 'Bar'.
            other: 1,
        }
    }
})

I don't think there's actually a way to get this working given the existing types. This is another instance where the updated oneof representation would make this trivial: : T extends { kind: string; value: infer U } ? { kind: T['kind']; value: PartialField<U> }.

jcready avatar Oct 26 '22 13:10 jcready

Good call. Would you like to open a PR against the 3.x branch?

timostamm avatar Oct 27 '22 08:10 timostamm

I believe I have a solution that can work for the existing v2 representation. I know this PartialMessage is depended upon heavily so it's possible I'm not properly covering some case.

type SetOneof = { oneofKind: string };
type SetOneofKind<T extends SetOneof> = T extends { oneofKind: string & keyof T }
    ? T['oneofKind']
    : never;

type PartialOneofValueMap<T extends SetOneof, K extends SetOneofKind<T> = SetOneofKind<T>> = {
    [k in K]: { oneofKind: k } & {
        [p in k]: T extends { oneofKind: k } ? PartialField<T[k]> : never;
    };
};
type PartialOneof<
    T extends SetOneof,
    K extends SetOneofKind<T> = SetOneofKind<T>,
    M extends PartialOneofValueMap<T, K> = PartialOneofValueMap<T, K>
> = T extends { oneofKind: keyof M } ? M[keyof M] : { oneofKind: undefined };

export type PartialMessage<T extends object> = {
    [K in keyof T]?: PartialField<T[K]>
}; type PartialField<T> =
     T extends (Date | Uint8Array | bigint | boolean | string | number) ? T
   : T extends Array<infer U> ? Array<PartialField<U>>
   : T extends ReadonlyArray<infer U> ? ReadonlyArray<PartialField<U>>
   : T extends { oneofKind: string & keyof T } ? PartialOneof<T>
   : T extends { oneofKind: undefined } ? T
   : T extends object ? PartialMessage<T>
   : T ;

jcready avatar Nov 11 '22 15:11 jcready