{"id":4527,"date":"2016-06-18T17:13:21","date_gmt":"2016-06-18T17:13:21","guid":{"rendered":"http:\/\/www.garysieling.com\/blog\/?p=4527"},"modified":"2016-06-18T17:13:21","modified_gmt":"2016-06-18T17:13:21","slug":"log4j-alternative-typescript","status":"publish","type":"post","link":"https:\/\/www.garysieling.com\/blog\/log4j-alternative-typescript\/","title":{"rendered":"Writing a Log4J alternative in TypeScript"},"content":{"rendered":"<p>An interesting exercise to understand TypeScript better is to write a logging utility.<\/p>\n<p>Typically, you want to be able to write code like this:<\/p>\n<pre lang=\"javascript\">\nlogger.log(LogLevel.error, \"A problem occurred\")\n<\/pre>\n<p>For extra credit, it&#8217;s nice to be able to also lazily evaluate the strings (prevents some needless concatenation):<\/p>\n<pre lang=\"javascript\">\nlogger.log(LogLevel.error, () => \"A problem occurred\")\n<\/pre>\n<p>TypeScript supports enums, so let&#8217;s first define one for the log level:<\/p>\n<pre lang=\"javascript\">\nenum LogLevel {\n  info, debug, error, critical, none\n}\n<\/pre>\n<p>When I was working through this, I thought I might try the Javascript trick, of storing values on a function (e.g the log level), like so:<\/p>\n<pre lang=\"javascript\">\nfunction log(level, message) {\n  if (log.level >= level) \n    console.log(message);\n}\n<\/pre>\n<p>Unfortunately TypeScript doesn&#8217;t like this, because &#8220;log&#8221; isn&#8217;t defined as having &#8220;level&#8221; as a member. You can usually get around this with index notation, like below, but this seems like cheating, and defeats the value of using TypeScript.<\/p>\n<pre lang=\"javascript\">\nfunction log(level, message) {\n  if (log[\"level\"] >= level) \n    console.log(message);\n}\n<\/pre>\n<p>Rather, if you want a private log level, you can wrap the whole thing in an extra level of functions. This way you can let people change the level, but still have some amount of control over the variable.<\/p>\n<pre lang=\"javascript\">\nconst logger =\n  <sup><a href=\"#footnote_0_4527\" id=\"identifier_0_4527\" class=\"footnote-link footnote-identifier-link\" title=\") =&gt; {\n    let currentLevel = LogLevel.debug;\n\n    return {\n      log: (level: LogLevel, message: string) =&gt; {\n        if (level &gt;= currentLevel) {\n          console.log(message);\n        }\n      },\n      setLevel(level: LogLevel) {\n        currentLevel = level;\n      }\n    })()\n\nIt&rsquo;s probably a good idea to avoid being dependent on console.log, as in a real system we might want to log to a file, network, etc. Log4j lets you control this through configuration. For our case, I&rsquo;d like to at least take the logging function as a parameter. \nWe can change the above code to accept a strongly-typed lambda:\n\nlet logger = \n   ((onLog: (value: string)=&gt;void ) =&gt; {\n     ..\n   }(console.log)\n\nAs I noted above, logging can be a performance impact, so it&rsquo;s nice not to evaluate the strings you&rsquo;re logging, if you&rsquo;re never going to put them anywhere. Consequently, we want to be able to detect whether the input is a function or not.\nThere are apparently a number of ways to do this &ndash; we could do it ourselves, or depend on a faster but puzzling implementation in a library ((http:\/\/stackoverflow.com\/questions\/5999998\/how-can-i-check-if-a-javascript-variable-is-function-type\">1<\/a><\/sup>. I don&#8217;t really want to introduce a dependency, so I&#8217;ve replicated here what the implementation of &#8220;isFunction&#8221; is in Underscore.<\/p>\n<pre lang=\"javascript\">\nfunction isFunction(obj) {\n  return !!(obj && obj.constructor && obj.call && obj.apply);\n}\n<\/pre>\n<p>Now, if we were writing Javascript, we could call it inside our logging code:<\/p>\n<pre lang=\"javascript\">\nif (log.level >= level) \n  onLog(\n    isFunction(message) ? \n    message() : \n    message);\n<\/pre>\n<p>What will actually happen is that TypeScript will complain about the things we&#8217;ve done. The &#8220;isFunction&#8221; call doesn&#8217;t specify the type of what it gets (because that is the whole point), and we haven&#8217;t told TypeScript that our logger could take a function:<\/p>\n<pre>\njobs.ts(8,21): error TS7006: \nParameter 'obj' implicitly has an 'any' type.\n\njobs.ts(20,17): error TS2349: \nCannot invoke an expression whose type lacks a call signature.\n<\/pre>\n<p>The first error is easily fixed, although in the long run this may not be a good solution:<\/p>\n<pre lang=\"javascript\">\nfunction isFunction(obj: any) {\n  return !!(obj && obj.constructor && obj.call && obj.apply);\n}\n<\/pre>\n<p>To get our logging code to work, we need to make type that represents the message lambda (something that takes no arguments and returns a string). Once we&#8217;ve done this, we can exploit another awesome TypeScript feature, which is union types. This lets us declare that the argument is either a string or a specific type of function. This is a pretty common Javascript behavior, so it&#8217;s clearly necessary to make TypeScript viable at all- for instance, core Node APIs for file access return a Buffer or String, depending on whether you request an encoding.<\/p>\n<pre lang=\"javascript\">\ntype MessageLambda = () => string;\ntype Message = MessageLambda | string;\n<\/pre>\n<pre lang=\"javascript\">\nlog: (level: LogLevel, message: Message ) => {\n  ...\n}\n<\/pre>\n<p>At this point, we&#8217;re still getting TypeScript errors, but we&#8217;re getting close (even with these errors, the Javascript output should still work)<\/p>\n<pre>\njobs.ts(23,17): error TS2349: \nCannot invoke an expression whose type lacks a call signature.\n<\/pre>\n<p>What is really going on here is that you effectively need to check what the union type actually is, and then handle it appropriately. In Java, this would be a type check and a cast, and in functional languages like Scala, you would use pattern matching.<\/p>\n<p>The TypeScript syntax for this is bizarre, but most resembles Scala pattern matching. . This is referred to as &#8220;type guards&#8221; &#8211; once the code steps into the type guard<sup><a href=\"#footnote_1_4527\" id=\"identifier_1_4527\" class=\"footnote-link footnote-identifier-link\" title=\"https:\/\/www.typescriptlang.org\/docs\/handbook\/advanced-types.html\">2<\/a><\/sup>, the type of the thing you&#8217;re checking becomes more specific, hence how it is similar to pattern matching, although Scala&#8217;s pattern matching lets you do a lot of stuff you can&#8217;t do here.<\/p>\n<pre lang=\"javascript\">\nif (typeof message === \"function\") {\n  onLog(message())\n} else if (typeof message === \"string\") {\n  onLog(message);\n}\n<\/pre>\n<p>I think the chief difficulty that TypeScript has here is that the additional type information isn&#8217;t retained at runtime, similar to Java&#8217;s type erasure. Unlike Java, the TypeScript developers have found ways to make the language useful in spite of the erasure. The compiler seems to be quite fast, so the tradeoffs seems to be a reasonable ones.<\/p>\n<p>So to test this, we can do exactly what we&#8217;d expect:<\/p>\n<pre lang=\"javascript\">\nlogger.log(LogLevel.error, \"error\");\nlogger.log(LogLevel.debug, \"debug\");\nlogger.log(LogLevel.debug, () => \"lambda\");\nlogger.log(LogLevel.info, \"info\");\n<\/pre>\n<pre>\nerror\ndebug\nlambda\n<\/pre>\n<ol class=\"footnotes\"><li id=\"footnote_0_4527\" class=\"footnote\">) => {\n    let currentLevel = LogLevel.debug;\n\n    return {\n      log: (level: LogLevel, message: string) => {\n        if (level >= currentLevel) {\n          console.log(message);\n        }\n      },\n      setLevel(level: LogLevel) {\n        currentLevel = level;\n      }\n    })()\n<\/pre>\n<p>It&#8217;s probably a good idea to avoid being dependent on console.log, as in a real system we might want to log to a file, network, etc. Log4j lets you control this through configuration. For our case, I&#8217;d like to at least take the logging function as a parameter. <\/p>\n<p>We can change the above code to accept a strongly-typed lambda:<\/p>\n<pre lang=\"javascript\">\nlet logger = \n   ((onLog: (value: string)=>void ) => {\n     ..\n   }(console.log)\n<\/pre>\n<p>As I noted above, logging can be a performance impact, so it&#8217;s nice not to evaluate the strings you&#8217;re logging, if you&#8217;re never going to put them anywhere. Consequently, we want to be able to detect whether the input is a function or not.<\/p>\n<p>There are apparently a number of ways to do this &#8211; we could do it ourselves, or depend on a faster but puzzling implementation in a library ((http:\/\/stackoverflow.com\/questions\/5999998\/how-can-i-check-if-a-javascript-variable-is-function-type<span class=\"footnote-back-link-wrapper\"> [<a href=\"#identifier_0_4527\" class=\"footnote-link footnote-back-link\">&#8617;<\/a>]<\/span><\/li><li id=\"footnote_1_4527\" class=\"footnote\">https:\/\/www.typescriptlang.org\/docs\/handbook\/advanced-types.html<span class=\"footnote-back-link-wrapper\"> [<a href=\"#identifier_1_4527\" class=\"footnote-link footnote-back-link\">&#8617;<\/a>]<\/span><\/li><\/ol>","protected":false},"excerpt":{"rendered":"<p>Using TypeScript to build a system for logging<\/p>\n","protected":false},"author":1,"featured_media":0,"comment_status":"open","ping_status":"closed","sticky":false,"template":"","format":"standard","meta":{"om_disable_all_campaigns":false,"_monsterinsights_skip_tracking":false,"_monsterinsights_sitenote_active":false,"_monsterinsights_sitenote_note":"","_monsterinsights_sitenote_category":0,"footnotes":""},"categories":[4],"tags":[302,557],"aioseo_notices":[],"amp_enabled":true,"_links":{"self":[{"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/posts\/4527"}],"collection":[{"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/posts"}],"about":[{"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/types\/post"}],"author":[{"embeddable":true,"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/users\/1"}],"replies":[{"embeddable":true,"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/comments?post=4527"}],"version-history":[{"count":0,"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/posts\/4527\/revisions"}],"wp:attachment":[{"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/media?parent=4527"}],"wp:term":[{"taxonomy":"category","embeddable":true,"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/categories?post=4527"},{"taxonomy":"post_tag","embeddable":true,"href":"https:\/\/www.garysieling.com\/blog\/wp-json\/wp\/v2\/tags?post=4527"}],"curies":[{"name":"wp","href":"https:\/\/api.w.org\/{rel}","templated":true}]}}