TypeScript 2.0 Awesomeness

By on September 1, 2016 6:22 am

typescript-2.0-awesomeness-image

Over the past few years, TypeScript has iterated and greatly improved developer ergonomics. With our efforts on Dojo 2, we’ve been very excited about many of the features and improvements made, including several key improvements that have landed for TypeScript 2, which is currently in beta release!

Control flow type analysis

TypeScript 2 adds a major improvement in the type analysis of code. Now types are narrowed and widened recognizing the flow of the code. Previously, there were limits where you could allow narrowing of a type within a closure before, but when outside of the block, it would reset the type. You always had to write your code within if...else blocks to take advantage of type narrowing, or use unsafe casts:

function foo(x: string | number | boolean) {
	if (typeof x === 'string') {
		x; // type of `string`
	}
	else {
		x; // type of `number | boolean`
	}
	x; // type of `string | number | boolean`
}

function bar(x: string | number) {
	if (typeof x === 'number') {
		return;
	}
	x; // type of `string | number`
}

With TypeScript 2.0, the code is statically analyzed and types are narrowed, widened or changed in line with the logic of the code. Meaning that you can code in patterns allowed by JavaScript and the type system will keep up. This can help guard you against many logic errors in your code, where you might have had to unsafely cast before, when in fact the logic wouldn’t have been safe to assume that the type was properly narrowed.

function foo(x: string | number | boolean) {
	if (typeof x === 'string') {
		x; // type of `string`
		x = 1;
		x; // type of `number`
	}
	x; // type of `number | boolean`
}

function bar(x: string | number) {
	if (typeof x === 'number') {
		return;
	}
	x; // type of `string`
}

this typing for Functions

As one of the most requested features for TypeScript for quite a while, many TypeScript users are happy to see this feature land. Prior to TypeScript 2.0, you had to choose to unsafely access this or use some sort of boilerplate which would add to your emit for no good value.:

interface Foo {
	foo: string;
	bar: string;
}

function foo() {
	this.foo = 3; // No Error, because `this` is `any`
}

function bar() {
	const self: Foo = this; // Annoying "boilerplate" that gets emitted
	self.foo = 3; // Error
}

There was an attempt to contextually infer this in object literals, but it caused too much of a challenge, because not all object literal methods will be invoked with the this of the enclosing object, so the change was rolled back. This means this in object literal methods is not contextually inferred and you need to be explicit.

interface Foo {
	foo: string;
	bar: string;
}

function foo(this: Foo) {
	this.foo = 3; // Error and no emitted "boilerplate"
}

There is also a new compiler flag (noImplicitThis) which ensures that if you are using this in the function body that cannot be properly contextually inferred, that it becomes a compiler error and informs you to be explicit about the type of this in the function.

Strict null checking

Again, a source for many logic errors. Previously undefined and null types were inclusive of other types. Now they may be considered separate types that do not intersect with other types.

Because of the likelihood of significant breakage to code, this improvement was introduced under the compiler flag --strictNullChecks. Without this flag set to true, this feature is not enabled. There are many cases where logic errors could easily be made, when values could be undefined or null at run-time, but the type system would assume they were assigned. So in older versions of TypeScript, or without the flag enabled, this would happen:

function foo(x?: string) {
	x.split('.'); // Ooops! Run time error, but no build time error
}

foo();

With --strictNullChecks enabled, optional parameters are automatically inferred as | undefined even if the parameter isn’t explicit about it:

function foo(x?: string) {
	x.split('.'); // Build time error "Object is possibly 'undefined'."
}

function bar(x?: string) {
	if (x) {
		x.split('.'); // type is `string`
	}
}

This change really highlights a lot of potential logic error, but likely will require some level of revisiting code to migrate, because of the number of potentially unsafe operations that JavaScript allows.

readonly keyword

The new readonly keyword disallows reassignment and implies a non-writable property or a property with only a get accessor. It does not mean non-primitives are immutable.

interface Foo {
	readonly foo: string;
	readonly bar: { foo?: string; };
}

