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!