Concept First Blog

IT consultancy, web development, data analysis and application development

TypeScript: Setting Up and Settling In

Recently we’ve been considering the move to something better than JavaScript for front-end development, offering type safety and organisational constructs that more readily support robust code.

I spent a week exploring TypeScript (a typed ‘superset’ of JavaScript) as a candidate in Visual Studio 2015. Here are the issues and fixes I encountered along the way, relating to initial setup, libraries, unanticipated runtime/compile-time behaviour, and general comments (focusing on issues that are not obvious from TypeScript documentation).

TL;DR,

Setup TypeScript is installed by default in VS 2015. However, for non-ASP. NET projects configuration is NOT automated and various elements need to be added to the .csproj file for correct build and debugging behaviour. Integration with ASP. NET projects is easy, and project configuration takes place automatically following the inclusion of the first .ts file.

Using Library Code Declaration files (typings) are required to consume libraries such as JQuery from TypeScript code (acting like C header files), and these are available for popular libraries via NuGet (DefinitelyTyped is a popular repository). Dependent on configuration, declaration files in a project will be located automatically by IntelliSense.

Unanticipated Runtime/Compile-time behaviour Despite the appearance of a statically, strongly typed language like C#, there are still limitations to the compile time warnings that TypeScript provides. The initial appearance of more safety than you are actually getting can result in unanticipated runtime behaviour.

General Comments

Generally, in my brief experience, the type safety and IntelliSense features that TypeScript provides out-of-the-box makes coding in TypeScript faster and easier, once you get passed the initial settling in. However, re-writing existing JavaScript code in TypeScript (to make it appear less like traditional, pre-ES6 JavaScript by making use of organisational features like classes) can result in quite a bit of refactoring, and may actually result in slightly more code than the original JavaScript file. For existing, pre-ES6 JavaScript code, it may make more sense (quicker and easier) to minimally refactor the code to the point of passing the compiler type checks.

Note: Despite claims that TypeScript is a superset of JavaScript, arguably that isn’t really the case, so long as TypeScript compile-time errors are regarded as just that (errors) and not just suggestions. In this case, all JavaScript code is NOT valid typescript code.

Initial Setup

Steven Fenton’s blog post provides useful information for setting up TypeScript support in a VS project.

If you are using Visual Studio 2013 and you have the TypeScript Visual Studio Extension installed, you’ll get the following message as soon as you add a TypeScript file to your project… ‘Your project has been configured to support TypeScript…’

However, this only seems to be the case in ASP. NET projects. For non-ASP. NET projects, at least in VS 2013+, the following import clause should be included in the .csproj file manually, as the last child of the Project element.

1
<Import Project="$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets" Condition="Exists('$(MSBuildExtensionsPath32)\Microsoft\VisualStudio\v$(VisualStudioVersion)\TypeScript\Microsoft.TypeScript.targets')"/>

This will make a TypeScript Build section accessible through the project properties pane in VS

Important: If a .tsconfig file (recommend in the TypeScript documentation for configuring the transcompiler) is detected somewhere in the project then the TypeScript Build pane becomes disabled. Using a .tsconfig file seems to be buggy in Visual Studio (changes to the .tsconfig do not always seem to be applied and library scripts are not automatically detected - these issues could have been caused by conflicts with the .csproj file, or incorrect .tsconfig formatting), so it seems best to only use the TypeScript Build pane provided by VS.

Following this manual addition to the .csproj file, navigate to the TypeScript Build pane in the project properties, and perform the following steps.

  1. Ensure the Configuration dropdown list is set to Debug
  2. In the Output section tick ‘Redirect JavaScript output to directory’ and specify the appropriate debug output directory in the provided field “…bin\Debug\Scripts”
  3. Now set the Configuration dropdown list to Release
  4. In the Output section tick ‘Redirect JavaScript output to directory’ and specify the appropriate release output directory in the provided field “…bin\Release\Scripts”

Now, after making updates to TypeScript files, the TypeScript transcompiler should place the transpiled JavaScript files in the specified locations automatically (this mode dependent behaviour does not seem to be configurable through the use of a .tsconfig file - another reason to use the TypeScript Build pane).

Note: In the TypeScript Build pane, “allow implicit ‘any’ types” is ticked by default. It seems to be generally more useful to disallow implicit ‘any’ types to take full advantage of compile time type checking.

