Generate (sound) TypeScript and Flow declaration files#9
Conversation
This change adds Flow declaration types so that we can check the types
of the generated code.
Notably:
- Adds a dependency to flowgen and calls it during compilation, which
takes the `.d.ts` and creates a `.flow.js` based on it.
- Passes in the `--force-number` flag to pbjs, which causes it to not
emit numbers as possibly being `Long`, since this confuses Flow. This
only affects JSDoc declarations, not the generated code.
- Passes in the `--force-message` flag to pbjs. Otherwise, this emits
invalid types: it tries to create code like this:
```TypeScript
interface IMessage {
field?: T|null;
}
class Message implements IMessage {
field: T; // notice the lack of optionality / nullness.
...
}
```
This violates the Liskov Substitution Principle, since `Message`
cannot be passed to any place that accepts an `IMessage`, so
TypeScript (correctly) rejects this as bogus. With this flag, the
number of places that will accept an `IMessage` instead of a `Message`
are limited to constructors, where we do want to have partial messages
being passed in.
This only affects JSDoc declarations, not the generated code.
- Removes all `@implements` annotations from the JSDoc, which takes care
of the very last `IMessage`/`Message` confusion. This, together with
the above element take care of
protobufjs/protobuf.js#837
This is actually valid typescript, and you can pass |
There was a problem hiding this comment.
I'm not sure how I feel about this. There is no room for optional fields anymore inside our commands in ICommand, even though in protobufs all fields are optional. This is especially problematic for re-used messages such as File (maybe we should fix our protobufs).
For example, when you want to send a read command. You send a File message, you only fill the path field, like so (crosis):
channel.request({ read: { path: '.replit' } }), this will now fail because the read command under ICommand is now api.File|undefined instead of api.IFile|undefined, which means you have to do
channel.request({
read: {
path: '.replit',
type: api.File.Type.REGULAR,
content: new Uint8Array(),
toJSON: () => ({ /* implement toJSON?*/}),
},
});
or less foolish
const read = new api.File({ read: { path: '.replit' } });
channel.request({ read })
This might not be too bad but definitely cumbersome, and definitely requires a lot of code changes for us to get past typescript errors.
If ICommand used interfaces as well, this might be ok, but it doesn't, is there a way around that? Or do you think there's some way we can type this to support passing partial messages to channels https://github.com/replit/crosis/blob/v6/src/channel.ts#L123?
|
Also, added a boop label to the repo |
oh this is one of those cases where TypeScript is intentionally unsound D: see https://www.typescriptlang.org/play?#code/JYOwLgpgTgZghgYwgAgJIFkIGctwOYoDeAUMsjMBADYAmA-AFzJZhSh4A+IArlVQNzEAvsWIIqcHMkw58KYAFsADlQgKI4LGhm4CyEmQrUaTFmxB5kAXmQAiOLeGiY3EAjDAA9iGRg4Aa2wMbF0IAAp1WQImYKiIAEp9UmRI0IA6I1prZFcaCAoQCBp+ZAB6Ut8AC2AtGuQANzgqYBpyTyhtELkAGmQAI24wZFBG5taYdukugjSnMW8WFOmUG0KAdym4sPjBP0CsWNCI5Z35kCxPVTSqTzxjuJ2gA for the rationale behind why this should be invalid TypeScript Flow gets that right: https://flow.org/try/#0PQKgBAAgZgNg9gdzCYAoVBLAdgFwKYBOUAhgMZ5gCSAsngM53EDmFA3qmGFBnjACYB+AFxg6OAtiYAfLAFcYMANyoAvulIxiDMLQbMKGALYAHGHkN5cdKrsYsw7Tt158RYiViZgAvGABExH7KaqhQslikOBhwWGA4xADW9DT0dngAFBZ6LCIp2XgAlA4cYFlpAHTO-D5g4Xx43Fh4fIpgwMBxABYY1j1gAG7EMBh8XHAENqn6ADRgAEayOGDYg8OjUOM6UyzlquoxYqXbFL5NSLb66QXK8Ul0eWmZx9eopAdwZuXwTE-510A and the fact that it is a flow compilation error is the reason why this was needed: when I tried to add this to the flow-typed server, it didn't like this unsoundness and errored in all the types. changed the wording to say that the emitted code is "unsound" instead of "invalid" to avoid confusion. |
all fields in
oh, this is a bit trickier, but i'll take a look. if a simple |
This still emits completely sound types (needed for Flow, since TypeScript is intentionally unsound), and also lets the callers do more idiomatic protobuf construction.
Hooray for type safety!
Added enough tooling to make the interface types also recursively accept interface types
|
unbooping: Your PR is too powerful! Try breaking it up into multiple changes. |
In a previous attempt to avoid changing types that are not interfaces (e.g. enums), the interfaces were also not converted. This is now fixed.
| @@ -1698,10 +1698,10 @@ export namespace api { | |||
| constructor(properties?: api.IFileEvent); | |||
|
|
|||
| /** FileEvent file. */ | |||
| public file?: (api.IFile|null); | |||
| public file?: (api.File|null); | |||
There was a problem hiding this comment.
One worth checking is that these class properties that reference another message type are actually instances of the class not just the interface.
It doesn't seem to be the case, the types are wrong unfortunately https://repl.it/@masfrost/luisprotos
|
welp, seems like i've reached the limit of what can be monkeypatched from the outside. i'll probably have to send something upstream to avoid having to do some invasive surgery that's going to become a lot more work going forward. In the interest of making forward progress, wdyt of adding a TODO and landing this mostly as-is? it'll take some time to get the change ready and approved and merged. worst case scenario, we fork that npm and patch it ourselves as a middle-term solution. |
|
There's also a small change that can be done to comply with all the types: |
This creates objects with the correct type requirements AND outputs objects with the correct types. Type safety FTW!
masad-frost
left a comment
There was a problem hiding this comment.
Sweet, bit spooky, but sweet. Thank you for powering through!
masad-frost
left a comment
There was a problem hiding this comment.
TBH I don't feel that great changing runtime semantics for the sake of types, but I don't wanna block this for too long.
From reading the code, fromObject has potentially non-trivial performance overhead. It's worth benchmarking this new code.
This fixes an accidental infinite loop.
it's hard to say whether there is a statistically significant difference: I tried running benchmark in https://repl.it/@luisreplit/luisprotos#index.js and https://repl.it/@luisreplit/luisprotos-1#index.js (cannot load them into the same script because the generated code pollutes global state, so the last version in a rough apples-to-apples comparison, I pitted |
protobufjs/protobuf.js#1486 released in protobufjs 6.10.2 and updated in @replit/protocol in replit/protocol#9 and released in 0.2.15

This change adds Flow declaration types so that we can check the types
of the generated code.
Notably:
Adds a dependency to flowgen and calls it during compilation, which
takes the
.d.tsand creates a.flow.jsbased on it.Passes in the
--force-numberflag to pbjs, which causes it to notemit numbers as possibly being
Long, since this confuses Flow. Thisonly affects JSDoc declarations, not the generated code.
Passes in the
--force-messageflag to pbjs. Otherwise, this emitsunsound types: it tries to create code like this:
This violates the Liskov Substitution Principle, since
Messagecannot be passed to any place that accepts an
IMessage, soTypeScript (correctly) rejects this as bogus. With this flag, the
number of places that will accept an
IMessageinstead of aMessageare limited to constructors, where we do want to have partial messages
being passed in.
This only affects JSDoc declarations, not the generated code.
Removes all
@implementsannotations from the JSDoc, which takes careof the very last
IMessage/Messageconfusion. This, together withthe above element take care of
Generated TypeScript message class not compatible with generated message interface protobufjs/protobuf.js#837