TypeScript is not a new project. The work on it started around 2012, but only in 2014 its 1.0 version was released. It may seem like novelty, because it did not gain popularity until recently, when Angular team switched from Dart to TypeScript for second version of Angular.
When it comes to everything what Microsoft produces, devs are still very cautious, although it really seems that they understand past mistakes and work hard on changing their company and the company image. To name few, there is Edge that replaced IE, Microsoft supports Mono project, “reverse Wine” called “Bash on Ubuntu on Windows” and, of course, TypeScript – Microsoft’s investment in web technologies, which, what is strongly understandable, taking into consideration another one – ASP.NET.
Using TypeScript in that early (1.x) version felt like it was going against what you could do with JavaScript, instead of helping. Things used to be made in a way that resulted in such experience, but core TypeScript team and contributors took their time and used it well, so since 2.1 update majority of these issues are gone and you should reconsider using TypeScript for your next project.
So what has changed, what might you have missed and what makes it suitable for safe usage?
Type inferring
This is not really a new feature, but the feature that some might not be aware of.
Default behaviour of TypeScript compiler is to try to figure out the type when it is not given implicitly. It doesn’t have to be given implicitly.
1 2 |
const n = 3 parseInt(n, 10) |
…will trigger a warning as it doesn’t make sense, it is not compliant to parseInt function signature that resides inside TypeScript library…
1 |
declare function parseInt(s: string, radix?: number): number; |
It didn’t require us to specify n as in:
1 |
const n: number = 3 |
…and yet TypeScript understands that this is a number due to default type inferring.
Union types
Let’s take following function. It’s pretty clear that it will work for almost all reasonable input.
1 2 3 |
function appendUnit(v, unit = 'px') { return v + unit } |
But if I would like to make this type annotated, I’d prefer not to be forced to produce two functions or complex definitions to indicate that this function accepts only numbers and strings. It’s simple and easy with Union types
1 2 3 |
function appendUnit(v: number | string, unit = 'px') { return v + unit } |
Intersection types
It is rather a common case to merge two objects into one. TypeScript can handle it and offers type checking for the product of such operation, even if you are not sure about object’s shape.
Let’s assign properties amount of object in that object with a following function:
1 2 3 4 5 6 7 8 9 10 11 12 |
function dictionaryOf(source: T): T & { length: number } { return Object.assign( { length: Object .keys(source) .filter(k => k !== 'length') .length }, source ) }} |
In this particular example specifying result type T & { length: number } is unnecessary. TypeScript would be able to figure it out on it’s own due to Object.assign definition.
Similarly, but not exactly the same object spread is handled:
1 2 3 4 |
const a = {a: 1} // is of type {a: number} const b = {...a, b: 2} // is of type {a: number, b: number} b.a // 1 b.c // wrong! |
TypeScript: Mapped types
As JavaScript is very flexible and Object may be a class instance, a unique value (mostly pre-Symbol solution) or a structure counterpart, it is expected of TypeScript to keep up with all the twists and tricks that JavaScript is capable of.
Consider this scenario where TypeScript has recently improved is partial application.
1 2 3 4 5 6 7 8 9 |
function defaults(defaults, partial) { return { ...defaults, ...partial } } function vector2d(partialVector) { return defaults({x: 0, y: 0}, partialVector) } const result = vector2d({x: 2})! |
Mentioned above code sample is fine in TypeScript, but to provide better developer experience, to have more strict type checking, to better express intentions and eliminate confusion it would be best if partial had only properties that were present on defaults too. Fortunately, we can do that!
Let’s read this generic function:
1 2 3 4 5 6 7 8 9 10 |
function defaults /* generic argument T */ ( /* default should be exactly this type */ defaults: T, /* partial type should be nothing more than this type */ /* This is mapped type */ partial: {[id in keyof T]?: T[id]} /* result will be of this type */ ): T { return Object.assign({}, defaults, partial) } |
For common cases there are few helpers that allow to express intention more swiftly. In this case, you can use Partial, which expresses same mapped signature as above.
1 2 3 |
function defaults(defaults: T, partial: Partial): T { return Object.assign({}, defaults, partial) } |
With mapped types you can ensure not only that two object have the same keys, but that same properties in both objects have the same type. This is T[id] part of {[id in keyof T]?: T[id]}.
To broaden the understanding of what it might be used to, let’s create a function that resolves all promises provided within an object and returns Promise of that object instead. It’s a reversed example of previous mapped types usage where you start with a mapped type, but for TypeScript it is all the same.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
/* as generic argument T will be used to create others and not directly let's call it archetype */ function resolveAll ( /* an object with same keys as T, but every property value will not be the same type, but rather that "expected" type, inside of a Promise */ promises: { [id in keyof T]: Promise<T[id]> } /* we'll return not that type, but that ach ): Promise { // (implementation) } |
JSX – cooperation between TypeScript and Angular
Despite close cooperation between TypeScript and Angular teams, there is no obstacle, but actually a lot of help when it comes to writing applications with React (or React compatible) libraries.
Consider these examples:
1 2 3 4 5 6 7 8 9 10 11 12 |
// ES5 + CommonJS - factory function module.exports = React.createClass({ propTypes: { foo: React.PropTypes.number.isRequired, bar: React.PropTypes.number }, render() { return <div>{this.props.foo * (this.props.bar | 0)}</div> } }) /// ES6 class export default class Whatever extends React.Component { render() { return <div>{this.props.foo * (this.props.bar | 0)}</div> } } Whatever.propTypes = { foo: React.PropTypes.number.isRequired, bar: React.PropTypes.number } |
In development mode, React will check whether these definitions match actual input, a component receives. This however requires code to be actually run. With TypeScript you will get errors before that.
1 2 3 4 5 6 |
// TypeScript export default class Whatever extends React.Component<{foo: number, bar?: number}> { render() { return <div>{this.props.foo * (this.props.bar | 0)}</div> } } |
TypeScript compiler will notify you if you try to use Whatever component without specifying foo property. If your editor is smart about TypeScript you’ll see an indication ever earlier
Async/await
Probably most notable feature of (still) new JavaScript (in most cases already supported) works fine with TypeScript too. Basically, TypeScript compiler does what you would expect from Babel.
1 2 3 4 5 6 7 8 9 10 |
function whatever () { Promise.resolve(5) .then(value => { /* `value` is a number */ }) } // ...but also async function whatever () { const value = await new Promise.resolve(5) /* `value` is a number */ } |
Although I assume it to be a feature that will not be widely used, it is still worth mentioning that TypeScript already have some support for asynchronous iterators as well and as time goes by it will be better.
Modules
Initially TypeScript had its own module system, which made it hard to use with other tools. If you had seen TypeScript file a while ago, you might have seen code like this:
1 2 3 4 |
module Foo { export function bar() { return 'ok' } } Foo.bar() // ok |
Modules now are namespaces, to avoid confusion as it used to be and is pretty clear that modules are going to be supported with future versions of JS.
1 2 3 4 |
namespace Foo { export function bar() { return 'ok' } } Foo.bar() // ok |
It seems namespaces are only to provide compatibility with existing code utilizing modules. As TypeScript supports ES6/CommonJS module syntax, consider modules/namespaces deprecated and TypeScript in line with JavaScript.
Dynamic import
If we are in a topic of module system, I have to mention dynamic imports as well. In 2.6 version, transpiled, if not in esnext target, dynamic imports allow using import keyword to load a module asynchronously.
The result of that call is typed just like static import, so this is fully functional part of the language.
Mind that you still need browser/bundler/runtime support for that feature, transpiled or not.
Third party libraries support
This was probably most cumbersome question of all. Without proper cooperation with legacy code and regular JavaScript libraries it was just a curiosity. How well it integrates with 3rd party libraries then? For a long time answer was not satisfying – you have to provide definitions. There was, of course, DefinitelyTyped project, but it had some issues with version compatibility. Since 2.0 version of TypeScript, you can use @types/* (notice namespace) packages from npm. It handled different 3rd party libraries versions problem way better, but you still had to do it, which was troublesome, if you use not popular library that did not have definitions up to date or at all.
This changed with 2.1 and “you can import a JavaScript module without needing a type declaration”. Popular libraries are still a reason to use @types/*, but for cases without type definitions – you are still good to go!
…but wait, that’s not all!
TypeScript handles well other features that do not require explanation, but merely a notice:
- template strings
- arrow functions
- decorators (behind –experimentalDecorators flag)
- ** operator
It does the job
Me and my team of 4 front-end developers are using that across 3 distinctive targets for over a year:
- browser
- browser + SSR with node (next.js)
- react-native
We also maintain a portion of code that we share across our projects. We use TypeScript with React and some few more libraries.
My favourite is a class – connector for our back end, we do not write by hand, but we generate using provided by that backend definitions of responses of every endpoint. This helps with development a lot, as very little to annotate by hand remains in general!
We are very satisfied with TypeScript and I can tell that for sure I’m not coming back to just JS in any version.