Stricter typing on data passed to worker#707
Stricter typing on data passed to worker#707SimonSimCity wants to merge 15 commits intotimgit:masterfrom
Conversation
# Conflicts: # src/index.ts # src/manager.ts
The here provided type-overrides ensure that the property `data`, even though being optional, is provided if the type does not allow `undefined.
|
Thanks for the submission. You are certainly further along in experience with TS than me, so I'm still trying to understand the rationale behind some of this. At first glance, it appears that you'd like to restrict the allowed queues you interact with from a type perspective. This means that if you'd like to interact with a queue, you first would have had to configure your intent to do that. This doesn't mean you can't interact with a queue dynamically at run-time, but TS would at least warn you that you should have registered that queue first, and if typechecks are part of your CI, you would want this lack of registration to fail your build. Another question I have would be the use of Overall, I'm fine with this, provided it's opt-in and doesn't require all users to use it. If it were required, my reaction was that it adds too much complexity to get started. |
|
Thanks for the review. I'll try to provide some insight into what I thought: All existing code-bases should work without changes.This is possible just to a certain extend. In codebases that didn't use any types, everything works fine, because the class name: The only exception to this rule, that I couldn't keep intact without substantial changes, is the types of earlier generic functions, like If types are used, the library will get a lot stricter than it has been earlier, but I think this is a good sign and can avoid common pitfalls, where a user accidentally assigns the wrong type to a worker - a type that is incompatible with what actually is passed to Types should be ensured without avoidable type-casting.This required some of the generic types to pass through to almost every part of the system, but it let's you have the assurance, that those types always are compatible. Might be quite an overhead if nobody actively maintaining is thoroughly proficient in TypeScript to the extend needed 😅 If a user decides to use types, they should be as strict as reasonably possible.The function passed to Having this allows users to have the As additional benefit, the library now "eats its own dogfood" in the way that the timekeeper is subject to the same type rules, and is always allowed without the user having to explicitly define it. Before, the types could vary for the same name and the user was responsible to set up a typing network around these functions which help to limit the types to what the For the tests, I didn't want to rewrite all of the tests, so I figured out that doing some quirx here and there is acceptable to have types but possibly different for each test. Hope this gives you more insight on my thought process. Would be nice to hear what you think of these statements. |
|
If you guys that were involved in the previous Typescript PRs, I would love to have a few more eyes and opinions on the DX being proposed. |
|
I can take a look this week. |
|
|
||
| async function typedInit (schema: string) { | ||
| const config = helper.getConfig() | ||
| const boss = new PgBoss<{ |
There was a problem hiding this comment.
Thanks @SimonSimCity, I'm really looking forward for stricter typing.
I'm wondering if the following usage will be possible:
// ~modules/core/core.types.ts
// Type intended to be augmented by other modules.
export interface JobSchema {}
// ~modules/core/core.module.ts
const queue = new PgBoss<JobSchema>({ ... });// ~modules/foo/foo.types.ts
declare module "~modules/core/core.types.js" {
interface JobSchema {
"queue-foo": { input: { a: string, b: number } };
}
}// ~modules/bar/bar.types.ts
declare module "~modules/core/core.types.js" {
interface JobSchema {
"queue-bar": { input: { c: boolean } };
}
}So we don't have to declare everything in a single location.
Also, could we make output optional when declaring job types ?
There was a problem hiding this comment.
Also, could we make output optional when declaring job types?
Thanks for the improvement, that wasn't too hard to add 😎
The other question, of making the job definition as an interface, is something I don't know whether this is possible. Everything I tried so far failed (I even found a quite old discussion on that issue microsoft/TypeScript#15300). In my opinion, it makes for a better control if the place where you instantiate pg-boss knows of all possible types of jobs.
There was a problem hiding this comment.
Thanks @SimonSimCity
In my opinion, it makes for a better control if the place where you instantiate pg-boss knows of all possible types of jobs.
It depends on the project architecture. In my case, I decided to expose a generic job service as part of a "core" module, which does not know anything about other modules (queues are defined in each module). I may need to rethink this abstraction if augmenting an interface does not work.
There was a problem hiding this comment.
Fair. I just found a way in which this would be doable:
import { JobStates, PgBoss } from '../../src'
export interface JobSchema {
[key: string]: {
input: object | null | undefined;
output?: {
[S in JobStates[keyof JobStates]]?: unknown;
}
},
'queue-bar': { input: { c: boolean }, output: { completed: boolean } }
}
const queue = new PgBoss<JobSchema>({})
queue.work('queue-bar', (j) => Promise.resolve(j[0]!.data.c))
queue.work('abba', (j) => Promise.resolve(j[0]!.data))But by this, you would make whatever string assignable to the first parameter of queue.work. I would want he last line should fail, because the job-type abba is never defined. It still works, because it fits the index-signature - something I had to add in order to have it fit the type definition.
There was a problem hiding this comment.
Indeed, it's not ideal because of the reason you mentioned. It also does not work well with code completion (i.e. queue.send(" does not show available queues, at least in VSCode).
This seems to workaround the index-signature error:
// pg-boss
export type JobsConfig<K extends PropertyKey> = Record<
K,
{
input: object | null | undefined;
output?: Record<JobStates, Json | undefined>;
}
>;
export class PgBoss<
Queues extends object = DefaultJobsConfig,
Config extends JobsConfig<keyof Queues> = Queues extends JobsConfig<keyof Queues> ? Queues : never,
// ...
> {
async send<K extends keyof Config>(
name: K,
data: Config[K]["input"],
): Promise<void> {
//...
}
}I'm not sure if it's an acceptable solution but then this works (including module augmentation):
// main.ts
export interface JobsSchema {
"queue-a": { input: { a: string; b: number } };
}
export const boss = new PgBoss<JobsSchema>();
// Code completion work as expected
boss.send("queue-a", { a: "a", b: 42 }); // no error
boss.send("queue-c", { foo: "bar" }); // type errorThere was a problem hiding this comment.
Thanks for the help and code, and I think I've settled on something that is sufficient without further changes on the codebase by destructuring the interface before passing it as generics to the class. See the following code example, which I also added to the library (9da63f8#diff-ac8ef012db944f3e55c1fbb64c909520514ed7dff642295b6242a2d035d8ef07):
import { PgBoss } from '../../src'
interface JobsSchema {
'queue-a': { input: { a: string; b: number } };
// foo: { bar: string }; // <-- This line will fail because its not a valid job definition.
}
interface JobsSchema {
'queue-b': { input: { a: string; b: number } };
}
export const boss = new PgBoss<{
[S in keyof JobsSchema]: JobsSchema[S];
}>({})
// Code completion work as expected
boss.send('queue-a', { a: 'a', b: 42 })
// boss.send('queue-c', { foo: 'bar' }) // <-- This line will fail because the job-type is not defined.This way, an error will occur at the instantiation of the PgBoss instance in case the programmer defined an invalid property on the interface, and you get proper typing on the functions on the instance. This way the error is at least as close to the origin of the source while still maintaining full type-safety 🎉
There was a problem hiding this comment.
Awesome! Thanks for looking at that use case.
| | { [property: string]: Json } | ||
| | Json[] | ||
|
|
||
| export type JobStates = { |
There was a problem hiding this comment.
Out of curiosity, do you know why declaring JobStates and Events like this instead of:
export type JobStates =
| "active"
| "cancelled"
| "completed"
| "created"
| "failed"
| "retry"
export type Events =
| "bam"
| "error"
| "stopped"
| "warning"
| "wip";I find it makes usage a bit cumbersome, for example:
{ [S in JobStates[keyof JobStates]]: Json | undefined }
// instead of:
{ [S in JobStates]: Json | undefined }
// or just:
Record<JobStates, Json | undefined>There was a problem hiding this comment.
It allows for easier object definition (https://github.com/timgit/pg-boss/blob/master/src/index.ts#L16). Whether this was the original reason, I don’t know. I wouldn’t change those types without good reason because it would break backwards compatibility, as they’re party of the public interface of the library (they’re exported in the index.ts and are thereby not just an internal type).
There was a problem hiding this comment.
I didn’t mean to change it, just curious about this approach (I didn’t see the export of events). Now that your PR relies on these types, a small improvement to get rid of JobStates[keyof JobStates] could be:
export type JobState =
| "active"
| "cancelled"
| "completed"
| "created"
| "failed"
| "retry";
export type JobStates = Record<JobState, JobState>;
// ... and maybe the same for Event / Events
Anyway, feel free to ignore this, and apologies for jumping into this PR while not maintaining the library.
There was a problem hiding this comment.
No need to apologize. Improvements and suggestions, also constructive criticism are always welcome 😎🎉
|
Sorry I mean to take a deeper pass at this and never did. |
Fixes #692.
I've set quite some types and think this is as complete as I can get it. I also tried to split up the work into meaningful commits, so going through them one-by-one should give the reviewer a good understanding of the individual type-refactorings I did.
I haven't yet put down the work for showing how the types work, other than the example. I can create some documentation around this but first want to know whether this goes into the right direction.
I found the function
getQueues()not to pass on thenameproperty. Maybe this is something that should be fixed.