Debug Setup

To enable TypeScript debugging from the browser the TypeScript Build option ‘Generate source maps’ must be ticked. For ASP. NET projects this seems to be all that is required.

If not using ASP. NET, the .ts files should also be explicitly included in the output directory (copy if newer). Also, as the build action for .ts files is automatically and necessarily set to ‘TypeScriptCompile’ (as opposed to ‘Content’) an MSBuild task must be added to csproj to copy the .ts files to the output directory (see below).

1
2
3
4
5
  <Target Name="BeforeBuild">
    <ItemGroup>
      <Content Include="@(TypeScriptCompile)" Condition="'$(TypeScriptSourceMap)' == 'true'" />
    </ItemGroup>
  </Target>

@(TypeScriptCompile) refers to a collection of .ts files that will be automatically defined elsewhere in the .csconfig file. $(TypeScriptSourceMap) is a variable that should be auto generated after making changes in the TypeScript Build project property section, which specifies if source maps are generated (if not TypeScript debugging can’t take place anyway, hence the condition in the above MSBuild task).

Edge appears to not work very well with .ts debugging (it sometimes points to an entirely different location in the TypeScript code), but Chrome seems to do a great job, with far more informative error messages.

Using Library Code (pretty simple)

Declaration files (typings) are required to consume libraries such as JQuery from TypeScript code, acting as an interface for the transcompiled JavaScript (implementation details are stripped away in TypeScript declaration files).

DefinitelyTyped is a popular declaration file repository, providing JQuery and other popular library typings. These can be installed via NuGet.

So long as a .tsconfig file is not being used, IntelliSense seems to locate TypeScript declaration files automatically without needing to specify a folder.

Unanticipated Runtime/Compile-time Behaviour

