Compilers are very smart, very fast, and very exact; Humans however vary wildly in all these areas. Humans make mistakes, I make mistakes, I make mistakes everyday. In a team environment my mistakes impact others, cause delays, and generally make a larger mess. When we started work at Harland Clarke we wanted to minimize our mistakes and be able to quickly correct before the team was impacted. We chose TypeScript to minimize human mistakes but over time we saw lots of additional value.
1) Compile-time errors > Runtime errors.
TypeScript will rather quickly show you complex errors at compile-time. This prevented non-valid builds from entering version control and resulted in a higher quality codebase. Better, these errors are very useful in both refactoring and seeing deeper into a codebase during development. With TypeScript we compile & edit more and test only when the build is valid.
2) You must be this tall to ride.
The compiler forced our team to a quality level before code moved into the team environment. Regardless of how senior or junior, everyone was accountable to the compiler. If your code did not validate, you learned why and fixed it. I know several developers improved the quality of code they wrote over time as a result. Some also found ways around the compiler but as we typed more of the codebase, more errors would surface for us to remove.
The type system in TypeScript is 100% optional. This is a great feature as you can “type-down” important areas of the codebase and all dependent code must conform yet leave other areas alone. We did this within our model classes and within our graphics api use ( CreateJS & Canvas ) and in these areas we were much stricter than others in terms of extension or use. Some of our more recent work was building a text/font engine extending CreateJS/EaselJS in TypeScript and we integrated this as an external library complete with its own type definition file, *.d.ts.
3) Validate all code at compile-time.
TSC takes all the code you feed it and validates it at compile-time. It makes sure that access or use of other developers code is valid. Even down to the type you pass in arguments to the type of data/object returned, to member variable. Over time this added lots of value in that every compilation is a full validation of the codebase. We know the app is valid across the whole codebase before we test.
4) Refactoring bliss
Refactoring in TypeScript is one of my favorite things. I tend to make all my changes at once and then run the compiler to see how the changes impact the codebase. This turns the compiler error output into a handy todo list with line/row numbers. Often times you will discover dependencies you never knew you had. Change a method name, compile, see everywhere it is used by line number. Change the data type passed, see all the incompatible calls. Delete a variable and see the impact. Upgrade the TypeScript compiler from 0.83 to 0.97 and see all the errors TS8.3-9.7p1 TS8.3-9.7p2 .
5) External Libraries
External JS libraries can be integrated seamlessly into a TypeScript project even though they are written in JS. The key is support for definition files. A TypeScript definition files describes the interface of an external JS library and adds type support to restrict your use of the api via the compiler. Rather than only be limited to TS libs, you can add definitions to structurally type your use of external libs like JQuery, VueJS, EaselJS and many many others. If you need a definition file, head on over to DefinitelyTyped for the latest from over 2,216 external libraries. If you cannot find one that fits, writing a definition file is fairly simple and easy to learn.
Is TypeScript appropriate for my project?
There are 100 different ways to structure a TypeScript project but after doing this 100 times, my ways are rather set in stone. I prefer a single build.ts file in the root of a project with internal references (/// <reference path=’../tools/requirejs/require.d.ts’/>) to the other project files in dependency order. By compiling one file, all files are processed and output like so:
// compile the project
//or with AMD module support
tsc -m 'amd' build.ts
I find this structure is easy to walk into and get working. It is simple and adding additional workflow above this is easy with grunt or gulp. Within our latest project we used gulp for compilation, example.
Having used anything for 2 years you run into a few things you want to see changes. Here is my list.
- [NOTE: ADDED as Union Types ] Type cascade – Rather than only providing one type, I would love to hand the compiler a set of appropriate types like so:
var foo:number|string = 123;
var foo:Solid|Liquid = new Liquid();
- TypeScript Configuration Metadata – It is hard to denote how you want referenced files handled exactly within a single compiler pass. While there is a notation for referencing files (adding external file into compiler within a .ts file ), there is no output notation other than use of import/export module syntax. Ideally one could add output values to the references to control compiler output. I prefer to have a single built.ts file in the root of the project and compile through it, thus the need for options for packaging, module syntax, concatenation, consolidating the __extends calls (subclass wiring), and optional file output at all (build.ts always results in an empty .js file). [NOTE: Turns out any file with a *.d.ts will not be output by the compiler but any references within will. This makes for an ideal entry point into the compiler with a single build.d.ts file. ]
- Better ES6 support
- Destructing assignments
- Macros – I have found that you can extend the TypeScript language with Sweet.js. Being able to define new language level extensions to the compiler would be a great hard feature.