Meteor + TypeScript: Setup and Best Practices in 2026

Introduction

Meteor has been around since 2012, and if you’ve heard it written off as a relic, you haven’t seen what a modern Meteor stack looks like. In 2026, Meteor will remain one of the few full-stack JavaScript frameworks that give you real-time reactivity, a unified client-server data layer, and a mature deployment target in Galaxy without stitching together five different tools to get there.

But Meteor’s flexibility has always come with a tradeoff: its dynamic, reactive nature makes runtime errors easy to introduce and difficult to trace. A publication returning the wrong shape, a method receiving an unexpected argument, and a collection query missing a required field. These bugs surface at runtime, not at compile time. That’s exactly the gap TypeScript fills.

TypeScript adds a safety net to the parts of Meteor where bugs hide most easily: your collections, your methods, and your subscriptions. Take a simple example: you define a `Link` document with a title, url`, and createdAt field. Without TypeScript, nothing stops you from inserting a document with the wrong field names or missing fields entirely. Meteor won’t complain  it’ll just save bad data, and you’ll find out when something breaks in production. With TypeScript, your editor catches that mistake before you even run the app.

This guide covers exactly that: how to set up a Meteor project with TypeScript correctly from the start and the practices that actually matter once you’re building. By the end, you’ll have a clean, scalable foundation and a clear picture of where TypeScript earns its keep in a Meteor app.

Setting Up a Meteor + TypeScript Project

Prerequisites

Before creating your project, make sure you have the following tools installed:

  • Meteor CLI Installed(verify with meteor –version)

If you don’t have Meteor installed yet, follow the official installation guide before continuing.

Once that’s done, scaffold a new project with the –typescript flag:

Bash
meteor create --typescript my-app
cd my-app

Note:  Replace my-app with your preferred project name.

It gives you a Meteor app with TypeScript and React pre-configured out of the box with no additional setup needed.

Project Structure

After scaffolding, your project will look like this:

Bash
my-app/
├── client/
   ├── main.css
   ├── main.html
   └── main.tsx        # Client entry point
├── imports/
   ├── api/
      └── links.ts    # Collections and types
   └── ui/
       ├── App.tsx
       ├── Hello.tsx
       └── Info.tsx    # Reactive data example
├── server/
   └── main.ts         # Server entry point
├── tests/
   └── main.ts
├── tsconfig.json
└── rspack.config.ts

The most important convention here is the imports/ folder. Meteor only auto-executes files placed directly in client/, server/, and tests/. Anything inside imports/ is lazy-loaded — it only runs when explicitly imported somewhere. This keeps your data layer and UI code decoupled and prevents files from executing in the wrong environment.

Understanding the tsconfig.json

The generated tsconfig.json includes some Meteor-specific options that are worth understanding:

JSONC
{
  "compilerOptions": {
    /* Basic Options */
    "target": "es2018",
    "module": "esNext",
    "lib": ["esnext", "dom"],
    "allowJs": true,
    "checkJs": false,
    "jsx": "preserve",
    "incremental": true,
    "noEmit": true,
    /* Strict Type-Checking Options */
    "strict": true,
    "noImplicitAny": true,
    "strictNullChecks": true,
    /* Additional Checks */
    "noUnusedLocals": true,
    "noUnusedParameters": true,
    "noImplicitReturns": false,
    "noFallthroughCasesInSwitch": false,
    /* Module Resolution Options */
    "baseUrl": ".",
    "paths": {
      /* Support absolute /imports/* with a leading '/' */
      "/*": ["*"],
      /* Pull in type declarations for Meteor packages from either zodern:types or @types/meteor packages */
      "meteor/*": [
        "node_modules/@types/meteor/*",
        ".meteor/local/types/packages.d.ts"
      ]
    },
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "types": ["node", "mocha"],
    "esModuleInterop": true,
    "preserveSymlinks": true
  },
  "exclude": [
    "./.meteor/**",
    "./packages/**",
    "./_build/**",
    "./public/build-chunks/**",
    "./public/build-assets/**"
  ]
}

“noEmit”: true means TypeScript is only doing type checking here — Meteor’s build tool handles the actual compilation. Without this option, TypeScript and Meteor’s build pipeline would conflict.

The paths mapping under meteor/* is what allows imports like import { Mongo } from ‘meteor/mongo’ to work without TypeScript throwing a module not found error. Don’t remove it.

The strict, noImplicitAny, and strictNullChecks options work together to give you meaningful type safety. Turning any of them off to silence errors is tempting but counterproductive you’ll just push the bugs back to runtime where they’re harder to find.

Linting and Formatting

TypeScript handles type safety, but it won’t catch explicit any usage slipping through your code or enforce consistent formatting across your client and server files. In a Meteor project where code runs on both the client and the server, having ESLint and Prettier enforce consistent rules across the whole codebase is well worth the setup time.

Install ESLint and Prettier with:

Bash
npm install --save-dev eslint prettier eslint-config-prettier @typescript-eslint/parser @typescript-eslint/eslint-plugin

Then create a minimal .eslintrc.json in the root of your project:

JSON
{
  "parser": "@typescript-eslint/parser",
  "plugins": ["@typescript-eslint"],
  "extends": [
    "eslint:recommended",
    "plugin:@typescript-eslint/recommended",
    "prettier"
  ]
}

And a .prettierrc in the root of your project:

JSON
{
  "singleQuote": true,
  "semi": true,
  "tabWidth": 2
}

That’s the full setup. Run meteor run to confirm everything boots cleanly before moving on.

Best Practices for Meteor + TypeScript Development

Typing Mongo Collections

The foundation of type safety in a Meteor app starts with your collections. The scaffold generates imports/api/links.ts with a clean pattern for typing collections out of the box:

TypeScript
import { Mongo } from 'meteor/mongo';

export interface Link {
  _id?: string;
  title: string;
  url: string;
  createdAt: Date;
}

export const LinksCollection = new Mongo.Collection<Link>('links');

By passing Link as a generic to Mongo.Collection, every operation on LinksCollection: inserts, updates, queries is now type-checked against the Link interface. If you try to insert a document with a missing url or a title typed as a number, TypeScript will catch it immediately in your editor before the code ever runs.

Notice that _id is marked optional with ?. That’s intentional Meteor generates _id automatically on insert, so you never pass it in manually.

Typed Server Functions

Without TypeScript, there’s nothing stopping a client from passing the wrong arguments to a server function and the server silently receiving bad data. Before defining methods, it’s good practice to type your helper functions first — here is how the scaffold handles this in server/main.ts:

TypeScript
import { Meteor } from 'meteor/meteor';
import { Link, LinksCollection } from '/imports/api/links';

async function insertLink({ title, url }: Pick<Link, 'title' | 'url'>) {
  await LinksCollection.insertAsync({ title, url, createdAt: new Date() });
}

Meteor.startup(async () => {
  if (await LinksCollection.find().countAsync() === 0) {
    await insertLink({
      title: 'Do the Tutorial',
      url: 'https://react-tutorial.meteor.com',
    });
  }
});

The Pick<Link, ‘title’ | ‘url’> utility type is doing real work here. Instead of defining a brand new interface for the function arguments, it picks only the title and url fields from the existing Link interface. This means if you ever change the type of a field in the Link interface, TypeScript will immediately flag any place that uses insertLink with the wrong type no duplicate type definitions to manually keep in sync.

Reactive Data with TypeScript

In Meteor, your UI automatically updates whenever the data it depends on changes, no manual refetching needed. The modern way to tap into this in a React + TypeScript app is through useFind and useSubscribe from meteor/react-meteor-data. The scaffold wires this up in imports/ui/Info.tsx:

TypeScript
import { useFind, useSubscribe } from 'meteor/react-meteor-data';
import { LinksCollection, Link } from '../api/links';

export const Info = () => {
  const isLoading = useSubscribe('links');
  const links = useFind(() => LinksCollection.find());

  if (isLoading()) {
    return <div>Loading...</div>;
  }

  const makeLink = (link: Link) => (
    <li key={link._id}>
      <a href={link.url} target="_blank">{link.title}</a>
    </li>
  );

  return (
    <div>
      <h2>Learn Meteor!</h2>
      <ul>{links.map(makeLink)}</ul>
    </div>
  );
};

Because LinksCollection is already typed as Mongo.Collection<Link>, TypeScript knows that any query on it returns a list of Link objects. So when useFind runs the query, it automatically knows links is of type Link[] — you don’t need to annotate it manually. This is why typing your collections correctly from the start matters — the types flow through the rest of your code automatically.

One thing worth noting: isLoading is a function, not a boolean value. You have to call it as isLoading() with the parentheses — if you write if (isLoading) without the parentheses, it will always evaluate to true because you’re checking if the function exists, not its return value. This is a runtime gotcha that TypeScript won’t catch, so it’s worth keeping in mind.

Recommended Packages

A few packages worth knowing for any Meteor + TypeScript project in 2026:

zodern:types — this is how TypeScript finds type definitions for Meteor packages. The good news is you don’t need to install it — it’s already included in the TypeScript scaffold by default. Whenever your app builds, it updates the type definitions for your Meteor packages automatically in the background. You don’t use it directly in your code, but without it, imports like import { Mongo } from ‘meteor/mongo’ wouldn’t have type information. You can find its full documentation on the Meteor docs.

meteor/react-meteor-data — already included in the TypeScript scaffold. This is the package that gives you useFind, useSubscribe, and useTracker for working with reactive data in React components. You’ve already seen it in use in imports/ui/Info.tsx:

TypeScript
import { useFind, useSubscribe } from 'meteor/react-meteor-data';

No additional installation needed, it’s ready to use as soon as you scaffold the project. The full documentation can be found on the Meteor docs.

Common Pitfalls and How to Avoid Them

Typing `Meteor.user()` correctly

One mistake that catches a lot of developers is assuming Meteor.user() always returns a user object. It doesn’t — it returns Meteor.User | null. If you try to access properties on it without checking for null first, you’re setting yourself up for a runtime crash. With strictNullChecks enabled in your tsconfig.json — which it is by default in the scaffold — TypeScript will catch this before your code ever runs.

This will cause a TypeScript error:

TypeScript
const email = Meteor.user().emails[0].address; // Error: Object is possibly null

The correct way to handle it:

TypeScript
const user = Meteor.user();
if (user) {
  const email = user.emails?.[0]?.address;
}

Never disable strictNullChecks to silence this error — it exists to protect you from a real runtime crash.

Using `any` in publications

Publications are one of the easiest places for any to sneak into your codebase. A publication that returns any means TypeScript stops checking the shape of your data entirely — and bugs that should be caught at compile time end up surfacing at runtime instead.

This is the pattern to avoid:

TypeScript
Meteor.publish('links', function (): any {
  return LinksCollection.find();
});

The correct approach is to let TypeScript infer the return type from your typed collection:

TypeScript
Meteor.publish('links', function () {
  return LinksCollection.find();
});

Because LinksCollection is already typed as Mongo.Collection<Link>, the return type is inferred automatically. No any needed.

Using arrow functions in publications

This is a Meteor-specific gotcha that trips up a lot of developers coming from a React background. Inside a Meteor publication, this refers to the publication context — giving you access to this.userId, this.ready(), and this.stop(). Arrow functions don’t have their own this, so using one silently breaks access to that context.

This will silently break your publication:

TypeScript
Meteor.publish('links', () => {
  console.log(this.userId); // undefined
  return LinksCollection.find();
});

Always use a regular function in publications:

TypeScript
Meteor.publish('links', function () {
  console.log(this.userId); // works correctly
  return LinksCollection.find();
});

This is actually called out in the official Meteor Guide — it’s not an edge case, it’s just easy to miss when you’re used to writing arrow functions everywhere.

Conclusion

TypeScript doesn’t slow down Meteor development — it makes Meteor’s reactivity model safer to work with as your app grows. The combination gives you the rapid prototyping speed Meteor is known for, with the type safety that keeps a growing codebase from becoming fragile.

What you’ve set up in this guide — typed collections, typed methods, reactive hooks with proper return types, and a clean project structure — isn’t just boilerplate. It’s the foundation that lets you move fast without constantly second-guessing whether your data shapes are correct or whether a method is receiving the right arguments.

If you’re starting a new Meteor project in 2026, the TypeScript scaffold is the right starting point. Run `meteor create –typescript my-app`, follow the practices in this guide, and you’ll have a clean, maintainable setup in under ten minutes. If you’re working on an existing Meteor project, the same principles apply — start by typing your collections and work outward from there.

Meteor’s docs are worth bookmarking as you build — they’re well-maintained and cover everything from deployment on Galaxy to advanced data patterns.