Constant-named properties
TypeScript 2.7 adds support for declaring const-named properties on types including ECMAScript symbols.
Example
// Lib
export const SERIALIZE = Symbol("serialize-method-key");
export interface Serializable {
[SERIALIZE](obj: {}): string;
}
// consumer
import { SERIALIZE, Serializable } from "lib";
class JSONSerializableItem implements Serializable {
[SERIALIZE](obj: {}) {
return JSON.stringify(obj);
}
}
This also applies to numeric and string literals.
Example
const Foo = "Foo";
const Bar = "Bar";
let x = {
[Foo]: 100,
[Bar]: "hello",
};
let a = x[Foo]; // has type 'number'
let b = x[Bar]; // has type 'string'
unique symbol
To enable treating symbols as unique literals a new type unique symbol
is available.
unique symbol
is a subtype of symbol
, and are produced only from calling Symbol()
or Symbol.for()
, or from explicit type annotations.
The new type is only allowed on const
declarations and readonly static
properties, and in order to reference a specific unique symbol, you’ll have to use the typeof
operator.
Each reference to a unique symbol
implies a completely unique identity that’s tied to a given declaration.
Example
// Works
declare const Foo: unique symbol;
// Error! 'Bar' isn't a constant.
let Bar: unique symbol = Symbol();
// Works - refers to a unique symbol, but its identity is tied to 'Foo'.
let Baz: typeof Foo = Foo;
// Also works.
class C {
static readonly StaticSymbol: unique symbol = Symbol();
}
Because each unique symbol
has a completely separate identity, no two unique symbol
types are assignable or comparable to each other.
Example
const Foo = Symbol();
const Bar = Symbol();
// Error: can't compare two unique symbols.
if (Foo === Bar) {
// ...
}
Strict Class Initialization
TypeScript 2.7 introduces a new flag called --strictPropertyInitialization
.
This flag performs checks to ensure that each instance property of a class gets initialized in the constructor body, or by a property initializer.
For example
class C {
foo: number;
bar = "hello";
baz: boolean;
// ~~~
// Error! Property 'baz' has no initializer and is not definitely assigned in the
// constructor.
constructor() {
this.foo = 42;
}
}
In the above, if we truly meant for baz
to potentially be undefined
, we should have declared it with the type boolean | undefined
.
There are certain scenarios where properties can be initialized indirectly (perhaps by a helper method or dependency injection library), in which case you can use the new definite assignment assertion modifiers for your properties (discussed below).
class C {
foo!: number;
// ^
// Notice this '!' modifier.
// This is the "definite assignment assertion"
constructor() {
this.initialize();
}
initialize() {
this.foo = 0;
}
}
Keep in mind that --strictPropertyInitialization
will be turned on along with other --strict
mode flags, which can impact your project.
You can set the strictPropertyInitialization
setting to false
in your tsconfig.json
’s compilerOptions
, or --strictPropertyInitialization false
on the command line to turn off this checking.
Definite Assignment Assertions
The definite assignment assertion is a feature that allows a !
to be placed after instance property and variable declarations to relay to TypeScript that a variable is indeed assigned for all intents and purposes, even if TypeScript’s analyses cannot detect so.
Example
let x: number;
initialize();
console.log(x + x);
// ~ ~
// Error! Variable 'x' is used before being assigned.
function initialize() {
x = 10;
}
With definite assignment assertions, we can assert that x
is really assigned by appending an !
to its declaration:
// Notice the '!'
let x!: number;
initialize();
// No error!
console.log(x + x);
function initialize() {
x = 10;
}
In a sense, the definite assignment assertion operator is the dual of the non-null assertion operator (in which expressions are post-fixed with a !
), which we could also have used in the example.
let x: number;
initialize();
// No error!
console.log(x! + x!);
function initialize() {
x = 10;
In our example, we knew that all uses of x
would be initialized so it makes more sense to use definite assignment assertions than non-null assertions.
Fixed Length Tuples
In TypeScript 2.6 and earlier, [number, string, string]
was considered a subtype of [number, string]
.
This was motivated by TypeScript’s structural nature; the first and second elements of a [number, string, string]
are respectively subtypes of the first and second elements of [number, string]
.
However, after examining real world usage of tuples, we noticed that most situations in which this was permitted was typically undesirable.
In TypeScript 2.7, tuples of different arities are no longer assignable to each other.
Thanks to a pull request from Tycho Grouwstra, tuple types now encode their arity into the type of their respective length
property.
This is accomplished by leveraging numeric literal types, which now allow tuples to be distinct from tuples of different arities.
Conceptually, you might consider the type [number, string]
to be equivalent to the following declaration of NumStrTuple
:
interface NumStrTuple extends Array<number | string> {
0: number;
1: string;
length: 2; // using the numeric literal type '2'
}
Note that this is a breaking change for some code.
If you need to resort to the original behavior in which tuples only enforce a minimum length, you can use a similar declaration that does not explicitly define a length
property, falling back to number
.
interface MinimumNumStrTuple extends Array<number | string> {
0: number;
1: string;
}
Note that this does not imply tuples represent immutable arrays, but it is an implied convention.
Improved type inference for object literals
TypeScript 2.7 improves type inference for multiple object literals occurring in the same context. When multiple object literal types contribute to a union type, we now normalize the object literal types such that all properties are present in each constituent of the union type.
Consider:
const obj = test ? { text: "hello" } : {}; // { text: string } | { text?: undefined }
const s = obj.text; // string | undefined
Previously type {}
was inferred for obj
and the second line subsequently caused an error because obj
would appear to have no properties.
That obviously wasn’t ideal.
Example
// let obj: { a: number, b: number } |
// { a: string, b?: undefined } |
// { a?: undefined, b?: undefined }
let obj = [{ a: 1, b: 2 }, { a: "abc" }, {}][0];
obj.a; // string | number | undefined
obj.b; // number | undefined
Multiple object literal type inferences for the same type parameter are similarly collapsed into a single normalized union type:
declare function f<T>(...items: T[]): T;
// let obj: { a: number, b: number } |
// { a: string, b?: undefined } |
// { a?: undefined, b?: undefined }
let obj = f({ a: 1, b: 2 }, { a: "abc" }, {});
obj.a; // string | number | undefined
obj.b; // number | undefined
Improved handling of structurally identical classes and instanceof
expressions
TypeScript 2.7 improves the handling of structurally identical classes in union types and instanceof
expressions:
- Structurally identical, but distinct, class types are now preserved in union types (instead of eliminating all but one).
- Union type subtype reduction only removes a class type if it is a subclass of and derives from another class type in the union.
- Type checking of the
instanceof
operator is now based on whether the type of the left operand derives from the type indicated by the right operand (as opposed to a structural subtype check).
This means that union types and instanceof
properly distinguish between structurally identical classes.
Example
class A {}
class B extends A {}
class C extends A {}
class D extends A { c: string }
class E extends D {}
let x1 = !true ? new A() : new B(); // A
let x2 = !true ? new B() : new C(); // B | C (previously B)
let x3 = !true ? new C() : new D(); // C | D (previously C)
let a1 = [new A(), new B(), new C(), new D(), new E()]; // A[]
let a2 = [new B(), new C(), new D(), new E()]; // (B | C | D)[] (previously B[])
function f1(x: B | C | D) {
if (x instanceof B) {
x; // B (previously B | D)
}
else if (x instanceof C) {
x; // C
}
else {
x; // D (previously never)
}
}
Type guards inferred from in
operator
The in
operator now acts as a narrowing expression for types.
For a n in x
expression, where n
is a string literal or string literal type and x
is a union type, the “true” branch narrows to types which have an optional or required property n
, and the “false” branch narrows to types which have an optional or missing property n
.
Example
interface A { a: number };
interface B { b: string };
function foo(x: A | B) {
if ("a" in x) {
return x.a;
}
return x.b;
}
Support for import d from "cjs"
from CommonJS modules with --esModuleInterop
TypeScript 2.7 updates CommonJS/AMD/UMD module emit to synthesize namespace records based on the presence of an __esModule
indicator under --esModuleInterop
.
The change brings the generated output from TypeScript closer to that generated by Babel.
Previously CommonJS/AMD/UMD modules were treated in the same way as ES6 modules, resulting in a couple of problems. Namely:
- TypeScript treats a namespace import (i.e.
import * as foo from "foo"
) for a CommonJS/AMD/UMD module as equivalent toconst foo = require("foo")
.Things are simple here, but they don’t work out if the primary object being imported is a primitive or a class or a function. ECMAScript spec stipulates that a namespace record is a plain object, and that a namespace import (foo
in the example above) is not callable, though allowed by TypeScript - Similarly a default import (i.e.
import d from "foo"
) for a CommonJS/AMD/UMD module as equivalent toconst d = require("foo").default
.Most of the CommonJS/AMD/UMD modules available today do not have adefault
export, making this import pattern practically unusable to import non-ES modules (i.e. CommonJS/AMD/UMD). For instanceimport fs from "fs"
orimport express from "express"
are not allowed.
Under the new --esModuleInterop
these two issues should be addressed:
- A namespace import (i.e.
import * as foo from "foo"
) is now correctly flagged as uncallable. Calling it will result in an error. - Default imports to CommonJS/AMD/UMD are now allowed (e.g.
import fs from "fs"
), and should work as expected.
Note: The new behavior is added under a flag to avoid unwarranted breaks to existing code bases. We highly recommend applying it both to new and existing projects. For existing projects, namespace imports (
import * as express from "express"; express();
) will need to be converted to default imports (import express from "express"; express();
).
Example
With --esModuleInterop
two new helpers are generated __importStar
and __importDefault
for import *
and import default
respectively.
For instance input like:
import * as foo from "foo";
import b from "bar";
Will generate:
"use strict";
var __importStar = (this && this.__importStar) || function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k in mod) if (Object.hasOwnProperty.call(mod, k)) result[k] = mod[k];
result["default"] = mod;
return result;
}
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
}
exports.__esModule = true;
var foo = __importStar(require("foo"));
var bar_1 = __importDefault(require("bar"));
Numeric separators
TypeScript 2.7 brings support for ES Numeric Separators.
Numeric literals can now be separated into segments using _
.
Example
const million = 1_000_000;
const phone = 555_734_2231;
const bytes = 0xFF_0C_00_FF;
const word = 0b1100_0011_1101_0001;
Cleaner output in --watch
mode
TypeScript’s --watch
mode now clears the screen after a re-compilation is requested.
Prettier --pretty
output
TypeScript’s --pretty
flag can make error messages easier to read and manage.
--pretty
now uses colors for file names, diagnostic codes, and line numbers.
File names and positions are now also formatted to allow navigation in common terminals (e.g. Visual Studio Code terminal).