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

`PlainMessage<Empty>` is `{}`, meaning any non-nullish value

Open haines opened this issue 1 year ago • 1 comments

I think it's a little bit unfortunate that PlainMessage<Empty> (or PlainMessage of any user-defined empty message) reduces to {}, because to quote typescript-eslint

Don't use {} as a type. {} actually means "any non-nullish value".

  • If you want a type meaning "any object", you probably want object instead.
  • If you want a type meaning "any value", you probably want unknown instead.
  • If you want a type meaning "empty object", you probably want Record<string, never> instead.
  • If you really want a type meaning "any non-nullish value", you probably want NonNullable<unknown> instead.

The problem is that TypeScript won't prevent you from doing nonsensical things like

const empty: PlainMessage<Empty> = 42;

I don't think it's really a bug, given that you can also do stuff like new Empty(42) without a runtime error, but it might be nicer if PlainMessage<Empty> produced Record<string, never> instead?

haines avatar Jun 14 '24 09:06 haines

The lint rule is neat, I only became aware of this behavior because of it. But I didn't connect the dots to PlainMessage - great find.

I think this should avoid the issue and map to empty object:

type ReallyPlainMessage<T extends Message<T>> = keyof PlainMessage<T> extends never ? Record<string, never> : PlainMessage<T>;

I am not sure that it would be wise to make this change, though. It's very possible that it breaks something.

timostamm avatar Jun 18 '24 15:06 timostamm

This is fixed with version 2:

import type { Empty } from "./gen/example_pb";

const empty: Empty = 42; // TS2322: Type number is not assignable to type Message<"Empty">

timostamm avatar Aug 01 '24 14:08 timostamm

Awesome, thanks @timostamm!

haines avatar Aug 01 '24 14:08 haines