TypeScript Class Definitions are Not Hoisted In TypeScript it seems valid at compile time to declare/define a class at the bottom of a file, and instantiate and use this object somewhere else in that file (as is the case in C#, Java, etc.).

However, in the transcompiled JavaScript the ‘class’ definition is not hoisted to the top of the file, or above where it is used (this is a problem as the transpiled JavaScript equivalent of declaring a TypeScript class is to run an immediately invoked function that returns a constructor). Despite the hoisting of variable declarations taking place when JavaScript is interpreted anyway, the actual initialisation of variables are not. This results in a runtime error “Object doesn’t support this action” (in Edge) or “… is not a constructor” (in Chrome).

Moving class declarations above where the classes are instantiated in TypeScript solves this problem (TypeScript does not warn or prompt about this).

Also be wary of introducing a race condition, through use of the $(document).ready shorthand, as below.

1
2
3
4
5
$(() => {
    let myObject = new MyClass();
});

// MyClass definition goes here

The above code will almost always work, unless the JQuery document.ready callback runs before the MyClass definition. It therefore seems best practice to place TypeScript entry points at the bottom of the file, or at least below all type declarations/definitions.

‘this’ Keyword Confusion Non-static class members must always be dereferenced with an instance qualifier (instance name, or ‘this’, i.e this.property), however care must be taken when using callback or local functions inside TypeScript classes. See the behaviour commented in the code below.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
class MyClass {
    myProperty: number = 1;

    private helperFunction() {
        console.log("myProperty", this.myProperty);
    }

    myMethod() {
        this.helperFunction(); // logs "myProperty 1"
        localHelperFunction(); // logs "myProperty undefined"

        function localHelperFunction() {
            console.log("myProperty", this.myProperty);
        }
    }
}
new MyClass().myMethod();

Essentially, ‘this’ does NOT necessarily refer to the class instance when used inside a class, and instead behaves as in JavaScript, referring to the caller of the function (such as the enclosing method).

TypeScript does not explicitly warn that the ‘this.myProperty’ inside the localHelperFunction is not defined (or ‘this.blahblah’ for that matter, probably due to the fact that properties can be added to JavaScript objects at runtime). Nevertheless, this can be detected by clicking on the myProperty declaration and noticing which ‘myProperty’ references are highlighted by IntelliSense.

Instance Dereferencing Inconsistencies There are at least three different kinds of syntax for defining the methods of a class, see methods print1, 2, and 3 in the class definition below.

1
2
3
4
5
6
7
8
9
10
11
12
13
class MyClass {
    property: string = "Hello";

    print1 = () => console.log("print1, this.property = ", this.property);

    print2 = function() {
        console.log("print2, this.property = ", this.property);
    }

    print3() {
        console.log("print3, this.property = ", this.property);
    }
}

The syntax used with print3 is used by Microsoft in the official TypeScript documentation (in the Classes section), but the other ways appear to be just as valid (no compile time issues). Instantiating MyClass and calling the print methods directly results in no unexpected behaviour.

1
2
3
4
5
6
let myClass = new MyClass();

console.log("calling print methods directly...")
myClass.print1();
myClass.print2();
myClass.print3();

Output

print1, this.property = Hello print2, this.property = Hello print3, this.property = Hello

However, I encountered an issue when wanting to pass a class' method as an argument to another function. As a simplified example see the code and output below:

1
2
3
4
5
6
7
8
9
10
11
12
13
let myClass = new MyClass();
let lambda: () => void;

console.log("calling print methods via a variable (in case you want to pass it around)...")

lambda = myClass.print1;
lambda();

lambda = myClass.print2;
lambda();

lambda = myClass.print3;
lambda();

Output

print1, this.property = Hello print2, this.property = undefined print3, this.property = undefined

While print1 (the method that was defined in MyClass as an ‘expression bodied member’) works as expected, the other two syntaxes result in what appears to be buggy behaviour.

I looked into why this was the case. See below for the transcompiled JavaScript for defining MyClass.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
var MyClass = (function () {
    function MyClass() {
        var _this = this;
        this.property = "Hello";
        this.print1 = function () {
            return console.log("print1, this.property = ", _this.property);
        };
        this.print2 = function () {
            console.log("print2, this.property = ", this.property);
        };
    }
    MyClass.prototype.print3 = function () {
        console.log("print3, this.property = ", this.property);
    };
    return MyClass;
}());

The problem is again related to use of the keyword ‘this’, which obviously varies based on the calling context.

You can see that the TypeScript developers have got around the issue by storing a ‘_this’ variable to capture the MyClass context. However ‘_this’ is only dereferenced by print1, while print2 and print3 utilize the ‘this’ keyword directly.

When called normally ‘this’ refers to MyClass. However, when assigned to and invoked via a variable, ‘this’ ends up referring to the global Window object. This was the context at the time of lambda initialisation. Nevertheless, this inconsistency is actually recognised as proper TypeScript behaviour. The ‘fat arrow’ syntax/semantics is newer, and is recognised as the means of correctly keeping a class’ ‘this’ context (see this TypeScript wiki article).

TSLint

I looked into the use of TSLint, hoping that it would provide compile time warnings for some of these issues that are not picked up by the TypeScript compiler. Despite available NuGet packages it appears to be difficult to setup in VS 2015 (at least for non-ASP. NET projects). The easiest way seems to be to install Mads Kristensen’s Web Analyzer extension.

TSLint picked up on a missing colon in my code and a call to parseInt() without using a radix argument. Other than that, by default TSLint is very strict about always using type definitions, even when the TypeScript compiler and IntelliSense can easily infer the type or when the type is blatantly obvious to developers, i.e.

let i = 0;

needs to become

let i : number = 0;

Despite this, unfortunately TSLint doesn’t provide warnings for any of the runtime issues described in this post.

Overall

As a final thought, the usefulness of compilers such as those for .NET, JAVA, C++ etc. are obviously that they compile down to MSIL or bytecode that isn’t readable and can’t be coded in by hand, or to different assembly languages based on the target operating system. JavaScript is already a readable language and it is already standardised.

TypeScript is nice to use, once you get passed the initial settling in, but it is difficult to say whether or not it is worth it in terms of learning the syntax and quirks of constructs such as TypeScript classes. When it comes to refactoring traditional JavaScript (pre-ES6), it is probably best to view TypeScript as more of a JavaScript code analysis tool with a few preprocessor directives thrown in for specifying types when they aren’t initialised inline.

On the other hand, it is worth noting that there is significant overlap between TypeScript syntax and that of ECMAScript 6 (the latest JavaScript specification), and maybe even future ECMAScript standards (speculatively), making the effort likely to be worthwhile in the long run.

Comments