{"id":298,"date":"2017-05-04T17:46:09","date_gmt":"2017-05-04T09:46:09","guid":{"rendered":"https:\/\/colliot.me\/?p=298"},"modified":"2017-11-20T06:56:44","modified_gmt":"2017-11-19T22:56:44","slug":"%e4%b8%80%e4%b8%aa%e4%b8%8d%e6%ad%a3%e7%a1%ae%e7%9a%84-typescript-%e7%b1%bb%e5%9e%8b%e5%ae%9a%e4%b9%89%e5%bc%95%e5%8f%91%e7%9a%84%e8%a1%80%e6%a1%88","status":"publish","type":"post","link":"https:\/\/colliot.org\/en\/2017\/05\/%e4%b8%80%e4%b8%aa%e4%b8%8d%e6%ad%a3%e7%a1%ae%e7%9a%84-typescript-%e7%b1%bb%e5%9e%8b%e5%ae%9a%e4%b9%89%e5%bc%95%e5%8f%91%e7%9a%84%e8%a1%80%e6%a1%88\/","title":{"rendered":"An incident caused by an incorrect TypeScrpit type definition of Mongoose"},"content":{"rendered":"<p>The TypeScript type definition of Mongoose are not fully correct!<\/p>\n<p>I inspected (or debugged) a nodejs server project of <a href=\"http:\/\/lingsamuel.github.io\">Little Gengsha<\/a>&#8216;s, where he&#8217;s using koa as the server and mongodb as the data store. The project seems called &#8220;Writer&#8217;s Platform&#8221;, which seemed very primitive then. There were only two routes, a <code>get<\/code>, a <code>post<\/code>, the latter of which is used to submit an <code>article<\/code>\u00a0to mongodb. During the test he discovered a problem &#8212; the body of the response should be changed in the callback of the <code>save<\/code>\u00a0function, but the action response is &#8220;OK&#8221;, which is the default response of koa.<\/p>\n<pre class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"original code\">router.post('\/path', (ctx, next) =&gt; {\r\n    const article = new Article( \/* something *\/ );\r\n    article.save((err, article) =&gt; {\r\n      if (err){\r\n        ctx.body = err;\r\n      } else {\r\n        ctx.body = \"success\";\r\n      }\r\n    });\r\n})<\/pre>\n<p><!--more--><\/p>\n<p>At the first sight\u00a0I discovered that<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">next()<\/code>\u00a0is not returned in <code>\/* body *\/<\/code> of\u00a0<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">router.post('\/path', (ctx, next) =&gt; { \/* body *\/ });<\/code>This may cause the response to return early before the <code>save<\/code>\u00a0action calls back. But simply return a <code>next()<\/code>\u00a0did not help.<\/p>\n<p>Then I thought it could be due to the chaos of the versions of the koa ecosystem. The versions of koa and koa-router are all mixed. They both defaulted to the old version while people would like to use the new, under-development version. Koa advanced afterwards, while koa-router still defaults to the older version. I once encountered a problem caused by such incompatibility and solved it for someone.<\/p>\n<p>But this time it&#8217;s not the case. We switched from promise to\u00a0<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">async\/await<\/code>, but the problem persists.<\/p>\n<pre class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"3,7,11-13\" data-caption=\"using promise\">router.post('\/path', (ctx, next) =&gt; {\r\n    const article = new Article( \/* something *\/ );\r\n    return article.save((err, saved) =&gt; {\r\n      if (err){\r\n        ctx.body = err;\r\n      } else {\r\n        console.log(saved);\r\n        ctx.body = \"success\";\r\n      }\r\n      \/\/ or `return next();` here.\r\n    }).then((thing) =&gt; {\r\n        console.log(thing);\r\n        return next();\r\n    });\r\n})<\/pre>\n<p>(However, we discovered that \u00a0<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">thing<\/code>\u00a0is <code>undefined<\/code>,\u00a0which turns out to be the key)<\/p>\n<p>I was then upset with the signatures of the various functions (like what should be returned in the callback of <code>router.get()<\/code>, or what is the type of <code>next<\/code>), so I proposed using TypeScript for auxiliary checks.<\/p>\n<p>We began to suspect that\u00a0\u00a0<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">article.save<\/code>\u00a0is not working as expected. The function, which is similar to the only sample code from koa regarding async operations, is expected to work as demonstrated by the doc\uff1a<\/p>\n<pre class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"3-7\" data-caption=\"koa router official demo\">router.get(\r\n  '\/users\/:id',\r\n  function (ctx, next) {\r\n    return User.findOne(ctx.params.id).then(function(user) {\r\n      ctx.user = user;\r\n      return next();\r\n\r\n    });\r\n  },\r\n  function (ctx) {\r\n    console.log(ctx.user);\r\n    \/\/ =&gt; { id: 17, name: \"Alex\" }\r\n  }\r\n);<\/pre>\n<p>So we specially inspected the TypeScript type definition of mongoose, in hope of finding any misuse. The \u00a0<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">article<\/code>\u00a0is an instance of <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">Article<\/code>, while <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">Article<\/code>\u00a0is actually a class (not only a instance, but also a class itself) dynamically constructed by\u00a0mongoose. It is reflected in the type system:<\/p>\n<pre class=\"prettyprint lang-typescript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"type definition of &#96;Model&#96;\">interface Model&lt;T extends Document&gt; extends NodeJS.EventEmitter, ModelProperties {\r\n    new(doc?: Object): T;\r\n    \/* other things *\/\r\n}<\/pre>\n<p>and <code>article<\/code> is just the\u00a0<code>&lt;T extends Document&gt;<\/code>\uff0c\u00a0<code>Document<\/code>&#8216;s <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">save<\/code>method is written as follows\uff1a<\/p>\n<pre class=\"prettyprint lang-typescript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"8-9\" data-caption=\"type definition of &#96;.save&#96;\">    \/**\r\n     * Saves this document.\r\n     * @param options options optional options\r\n     * @param options.safe overrides schema's safe option\r\n     * @param options.validateBeforeSave set to false to save without validating.\r\n     * @param fn optional callback\r\n     *\/\r\n    save(options?: SaveOptions, fn?: (err: any, product: this, numAffected: number) =&gt; void): Promise&lt;this&gt;;\r\n    save(fn?: (err: any, product: this, numAffected: number) =&gt; void): Promise&lt;this&gt;;<\/pre>\n<p>(taken from the result of\u00a0<code class=\"prettyprint lang-powershell\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">npm install @types\/mongoose<\/code>, which somehow differs from that on the github repo of\u00a0DefinitelyTyped .) \u00a0Seeing the <code class=\"prettyprint lang-typescript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">Promise&lt;this&gt;<\/code>, I was convinced that our use of the function is valid. But the fact is the <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">thing<\/code>\u00a0returned above is <code>undefined<\/code>. So are there any peculiarities?<\/p>\n<p>Since TypeScript was not reliable then, we could only resort to the source code. Directly searching for\u00a0\u00a0<code>.prototype.save<\/code>\u00a0will yield us the relevant code. It&#8217;s actually <a href=\"https:\/\/github.com\/Automattic\/mongoose\/blob\/773639adbde210945598fcd6cf11a65d418fe5f2\/lib\/model.js#L307\">here<\/a>.\u00a0But seeing it makes me more desperate:<\/p>\n<pre class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"15\" data-caption=\"source code of &#96;.save&#96;\">Model.prototype.save = function(options, fn) {\r\n  if (typeof options === 'function') {\r\n    fn = options;\r\n    options = undefined;\r\n  }\r\n\r\n  if (!options) {\r\n    options = {};\r\n  }\r\n\r\n  if (fn) {\r\n    fn = this.constructor.$wrapCallback(fn);\r\n  }\r\n\r\n  return this.$__save(options, fn);\r\n};<\/pre>\n<p>It returns the result of <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">this.$__save<\/code><span class=\"pl-en\">, but\u00a0<a href=\"https:\/\/github.com\/Automattic\/mongoose\/blob\/773639adbde210945598fcd6cf11a65d418fe5f2\/lib\/model.js#L210\">that function<\/a>\u00a0does not return actually! How could it return a promise from a function that returns nothing\uff1f<\/span><\/p>\n<p>We thought of another ultimate measure: inspecting the call stack\u3002We used <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">console.trace<\/code>\u00a0for it. Although no direct clue shown, it still gave us some hint\u2014\u2014<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">save<\/code>\u00a0is not called directly, but wrapped with some <code>hook<\/code>.<\/p>\n<p>Suddenly, I thought about directly <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">console.log(article.save)<\/code>, which told us that <code>article.save<\/code>\u00a0is actually the result of a\u00a0<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">wrappedPointCut<\/code>\u00a0function! Searching for <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">wrappedPointCut <\/code>in the code almost revealed the answer. The crucial part is\u00a0<a href=\"https:\/\/github.com\/Automattic\/mongoose\/blob\/fa2caf50a9c7c95a16e11f2aa95fa516b28cbce2\/lib\/services\/model\/applyHooks.js#L102\">here<\/a>:<\/p>\n<pre class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"7,33,21\" data-caption=\"wrapped pointcut\">model.prototype[pointCut] = (function(_newName) {\r\n      return function wrappedPointCut() {\r\n        var Promise = PromiseProvider.get();\r\n\r\n        var _this = this;\r\n        \/* something omitted for simplicity *\/\r\n        var promise = new Promise.ES6(function(resolve, reject) {\r\n          args.push(function(error) {\r\n            if (error) {\r\n              \/* something omitted for simplicity *\/\r\n              reject(error);\r\n              return;\r\n            }\r\n            $results = Array.prototype.slice.call(arguments, 1);\r\n            resolve.apply(promise, $results);\r\n          });\r\n          _this[_newName].apply(_this, args);\r\n        });\r\n        if (fn) {\r\n          \/* something omitted for simplicity *\/\r\n          return promise.then(\r\n            function() {\r\n              process.nextTick(function() {\r\n                fn.apply(null, [null].concat($results));\r\n              });\r\n            },\r\n            function(error) {\r\n              process.nextTick(function() {\r\n                fn(error);\r\n              });\r\n            });\r\n        }\r\n        return promise;\r\n      };\r\n    })(newName);<\/pre>\n<p>Without <code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">fn<\/code>\uff0c a promise defined on line 7 will be returned on line 33, while with a<code class=\"prettyprint lang-javascript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">fn<\/code>\u00a0in the parameter, the promise defined would be chained with a <code>then<\/code>\u00a0on line 21, which returns nothing &#8212; In this case, the return value should be\u00a0<code class=\"prettyprint lang-typescript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">Promise&lt;undefined&gt;<\/code>\u00a0instead of <code class=\"prettyprint lang-typescript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"\" data-caption=\"\">Promise&lt;this&gt;<\/code>\u2014\u2014 We were fooled by such sloppy type definition! How I wish it could be annotated correctly from the beginning, just like this:<\/p>\n<pre class=\"prettyprint lang-typescript\" data-start-line=\"1\" data-visibility=\"visible\" data-highlight=\"1-2\" data-caption=\"suggested correct type definition\">    save(options: SaveOptions, fn: (err: any, product: this, numAffected: number) =&gt; void): Promise&lt;undefined&gt;;\r\n    save(fn: (err: any, product: this, numAffected: number) =&gt; void): Promise&lt;undefined&gt;;\r\n    save(options?: SaveOptions): Promise&lt;this&gt;;\r\n    save(): Promise&lt;this&gt;;<\/pre>\n<p style=\"text-align: right;\">May, 4, 2017<\/p>\n<p><\/p>","protected":false},"excerpt":{"rendered":"<p>The TypeScript type definition of Mongoose are not fully correct! I inspected (or debugged) a nodejs server project of Little Gengsha&#8216;s, where he&#8217;s using koa as the server and mongodb as the data store. The project seems called &#8220;Writer&#8217;s Platform&#8221;, which seemed very primitive then. There were only two routes, a get, a post, the &hellip; <a href=\"https:\/\/colliot.org\/en\/2017\/05\/%e4%b8%80%e4%b8%aa%e4%b8%8d%e6%ad%a3%e7%a1%ae%e7%9a%84-typescript-%e7%b1%bb%e5%9e%8b%e5%ae%9a%e4%b9%89%e5%bc%95%e5%8f%91%e7%9a%84%e8%a1%80%e6%a1%88\/\" class=\"more-link\">Continue reading<span class=\"screen-reader-text\"> &#8220;An incident caused by an incorrect TypeScrpit type definition of Mongoose&#8221;<\/span><\/a><\/p>\n","protected":false},"author":1,"featured_media":318,"comment_status":"open","ping_status":"open","sticky":false,"template":"","format":"standard","meta":[],"categories":[15,16],"tags":[17,20,19,18,21],"_links":{"self":[{"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/posts\/298"}],"collection":[{"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/comments?post=298"}],"version-history":[{"count":10,"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/posts\/298\/revisions"}],"predecessor-version":[{"id":398,"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/posts\/298\/revisions\/398"}],"wp:featuredmedia":[{"embeddable":true,"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/media\/318"}],"wp:attachment":[{"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/media?parent=298"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/categories?post=298"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/colliot.org\/en\/wp-json\/wp\/v2\/tags?post=298"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}