From 2f995d071e2d5073773143bc0be628e0e09e4d41 Mon Sep 17 00:00:00 2001 From: Evan Broder Date: Wed, 8 Jan 2020 13:01:05 -0800 Subject: [PATCH] [meteor] Make check an assertion function (#41414) Meteor's check is an assertion function, and Match.test returns a type predicate. TypeScript 3.7 is expressive enough to capture this. This lets us reduce duplication in Meteor code (e.g. needing to keep function parameter types and check calls in sync with each other). --- types/angular-meteor/index.d.ts | 2 +- types/meteor-astronomy/index.d.ts | 2 +- types/meteor-jboulhous-dev/index.d.ts | 2 +- types/meteor-persistent-session/index.d.ts | 2 +- .../meteor-prime8consulting-oauth2/index.d.ts | 2 +- types/meteor-publish-composite/index.d.ts | 2 +- types/meteor-roles/index.d.ts | 2 +- types/meteor-universe-i18n/index.d.ts | 2 +- types/meteor/check.d.ts | 47 ++++++++++++++----- types/meteor/globals/check.d.ts | 47 ++++++++++++++----- types/meteor/index.d.ts | 3 +- types/meteor/test/globals/meteor-tests.ts | 30 +++++++++--- types/meteor/test/meteor-tests.ts | 33 ++++++++++--- 13 files changed, 128 insertions(+), 48 deletions(-) diff --git a/types/angular-meteor/index.d.ts b/types/angular-meteor/index.d.ts index 11d782ee47..52df3ad7cd 100644 --- a/types/angular-meteor/index.d.ts +++ b/types/angular-meteor/index.d.ts @@ -2,7 +2,7 @@ // Project: https://github.com/Urigo/angular-meteor // Definitions by: Peter Grman // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// diff --git a/types/meteor-astronomy/index.d.ts b/types/meteor-astronomy/index.d.ts index 686d871bcf..35db4581c2 100644 --- a/types/meteor-astronomy/index.d.ts +++ b/types/meteor-astronomy/index.d.ts @@ -2,7 +2,7 @@ // Project: https://github.com/jagi/meteor-astronomy/ // Definitions by: Igor Golovin // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// diff --git a/types/meteor-jboulhous-dev/index.d.ts b/types/meteor-jboulhous-dev/index.d.ts index 28d7ec19cd..41642555f5 100644 --- a/types/meteor-jboulhous-dev/index.d.ts +++ b/types/meteor-jboulhous-dev/index.d.ts @@ -2,7 +2,7 @@ // Project: https://github.com/jboulhous/dev // Definitions by: Robbie Van Gorkom // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// diff --git a/types/meteor-persistent-session/index.d.ts b/types/meteor-persistent-session/index.d.ts index 5efaed6dfa..bca0371649 100644 --- a/types/meteor-persistent-session/index.d.ts +++ b/types/meteor-persistent-session/index.d.ts @@ -2,7 +2,7 @@ // Project: https://github.com/okgrow/meteor-persistent-session // Definitions by: Robbie Van Gorkom // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// diff --git a/types/meteor-prime8consulting-oauth2/index.d.ts b/types/meteor-prime8consulting-oauth2/index.d.ts index f9f08efca3..94a187b599 100644 --- a/types/meteor-prime8consulting-oauth2/index.d.ts +++ b/types/meteor-prime8consulting-oauth2/index.d.ts @@ -2,7 +2,7 @@ // Project: https://github.com/prime-8-consulting/meteor-oauth2/ // Definitions by: Robbie Van Gorkom // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// diff --git a/types/meteor-publish-composite/index.d.ts b/types/meteor-publish-composite/index.d.ts index 1582e569ba..ab46c8761b 100644 --- a/types/meteor-publish-composite/index.d.ts +++ b/types/meteor-publish-composite/index.d.ts @@ -3,7 +3,7 @@ // Definitions by: Robert Van Gorkom // Matthew Zartman // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// diff --git a/types/meteor-roles/index.d.ts b/types/meteor-roles/index.d.ts index 348f6eef9c..6fccaee8a2 100644 --- a/types/meteor-roles/index.d.ts +++ b/types/meteor-roles/index.d.ts @@ -3,7 +3,7 @@ // Definitions by: Robbie Van Gorkom // Matthew Zartman // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// diff --git a/types/meteor-universe-i18n/index.d.ts b/types/meteor-universe-i18n/index.d.ts index 1596ceef81..6067d16862 100644 --- a/types/meteor-universe-i18n/index.d.ts +++ b/types/meteor-universe-i18n/index.d.ts @@ -2,7 +2,7 @@ // Project: meteor-universe-i18n // Definitions by: Mathias Scherer // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// /// diff --git a/types/meteor/check.d.ts b/types/meteor/check.d.ts index 065850fb4e..c4a4ee4b0c 100644 --- a/types/meteor/check.d.ts +++ b/types/meteor/check.d.ts @@ -1,24 +1,45 @@ declare module "meteor/check" { module Match { - var Any: any; - var String: any; - var Integer: any; - var Boolean: any; - var undefined: any; - var Object: any; + interface Matcher { _meteorCheckMatcherBrand: void } + export type Pattern = + typeof String | + typeof Number | + typeof Boolean | + typeof Object | + typeof Function | + (new (...args: any[]) => any) | + undefined | null | string | number | boolean | + [Pattern] | + {[key: string]: Pattern} | + Matcher; + export type PatternMatch = + T extends Matcher ? U : + T extends typeof String ? string : + T extends typeof Number ? number : + T extends typeof Boolean ? boolean : + T extends typeof Object ? object : + T extends typeof Function ? Function : + T extends undefined | null | string | number | boolean ? T : + T extends new (...args: any[]) => infer U ? U : + T extends [Pattern] ? PatternMatch[] : + T extends {[key: string]: Pattern} ? {[K in keyof T]: PatternMatch} : + unknown; - function Maybe(pattern: any): boolean; + var Any: Matcher; + var Integer: Matcher; - function Optional(pattern: any): boolean; + function Maybe(pattern: T): Matcher | undefined | null>; - function ObjectIncluding(dico: any): boolean; + function Optional(pattern: T): Matcher | undefined>; - function OneOf(...patterns: any[]): any; + function ObjectIncluding(dico: T): Matcher>; - function Where(condition: any): any; + function OneOf(...patterns: T[]): Matcher>; - function test(value: any, pattern: any): boolean; + function Where(condition: (val: any) => boolean): Matcher; + + function test(value: any, pattern: T): value is PatternMatch; } - function check(value: any, pattern: any): void; + function check(value: any, pattern: T): asserts value is Match.PatternMatch; } diff --git a/types/meteor/globals/check.d.ts b/types/meteor/globals/check.d.ts index 1b1ec39736..d4433b4479 100644 --- a/types/meteor/globals/check.d.ts +++ b/types/meteor/globals/check.d.ts @@ -1,22 +1,43 @@ declare module Match { - var Any: any; - var String: any; - var Integer: any; - var Boolean: any; - var undefined: any; - var Object: any; + interface Matcher { _meteorCheckMatcherBrand: void } + export type Pattern = + typeof String | + typeof Number | + typeof Boolean | + typeof Object | + typeof Function | + (new (...args: any[]) => any) | + undefined | null | string | number | boolean | + [Pattern] | + {[key: string]: Pattern} | + Matcher; + export type PatternMatch = + T extends Matcher ? U : + T extends typeof String ? string : + T extends typeof Number ? number : + T extends typeof Boolean ? boolean : + T extends typeof Object ? object : + T extends typeof Function ? Function : + T extends undefined | null | string | number | boolean ? T : + T extends new (...args: any) => infer U ? U : + T extends [Pattern] ? PatternMatch[] : + T extends {[key: string]: Pattern} ? {[K in keyof T]: PatternMatch} : + unknown; - function Maybe(pattern: any): boolean; + var Any: Matcher; + var Integer: Matcher; - function Optional(pattern: any): boolean; + function Maybe(pattern: T): Matcher | undefined | null>; - function ObjectIncluding(dico: any): boolean; + function Optional(pattern: T): Matcher | undefined>; - function OneOf(...patterns: any[]): any; + function ObjectIncluding(dico: T): Matcher>; - function Where(condition: any): any; + function OneOf(...patterns: T[]): Matcher>; - function test(value: any, pattern: any): boolean; + function Where(condition: (val: any) => boolean): Matcher; + + function test(value: any, pattern: T): value is PatternMatch; } -declare function check(value: any, pattern: any): void; +declare function check(value: any, pattern: T): asserts value is Match.PatternMatch; diff --git a/types/meteor/index.d.ts b/types/meteor/index.d.ts index 4817ec087e..f9e84f6147 100644 --- a/types/meteor/index.d.ts +++ b/types/meteor/index.d.ts @@ -13,8 +13,9 @@ // alesn // Per Bergland // Nicusor Chiciuc +// Evan Broder // Definitions: https://github.com/DefinitelyTyped/DefinitelyTyped -// TypeScript Version: 2.8 +// Minimum TypeScript Version: 3.7 /// /// diff --git a/types/meteor/test/globals/meteor-tests.ts b/types/meteor/test/globals/meteor-tests.ts index bf0bff5e64..5dd14d48d2 100644 --- a/types/meteor/test/globals/meteor-tests.ts +++ b/types/meteor/test/globals/meteor-tests.ts @@ -66,8 +66,10 @@ Meteor.publish("adminSecretInfo", function () { return Rooms.find({ admin: this.userId }, { fields: { secretInfo: 1 } }); }); -Meteor.publish("roomAndMessages", function (roomId: string) { +Meteor.publish("roomAndMessages", function (roomId: unknown) { check(roomId, String); + // $ExpectType string + roomId; return [ Rooms.find({ _id: roomId }, { fields: { secretInfo: 0 } }), Messages.find({ roomId: roomId }) @@ -77,9 +79,11 @@ Meteor.publish("roomAndMessages", function (roomId: string) { /** * Also from Publish and Subscribe, Meteor.publish section */ -Meteor.publish("counts-by-room", function (roomId: string) { +Meteor.publish("counts-by-room", function (roomId: unknown) { var self = this; check(roomId, String); + // $ExpectType string + roomId; var count = 0; var initializing = true; var handle = Messages.find({ roomId: roomId }).observeChanges({ @@ -136,10 +140,15 @@ Tracker.autorun(function () { * From Methods, Meteor.methods section */ Meteor.methods({ - foo: function (arg1: string, arg2: number[]) { + foo: function (arg1: unknown, arg2: unknown) { check(arg1, String); check(arg2, [Number]); + // $ExpectType string + arg1; + // $ExpectType number[] + arg2; + var you_want_to_throw_an_error = true; if (you_want_to_throw_an_error) throw new Meteor.Error("404", "Can't find my pants"); @@ -595,14 +604,16 @@ var body = Template.body; */ var Chats = new Mongo.Collection('chats'); -Meteor.publish("chats-in-room", function (roomId: string) { +Meteor.publish("chats-in-room", function (roomId: unknown) { // Make sure roomId is a string, not an arbitrary mongo selector object. check(roomId, String); + // $ExpectType string + roomId; return Chats.find({ room: roomId }); }); Meteor.methods({ - addChat: function (roomId: string, message: { text: string, timestamp: Date, tags: string }) { + addChat: function (roomId: unknown, message: unknown) { check(roomId, String); check(message, { text: String, @@ -611,7 +622,14 @@ Meteor.methods({ tags: Match.Optional('Test String') }); - // ... do something with the message ... + // $ExpectType string + roomId; + // $ExpectType string + message.text; + // $ExpectType Date + message.timestamp; + // $ExpectType "Test String" + message.tags; } }); diff --git a/types/meteor/test/meteor-tests.ts b/types/meteor/test/meteor-tests.ts index 589e36c04c..062c59697b 100644 --- a/types/meteor/test/meteor-tests.ts +++ b/types/meteor/test/meteor-tests.ts @@ -80,8 +80,10 @@ Meteor.publish("adminSecretInfo", function () { return Rooms.find({ admin: this.userId }, { fields: { secretInfo: 1 } }); }); -Meteor.publish("roomAndMessages", function (roomId: string) { +Meteor.publish("roomAndMessages", function (roomId: unknown) { check(roomId, String); + // $ExpectType string + roomId; return [ Rooms.find({ _id: roomId }, { fields: { secretInfo: 0 } }), Messages.find({ roomId: roomId }) @@ -91,9 +93,11 @@ Meteor.publish("roomAndMessages", function (roomId: string) { /** * Also from Publish and Subscribe, Meteor.publish section */ -Meteor.publish("counts-by-room", function (roomId: string) { +Meteor.publish("counts-by-room", function (roomId: unknown) { var self = this; check(roomId, String); + // $ExpectType string + roomId; var count = 0; var initializing = true; var handle = Messages.find({ roomId: roomId }).observeChanges({ @@ -150,10 +154,15 @@ Tracker.autorun(function () { * From Methods, Meteor.methods section */ Meteor.methods({ - foo: function (arg1: string, arg2: number[]) { + foo: function (arg1: unknown, arg2: unknown) { check(arg1, String); check(arg2, [Number]); + // $ExpectType string + arg1; + // $ExpectType number[] + arg2; + var you_want_to_throw_an_error = true; if (you_want_to_throw_an_error) throw new Meteor.Error("404", "Can't find my pants"); @@ -609,14 +618,16 @@ var body = Template.body; */ var Chats = new Mongo.Collection('chats'); -Meteor.publish("chats-in-room", function (roomId: string) { +Meteor.publish("chats-in-room", function (roomId: unknown) { // Make sure roomId is a string, not an arbitrary mongo selector object. check(roomId, String); + // $ExpectType string + roomId; return Chats.find({ room: roomId }); }); Meteor.methods({ - addChat: function (roomId: string, message: { text: string, timestamp: Date, tags: string }) { + addChat: function (roomId: unknown, message: unknown) { check(roomId, String); check(message, { text: String, @@ -625,7 +636,14 @@ Meteor.methods({ tags: Match.Optional('Test String') }); - // ... do something with the message ... + // $ExpectType string + roomId; + // $ExpectType string + message.text; + // $ExpectType Date + message.timestamp; + // $ExpectType "Test String" + message.tags; } }); @@ -633,8 +651,9 @@ Meteor.methods({ * From Match patterns section */ var pat = { name: Match.Optional('test') }; -check({ name: "something" }, pat) // OK +check({ name: "test" }, pat) // OK check({}, pat) // OK +check({ name: "something" }, pat) // Throws an exception check({ name: undefined }, pat) // Throws an exception // Outside an object