Following on from a solution I came up with for strongly typed Meteor.method
parameters and results, as I was starting out on a new project my subscriptions all felt “dirty” because they weren’t typed. I could make any typo or miss any paramater and not know 'till runtime! - TypeScript FTW!
So here’s what I’ve come up with for defining publications and calling subscriptions with types. My objective when defining types is that I should never have to define something twice - everything should be inferred from first usage. Otherwise there’s scope for error.
First you need to define an interface for each publication and at the same time create wrapper functions for Meteor.publish()
and Meteor.subscribe()
. Technically these wrappers only need to be on the server and client respectively, but for convenience I put them in the same file (they are very small):
// imports/TypedPubSub.ts
export default function TypedPubSub<T>(name: string, _paramDef: T) {
return {
publish: (fn: (this: Subscription, params: T) => void) => Meteor.publish(name, fn),
subscribe: (params: T, callback?: any) => Meteor.subscribe(name, params, callback),
}
}
Then you can define your interfaces for each publication. These need to be available on the client and the server, so don’t combine it with any code you don’t want on the client, hence I put them in their own imports folder, grouped by collection:
// imports/subdefs/tenants.ts
import TypedPubSub from "/imports/TypedPubSub";
export const tenantSubs = {
tenants: TypedPubSub('tenants', {tradingOnly: false}),
tenantMonths: TypedPubSub('tenantMonths', {tenantId: '' as string}),
tenantsMonths: TypedPubSub('tenantsMonths', {tenantIds: [] as string[], minMonth: '' as string, maxMonth: '' as string}),
} as const
Then you can create your publications on the server:
// server/publications/tenants.ts
import { tenantsSubs } from '/imports/subdefs/tenants';
tenantSubs.tenantsMonths.publish(function(params) {
check(this.userId, String);
check(params, {
tenantIds: [String],
minMonth: String,
maxMonth: String,
})
return tenantMonthsCollection.find({
deleted: {$ne: true},
tenantId: {$in: params.tenantIds},
ownerId: this.userId,
month: {$gte: params.minMonth, $lte: params.maxMonth},
});
});
Here the types of tenantIds
, minMonth
and maxMonth
are all automatically known without any further type annotations, although using check
to validate the parameters does that for us anyway.
Finally, to subscribe to the publication on the client:
// somewhere/in/a/client/component
import { tenantsSubs } from '/imports/subdefs/tenants';
...
// any error in these parameters will display an error in your IDE... ;-)
const subHandle = tenantSubs.tenantsMonths.subscribe({tenantIds: [...], minMonth: '2021-01', maxMonth: '2021-12'})
The eagle eyed among you will notice that subscriptions defined in this way only take one object parameter, just to keep things simple.
Hopefully this helps someone else!