/meteor-type-validation

ℹ️ Improved type inference for Meteor's built-in method call() and subscribe() methods.

Primary LanguageTypeScriptMIT LicenseMIT

Meteor Type Validation

Improves Meteor's built-in types using declarative API resource definitions.

Installation

Install the package, and optionally valibot for schema validation.

npm i meteor-type-validation valibot

Defining your Meteor API

This package takes a different approach to defining your Meteor methods and publications. To properly infer types for each resource without resorting to a compilation step, we explicitly export each method/publication we want to expose.

Instead of defining your methods using Meteor.methods(...), we export an object with the methods you want to publish. We have two helper functions for this, defineMethods() and definePublications() they're only there for type inference and just returns the same object you provided.

Example methods

// ./imports/api/topics/methods.ts
import { defineMethods } from 'meteor-type-validation';
import * as v from 'valibot';

const CreateSchema = v.object({
    title: v.string(),
});

export default defineMethods({
    'topics.create': {
        schema: [CreateSchema],
        method(topic) { // Method parameters are validated and have proper types
            return TopicsCollection.insert(topic);
        }
    },
    'topics.remove': {
        schema: [v.string()],
        method(topicId) {
            return TopicsCollection.remove(topicId);
        }
    }
});

Example publications

// ./imports/api/topics/server/publications.ts
import { definePublications } from 'meteor-type-validation';
import * as v from 'valibot';

const QuerySchema = v.object({
    _id: v.optional(v.string()),
    createdBy: v.optional(v.string()),
});
const OptionsSchema = v.object({
    limit: v.number(v.maxValue(255))
})

export default definePublications({
    'topics': {
        schema: [QuerySchema, OptionsSchema],
        publish(query, options) {
            return TopicsCollection.find(query, options);
        }
    }
})

Registering your API definitions

Having all your Meteor API resources be defined as exports rather than through a method call, we can now extend and augment all your resources. But most importantly, we can extend Meteor's types to make things like Meteor.call() autocomplete and perform type validation.

On the server, import all of your publication and method definitions and add them to one big index object.

// ./imports/api/index.ts
import { exposeMethods, UnwrapMethods } from 'meteor-type-validation'
import TopicMethods from '/imports/api/topics/methods';
import TopicPublications from '/imports/api/topics/server/publications';

export const AllMethods = {
    ...TopicMethods,
} as const;

export const AllPublications = {
    ...TopicPublications,
} as const;

Then, in your server startup module, import and expose each resource like you normally would with Meteor.publish() and Meteor.methods().

// ./server/startup.ts
import { AllMethods, AllPublications } from '/imports/api';
import { 
    exposeMethods, 
    exposePublications,
    UnwrapPublications,
    UnwrapMethods 
} from 'meteor-type-validation';

Meteor.startup(() => {
    exposeMethods(AllMethods);
    exposePublications(AllPublications);
});

// This extends Meteor's types so that Meteor.call() and Meteor.subscribe()
// will autocomplete and do all that sweet type checking for you 👌
declare module 'meteor/meteor' {
    interface DefinedPublications extends UnwrapPublications<typeof AllPublications> {}
    interface DefinedMethods extends UnwrapMethods<typeof AllPublications> {}
}

And that's about it. Whenever you use Meteor.subscribe() or Meteor.call() you should see that it both autocompletes method/publication names, and it type checks your provided parameters.

Notes

If you're using the @types/meteor package, you might only get auto-completion for publication/method names. The best way to enforce strict typing for these calls would be to explicitly define the resource name as a generic param.

Meteor.call<'topics.create'>('topics.create', { 
    title: '...'  // strictly type checked
})

Meteor.subscribe<'topics'>('topics', { 
    createdBy: null // Emits a type error that we were expecting to see here
})

We also export a type helper that have these rules pre-applied, so you won't have to repeat yourself.

import { MeteorApi } from 'meteor-type-validation';

// Typo checks
MeteorApi.call('topics.creatE') // type error: Argument of type `topics.creatE` is...


MeteorApi.subscribe('topics', {
    createdBy: null, // type error: `null` is not assignable to `string`
})

License

MIT