Meteor 💜 Typescript

Ever since I first heard of Meteor four years ago (since 0.8 if I recall correctly), I’ve loved it. I really love the way Meteor solves a lot of boilerplate when it comes to data synchronisation and rendering. But there is one thing I really started to hate as someone who spends a lot of time building iOS apps. Because I’ve been developing with the help of a compiler for the last two years, I really fell in love with the compiler, and since we started developing in Swift, a static type system. But Javascript does not provide one.

When I started a new school project last September, I immediately thought of Meteor. We had foreseen that our project would have a lot of data floating around, so I felt that Meteor was a good match. And it was. Also, since I study part-time, our team would not meet very often. Two months into the project, with a really scattered schedule, I found myself trying to figure out how my data model had changed (because others/I changed it), and which property or method I could use. This was really slowing us down, so I started looking for a solution.

I had used Typescript several times, but I hated the fact that I always needed to get a lot of pipelining going, such as Grunt or Gulp, to compile your LESS/SCSS or minify your Javascript. The Typescript Compiler (in short: TSC) in itself is a powerful tool, but to integrate it into the project, you need such a pipeline in place to compile your Javascript. For big projects it’s no problem at all to spend a good amount of time with your tooling. But for short projects you just want to code. I found a really awesome tool on Github that integrates the TSC compiler into the Meteor precompiler. Awesome!

What I really like is how they approached type safety in Meteor. Normally you would type:

Answers = new Meteor.Collection('answers');

But with Typescript you now can declare:

class Answer {
    value: string;
    questionId: string;

constructor(value: string, questionId: string) {
        this.value = value;
        this.questionId = questionId;
    }
}

Answers = new Mongo.Collection<Answer>('answers');

Yay! Generics! So Answer is an interface in which I declare my properties. This way I can make an Answer in a typesafe manner:

var answer: Answer = new Answer("Ja!", "_some_id_");
Answers.insert(answer);

And I will be confronted by a compiler error when I pass in the wrong types.

var answer = new Answer("A", 1);

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Compiling TypeScript web.browser files...
=> Errors prevented startup:

While building the application:
 foo.ts:1:2: error
 TS2345: Argument of type 'number' is not assignable to parameter of type
  'string'.

=> Your application has errors. Waiting for file change.

Or, insert another type has now become impossible. For instance:

var answer = {"A": 1, "C": 42};
Answers.insert(answer);

~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
Compiling TypeScript web.browser files...
=> Errors prevented startup:

While building the application:
 /Users/tomas/Developer/HvA/tag-and-trace-server/client/client.ts:24:6: error
 TS2345: Argument of type '{ "A": number; "C": number; }' is not assignable
  to parameter of type 'Answer'.
 
=> Your application has errors. Waiting for file change.

Caveats

Some things aren’t that compatible yet. Many packages aren’t covered by a Definition file (.d.ts) so usage in Typescript is impossible. In short: d.ts files define all the type information for vanilla Javascript. So if you want to use Meteor functions in Typescript, you need the Meteor definition. Some packages are providing such a definition file, but for some I had to write my own.

Additionally, find and findOne are typed by the compiler, but your defined methods in your class are not injected by the underlying find method in Meteor. You just receive an Object.

So this works:

class Answer {
   value: string;
   questionId: string;

constructor(...) {...}

foo() {
     console.log("Bar");
   }
}

var answer: Answer = new Answer("Ja!", "_some_id_");
answer.foo() // Bar

This compiles, because instancetype Answer has function foo. But this fails:

var answer: Answer = Answers.findOne("AABCCDD");

answer.foo(); // Uncaught TypeError: answer.foo is not a function

typeof answer // ‘object’

The compiler is happy, because you comply with the return type of findOne (which is Answer). But the underlying function executed does not know of the type defined by Typescript. Same goes for i.e. a cast in Java. Bytecode does not know what the type is of a certain memory reference. That is something the Java compiler decided when it compiled your code. Since there is no cast mechanism in Typescript (typescript barely has any runtime), I had to write my own cast function.

¯\_(ツ)_/¯