class Bar implements Foo {
	get foo() {
		return 'bar';
	}
	readonly bar: { foo?: string; } = {};
}

This is one of the items though that is a breaking change. If you have an interface (a .d.ts) file that uses this, older versions of TypeScript will not be able to understand it.

Type guarding on property access

Prior to TypeScript 2, you cannot narrow types on property accessors, only properties/values.

interface Foo {
	foo: string | string[];
}

function foo(x: Foo) {
	if (Array.isArray(x.foo)) {
		x.foo; // Still type of `string | string[]`
	}
}

function bar(x: Foo) {
	const foo = x.foo;
	if (Array.isArray(foo)) {
		foo; // Type of 'string[]'
	}
}

With TypeScript 2.0, you can safely narrow types through property accessors:

interface Foo {
	foo: string | string[];
}

function foo(x: Foo) {
	if (Array.isArray(x.foo)) {
		x.foo; // type is `string[]`
	}
}

Wildcard modules

To support module loader plugins within AMD or SystemJS, it’s necessary to be able to type the module, with the understanding that the name of the module is variable through the parameter that is passed to the module loader plugin. For example, this makes it possible to support the loading of HTML files, JSON resources, and other resources with more flexibility.

declare module "json!*" {
    let json: any;
    export default json;
}

import d from "json!a/b/bar.json";
// lookup:
//    json!a/b/bar.json
//    json!*

Path mapping configuration

Similar to what we’ve had for many years with AMD, TypeScript 2 now supports configuration settings to remap a path!

Prior to TypeScript 2, support existed for two ways of resolving module names: classic (a module name always resolves to a file, modules are searched using a folder walk) and node (uses rules similar to the Node.js module loader). Unfortunately neither approach solves the approach of defining modules relative to a baseUrl, which is what AMD systems such as Dojo and RequireJS, and SystemJS use.

Instead of introducing a third type of module resolution, the TypeScript team added the configuration settings to solve this within the existing systems: baseUrl, paths, and rootDirs.

paths may only be used if baseUrl is set. If at least one of these properties is defined then the TypeScript compiler will try to use it to resolve module names and if it fails, it will fallback to a default strategy.

async and await for ES5 (TypeScript 2.1)

One more feature that we’re very very excited about for TypeScript has been deferred until version 2.1. For the ES8 async and await syntax, it is currently possible to transpile to ES6 syntax, but this requires a global ES6 Promise compatible implementation. Soon it will be possible to transpile code relying on async and await to ES5.

async function foo() {
	return 'foo';
}

await foo();

A new bottom type

One of the challenges that the TypeScript team faced with the code flow analysis was that there was no bottom type, or in other words a total lack of a type, which is important in any type system. Traditionally, if a function in TypeScript was not expected to return a value, it could be typed as void but this wasn’t strictly a bottom type (since this was actually a version of undefined | null which are actually types in TypeScript).

In most cases, never is inferred in functions where the code flow analysis detects unreachable code and as a developer you don’t have to worry about it. For example, if a function only throws, it will get a never type:

function error(message: string): never {
    throw new Error(message);
}

function foo() { // inferred as a `never` return
    return error('I threw');
}

This does mean that functions that may potentially contain an unreachable return will have their return type analyzed as never, which in turn can end up being part of a definition file. So like readonly, this is a breaking change that is not backwards compatible.

TypeScript 2 and beyond

Our team have been big fans of TypeScript and have actively contributed feedback, bug reports, and pull requests. All of our current Dojo 2 work has already been updated to support TypeScript 2, and our ES6 and TypeScript workshops are also updated for TypeScript 2. We’re very excited to build Dojo 2 and applications for our customers on top of TypeScript 2!


Learning more

Support Logo

Get help from SitePen Support, our fast and efficient solutions to JavaScript development problems of any size.

Workshops Logo

SitePen workshops are a fun, hands-on way to keep up with JavaScript development and testing best practices. Register for an online workshop, today!

Let's Talk! Logo

Let’s talk about how we can help your organization improve their approach to automated testing.

Contact Us Logo

Have a question? We’re here to help! Get in touch and let’s see how we can work together.

Comments