Thursday, 9 January 2014

Upgrading to TypeScript 0.9.5: A Personal Memoir

I recently made the step to upgrade from TypeScript 0.9.1.1 to 0.9.5. To my surprise this process was rather painful and certainly not an unalloyed pleasure. Since I'm now on the other side, so to speak, I thought I'd share my experience and cast back a rope bridge to those about to journey over the abyss.

TL;DR

TypeScript 0.9.5 is worth making the jump to. However, if you are using Visual Studio (as I would guess many are) then you should be aware of a number of problems with the TypeScript Visual Studio tooling for TS 0.9.5. These problems can be worked around if you follow the instructions in this post.

Upgrading the Plugin

At home I upgraded the moment TS 0.9.5 was released. This allowed me to help with migrating the Definitely Typed typings over from 0.9.1.1. And allowed me to give TS 0.9.5 a little test drive. However, I deliberately held off performing the upgrade at work until I knew that all the Definitely Typed typings had been upgraded. This was completed by the end of 2013. So in the new year it seemed a good time to make the move.

If, like me, you are using TypeScript inside Visual Studio then you'd imagine it's as simple as closing down VS, uninstalling TypeScript 0.9.1.1 from Programs and Features and then installing the new plugin. And it is if you are running IE 10 or IE 11 on your Windows machine. If you are running a lower IE version then there is a problem.

Regrettably, the TypeScript 0.9.5 plugin installer has a dependency on IE 10. Fortunately TypeScript itself has no dependency on IE 10 at all (and why would it?). This dependency appears to have been a mistake. I raised it as an issue and the TS team have said that this will be resolved in the next major release.

Happily there is a workaround if you're running IE 9 or lower which has been noted in the comments underneath the TS 0.9.5 release blog post. All you do is set the HKEY_LOCAL_MACHINE\SOFTWARE\Wow6432Node\Microsoft\Internet Explorer\svcVersion registry key value to 10.0.9200.16384 for the duration of the install.

First hurdle jumped, the upgrade continues simple enough. Then the fun starts...

Declaration Merging is dead... Sort of

Having upgraded my plugin I opened up the project I'm working on in Visual Studio. I used NuGet to upgrade all the Definitely Typed packages to the latest (TS 0.9.5) versions. Then I tried, and failed, to compile. It was the the most obscure error I've seen in a while:

VSTSC : tsc.js(37574, 25) Microsoft JScript runtime error : Unable to get value of the property 'wrapsSomeTypeParameter': object is null or undefined

As you can see there was no indication where in my code the problem was being caused. Fortunately someone had already suffered this particular problem and logged an issue here. Digging through the comments I found a common theme; everyone experiencing the problem was using the Q typings. So what's up with that?

Strangely, if you directly referenced the Q typings everything was okay - which is how the Definitely Typed tests came to pass in the first place. But if you wanted to make use of these typings with implicit referencing (in Visual Studio since TS 0.9.1, all TypeScript files in a project are considered to be referencing each other) - well it doesn't work.

I decided to take a look at the Q typings at this point to see what was so upsetting about them. The one thing that was obvious was that these typings make use of Declaration Merging. And this made them slightly different to most of the other typing libraries that I was using. So I decided to refactor the Q typings to use the more interface driven approach the other typing libraries used in the hope that might resolve the issue.

Roughly speaking I went from:

declare function Q<T>(promise: Q.IPromise<T>): Q.Promise<T>;
declare function Q<T>(promise: JQueryPromise<T>): Q.Promise<T>;
declare function Q<T>(value: T): Q.Promise<T>;

declare module Q {
    //… functions etc in here
}

declare module "q" {
    export = Q;
}

To:

interface QIPromise<T> {
    //… functions etc in here
}
 
interface QDeferred<T> {
    //… functions etc in here
}
 
interface QPromise<T> {
    //… functions etc in here
}
 
interface QPromiseState<T> {
    //… functions etc in here
}
 
interface QStatic {
 
    (promise: QIPromise<T>): QPromise<T>;
    (promise: JQueryPromise<T>): QPromise<T>;
    (value: T): QPromise<T>;
 
    //… other functions etc continue here
}
 
declare module "q" {
    export = Q;
}
declare var Q: QStatic;

And that fixed the obscure 'wrapsSomeTypeParameter' error. The full source code of these amended typings can be found as a GitHub Repo here in case you want to use it yourself. (I did originally consider adding this to Definitely Typed but opted not to in the end - see discussion on GitHub.)

The Promised Land

You're there. You've upgraded to the new plugin and the new typings. All is compiling as it should and the language service is working as well. Was it worth it? I think yes, for the following reasons:

  1. TS 0.9.5 compiles faster, and hogs less memory.

  2. When we compiled with TS 0.9.5 we found there were a couple of bugs in our codebase which the tightened up compiler was now detecting. Essentially where we'd assumed types were flowing through to functions there were a couple of occasions with TS 0.9.1.1 where they weren't. Where we'd assumed we had a type of T available in a function whereas it was actually a type of any. I was really surprised that this was the case since we were already making use of noImplicitAny compiler flag in our project. So where a type had changed and a retired property was being referenced TS 0.9.5 picked up an error that TS 0.9.1.1 had not. Good catch!

  3. And finally (and I know these are really minor), the compiled JS is a little different now. Firstly, the compiled JS features all of TypeScript comments in the positions that you might hope for. Previously it seemed that about 75% came along for the ride and ended up in some strange locations sometimes. Secondly, enums are treated differently during compilation now - where it makes sense the actual backing value of an enum is used rather than going through the JavaScript construct. So it's a bit like a const I guess - presumably this allows JavaScript engines to optimise a little more.

I hope I haven't put you off with this post. I think TypeScript 0.9.5 is well worth making the leap for - and hopefully by reading this you'll have saved yourself from a few of the rough edges.