Use TypeScript types for check in methods

@wildhart That approach does not do type narrowing. After you call check(), the type of the variable is not narrowed.

Here’s an example (in Solid Playground, which can be useful as a TypeScript playground too): Solid Playground

Here is what we want: Solid Playground

(that’s only a sample limited to number types, but Match.test should cover the pattern passed in)

@trusktr there is a small difference in your Solid Playground compared with the type definition I provided. Here’s a working example which matches the error thrown by check().

image

2 Likes

Ooooh, asserts. I somehow missed that TS feature update and hadn’t stumbled on it until now! Super nifty!

I see that check.d.ts is using that already in Meteor source. So, it should already be working then.

2 Likes

Ok, but how exactly would I use this in a method then?

I don’t know why you need a “special version” of check function to work with typescript.
I’m using typescript and it just work.

async "sample.method"({ strIn, numIn }: { strIn: string; numIn:  number; }) {
  check(strIn, String);
  // or you can use Match.test
  if (!Match.test(numIn, Number)) {
    throw new Meteor.Error("someCode", "some message");
  }
}

my tsconfig.json file:

{
  "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 '/' */
      "/*": ["*"]
    },
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "types": ["node", "mocha"],
    "esModuleInterop": true,
    "preserveSymlinks": true
  },
  "exclude": ["./.meteor/**", "./packages/**"]
}

@waldgeist you shouldn’t need to do anything extra to use this. In any normal code (including Meteor.methods), after you’ve called the built-in check() method on a variable, typescript should know what type the variable is:

The only thing you might need to do is npm install -D @types/meteor

@minhna we’re not using a “special version” of anything. The code examples above were contrived simplifications to demonstrate how the type-narrowing definition of the existing check() method works.

@waldgeist if you were to use a strongly-typed method wrapper, such as the one in my post (or Zodern:relay)…

image

…then you would get type checking goodness when calling a method too:

1 Like

@waldgeist he is asking what to do. With my experience all you need to do is install the @types/meteor package.

I am using Zoderns relay package in my typescript projects at the moment and really happy with the results.

It checks the parsed parameters with zod and returns an error when they don’t match, so no need for the check package anymore.

It also creates functions instead of Meteor.call() that have types infered to them so you know which props to parse etc.

2 Likes

You’re still doing double checks here. One at compile time (TS) and one on runtime (check, Match).

Ah, ok, thanks. This is exactly what I was looking for. Sweet that check automatically hands the types over to TypeScript.

That’s also nice! Thanks for the hint.

Relay is also great because it proxies return types between server and client.

1 Like

Oh, cool, so it’s like EJSON?

@waldgeist no, it’s just the types, so basically if Typescript knows the return type for the server return, the code you call on the client will have knowledge of that type in the client code. So if your method returns an Number the client code will have knowledge that the return is a Number.

I’ll second zodern:relay. Amazing improvement to developer experience. @zodern @copleykj

As others have said, the check function will handle coercion from one type to another automatically with Meteor’s typescript support set up correctly and after you start up your app (for zodern:types)*

The type signature of check basically says, “if I return successfully without throwing an error, the variable passed into me is definitely of type T, where T is inferred from the schema object passed in”.

In addition to the solutions provided by others, there is also (on npm) the following you can use for type assertions at runtime that have a corresponding effect on your static types:

  • @sinclair/typebox (supposedly faster than Zod)
  • purify-ts (for functional programmers, check out the “Coerce” module)

(Amongst many others)

Edit: regarding the asterisk*, I believe that’s the case at least. If not, I had the behaviour with an up to date version of the npm package @types/meteor - either way, I didn’t have to configure any special type overrides or anything, I just needed a strict TSConfig set up with appropriate “includes” references to the corresponding npm/.meteor/local .d.ts files, and it infers the types for me from the schema passed into check.

One other thing that I forgot to mention about zod with zodern:relay is that the React-Hook-Form library also has a fantastic zod integration so you can share you schema’s across client and server, get validated and type safe method params, form data, as well as a type aware form integration. If you are passionate about type safety you’ll absolutely love this, and if you’re not yet, you will be after using tools like this for a while.

There is one thing that’s still missing from Meteor’s type safety story though. Currently if you pass a type to your meteor collection it will set up inference for all of the methods on the collection and when you run a find or find one, it will return a document or a cursor of documents with that type you specified. But what if you have a Profile type and it has a firstName, lastName, bio etc. you pass this to the collection via the type parameter and that’s the type of documents. Then you do something quite normal and you specify that you only want the firstName field in the find method options. Now the type for the return lies and tells our tooling that all the fields are available on each document of the result set. For some this might not seem like that big of a deal, but If you’ve ever worked with a data stack like TRPC with Prisma you understand the DX that you are missing out on. This E2E type safety all the way from the database to the client, this for me, is the Holy Grail, and I’m hoping to make this a reality over the next few months by helping to surface a bunch of code written by Evan Broder for his Jolly Rodger project.

Do you mean this in the context of subfields? So fields like:

Meteor.user().profile.firstName
Meteor.user().profile.lastName

//or

Meteor.user().profile: {
  firstName: 'John',
  lastName: 'Doe',
}

?

We actually don’t use that as DDP has no reactivity on subfields

For example explaines this. Or has this been solved between in case we missed it?

As for this issue we just name fields properly as top-level fields which works out:

Meteor.user().firstName
Meteor.user().lastName

No, I’m just talking TypeScript in this case. I’ve always been a staunch advocate of keeping a flat document structure due to this limitation of Meteor’s mergebox, which is why I created socialize:user-profile for the example scenario, but that’s a topic for another time.

To clarify what I mean above take the following code as an example.

import { Mongo } from 'meteor/mongo';
import z from 'zod';

export const ProfileSchema = z.object({
  firstName: z.string(),
  lastName: z.string(),
  birthDate: z.date(),
  bio: z.string(),
});

export type Profile = z.infer<typeof ProfileSchema>;
export const ProfilesCollection = new Mongo.Collection<Profile>('profiles');
export const p1 = ProfilesCollection.findOne({ firstName: 'John' });
export const p2 = ProfilesCollection.findOne({ firstName: 'John' }, { fields: { firstName: 1 } });

No we see here that we have 2 similar queries to find a single document, the difference is that we restrict the second one to the firstName field using { fields: { firstName: 1 }}. This of course means that the return will be an object with the shape { _id: string, firstName: string } or it will be undefined if no document is found. The issue is that if we examine the information typescript knows about this return, we find out that it thinks we have a full profile when a document is found as exampled by this screenshot using 2 slash to surface TypeScript info.

If you notice, it also doesn’t know that there is going to be an _id field on the document as well.This means TypeScript has given us improper information which is a source of totally preventable runtime errors.

If Meteor is ever to be taken as a serious tool for creating large scale codebases, the type safety story is going to have to get a lot better.

Can someone please give me (a related) advice?

It looks like I am unable to use zodern:relay in a Svelte project (and React probably as well).

Does it still work?

Is GitHub - Grubba27/meteor-rpc: Meteor Methods Evolved something I should prefer (it doesn’t look as simple as relay)?

Has anyone made any of these work with Svelte?