Today we’re announcing the release of TypeScript 2.9!
If you’re not familiar with TypeScript, it’s a language that adds optional static types to JavaScript. Those static types help make guarantees about your code to avoid typos and other silly errors. They can also provide nice things like code completions and easier project navigation thanks to tooling built around those types. When your code is run through the TypeScript compiler, you’re left with clean, readable, and standards-compliant JavaScript code, potentially rewritten to support much older browsers that only support ECMAScript 5 or even ECMAScript 3.
If you can’t wait any longer, you can download TypeScript via NuGet or by running
npm install -g typescript
You can also get editor support for
Other editors may have different update schedules, but should all have excellent TypeScript support soon as well.
This release brings some great editor features:
And we also have core language/compiler features:
We also have some minor breaking changes that you should keep in mind if upgrading.
But otherwise, let’s look at what new features come with TypeScript 2.9!
Editor features
Because TypeScript’s language server is built in conjunction with the rest of the compiler, TypeScript can provide consistent cross-platform tooling that can be used on any editor. While we’ll dive into language improvements in a bit, it should only take a minute to cover these features which are often the most applicable to users, and, well, fun to see in action!
Rename file and move declaration to new file
After much community demand, two extremely useful refactorings are now available! First, this release of TypeScript allows users to move declarations to their own new files. Second, TypeScript 2.9 has functionality to rename files within your project while keeping import paths up-to-date.
While not every editor has implemented these features yet, we expect they’ll be more broadly available soon.
Unused span reporting
TypeScript provices two lint-like flags: --noUnusedLocals
and --noUnusedParameters
. These options provide errors when certain declarations are found to be unused; however, while this information is generally useful, errors can be a bit much.
TypeScript 2.9 has functionality for editors to surface these as “unused” suggestion spans. Editors are free to display these as they wish. As an example, Visual Studio Code will be displaying these as grayed-out text.
Convert property to getter/setter
Thanks to community contributor Wenlu Wang, TypeScript 2.9 supports converting properties to get- and set- accessors.
import()
types
One long-running pain-point in TypeScript has been the inability to reference a type in another module, or the type of the module itself, without including an import at the top of the file.
In some cases, this is just a matter of convenience – you might not want to add an import at the top of your file just to describe a single type’s usage. For example, to reference the type of a module at an arbitrary location, here’s what you’d have to write before TypeScript 2.9:
import * as _foo from "foo";
export async function bar() {
let foo: typeof _foo = await import("foo");
}
In other cases, there are simply things that you can’t achieve today – for example, referencing a type within a module in the global scope is impossible today. This is because a file with any imports or exports is considered a module, so adding an import for a type in a global script file will automatically turn that file into a module, which drastically changes things like scoping rules and strict mode within that file.
That’s why TypeScript 2.9 is introducing the new import(...)
type syntax. Much like ECMAScript’s proposed import(...)
expressions, import types use the same syntax, and provide a convenient way to reference the type of a module, or the types which a module contains.
// foo.ts
export interface Person {
name: string;
age: number;
}
// bar.ts
export function greet(p: import("./foo").Person) {
return `
Hello, I'm ${p.name}, and I'm ${p.age} years old.
`;
}
Notice we didn’t need to add a top-level import specify the type of p
. We could also rewrite our example from above where we awkwardly needed to reference the type of a module:
export async function bar() {
let foo: typeof import("./foo") = await import("./foo");
}
Of course, in this specific example foo
could have been inferred, but this might be more useful with something like the TypeScript language server plugin API.
--pretty
by default
TypeScript’s --pretty
mode has been around for a while, and is meant to provide a friendlier console experience. Unfortunately it’s been opt-in for fear of breaking changes. However, this meant that users often never knew --pretty
existed.
To minimize breaking changes, we’ve made --pretty
the default when TypeScript can reasonably detect that it’s printing output to a terminal (or really, whatever Node considers to be a TTY device). Users who want to turn --pretty
off may do so by specifying --pretty false
on the command line. Programs that rely on TypeScript’s output should adjust the spawned process’s TTY options.
Support for well-typed JSON imports
TypeScript is now able to import JSON files as input files when using the node
strategy for moduleResolution
. This means you can use json
files as part of their project, and they’ll be well-typed!
// ./tsconfig.json
{
"compilerOptions": {
"module": "commonjs",
"resolveJsonModule": true,
"esModuleInterop": true
"outDir": "lib"
},
"include": ["src"]
}
// ./src/settings.json
{
"dry": false,
"debug": false
}
// ./src/foo.ts
import settings from "./settings.json";
settings.debug === true; // Okay
settings.dry === 2; // Error! Can't compare a `boolean` and `number`
These JSON files will also carry over to your output directory so that things “just work” at runtime.
Type arguments for tagged template strings
If you use tagged template strings, you might be interested in some of the improvements in TypeScript 2.9.
Most of the time when calling generic functions, TypeScript can infer type arguments. However, there are times where type arguments can’t be inferred. For example, one might imagine an API like the following:
export interface RenderedResult {
// ...
}
export interface TimestampedProps {
timestamp: Date;
}
export function timestamped<OtherProps>(
component: (props: TimestampedProps & OtherProps) => RenderedResult):
(props: OtherProps) => RenderedResult {
return props => {
const timestampedProps =
Object.assign({}, props, { timestamp: new Date() });
return component(timestampedProps);
}
}
Here, let’s assume a library where “components” are functions which take objects and return some rendered content. The idea is that timestamped
will take a component that may use a timestamp
property (from TimestampedProps
) and some other properties (from OtherProps
), and return a new component which only takes properties specified in OtherProps
.
Unfortunately there’s a problem with inference when using timestamped
naively:
declare function createDiv(contents: string | RenderedContent): RenderedContent;
const TimestampedMessage = timestamped(props => createDiv(`
Message opened at : ${props.timestamp}
Message contentsn${props.contents}
`));
Here, TypeScript infers the wrong type for props
when calling timestamped
because it can’t find any candidates for OtherProps
. OtherProps
gets the type {}
, and props
is then assigned the type TimestampedProps & {}
which is undesirable.
We can get around this with an explicit annotation on props
:
interface MessageProps {
contents: string;
}
// Notice this intersection type vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv
const TimestampedMessage = timestamped((props: MessageProps & TimestampedProps) => /*...*/);
But we would prefer not to write as much; the type system already knows TimestampedProps
will be part of the type; it just needs to know what OtherProps
will be, so we can specify that explicitly.
interface MessageProps {
contents: string;
}
const TimestampedMessage = timestamped<MessageProps>(props => createDiv(`
Message opened at : ${props.timestamp.toLocaleString()}
Message contentsn${props.contents}
`));
Whew! Great! But what does that have to do with tagged template strings?
Well, the point here is that we the users were able to give type arguments when the type system had a hard time figuring things out on our invocations. It’s not ideal, but it at least it was possible.
But tagged template strings are also a type of invocation. Tagged template strings actually invoke functions, but up until TypeScript 2.9, they support type arguments at all.
For tagged template strings, this can be useful for libraries that work like styled-components:
interface StyleProps {
themeName: string;
}
declare function styledInput<OtherProps>(
strs: TemplateStringsArray,
...fns: ((props: OtherProps & StyleProps) => string)[]):
React.Component<OtherProps>;
Similar to the above example, TypeScript would have no way to infer the type of OtherProps
if the functions passed to fns
were not annotated:
export interface InputFormProps {
invalidInput: string;
}
// Error! Type 'StyleProps' has no property 'invalidInput'.
export const InputForm = styledInput `
color:
${({themeName}) => themeName === 'dark' ? 'black' : 'white'};
border-color: ${({invalidInput}) => invalidInput ? 'red' : 'black'};
`;
TypeScript now 2.9 allows type arguments to be placed on tagged template strings, and makes this just as easy as a regular function call!
export interface InputFormProps {
invalidInput: string;
}
export const InputForm = styledInput<InputFormProps> `
color:
${({themeName}) => themeName === 'dark' ? 'black' : 'white'};
border-color: ${({invalidInput}) => invalidInput ? 'red' : 'black'};
`;
In the above example, themeName
and invalidInput
are both well-typed. TypeScript knows they are both string
s, and would have told us if we’d misspelled either.
Support for symbols and numeric literals in keyof
and mapped object types
TypeScript’s keyof
operator is a useful way to query the property names of an existing type.
interface Person {
name: string;
age: number;
}
// Equivalent to the type
// "name" | "age"
type PersonPropertiesNames = keyof Person;
Unfortunately, because keyof
predates TypeScript’s ability to reason about unique symbol
types, keyof
never recognized symbolic keys.
const baz = Symbol("baz");
interface Thing {
foo: string;
bar: number;
[baz]: boolean; // this is a computed property type
}
// Error in TypeScript 2.8 and earlier!
// `typeof baz` isn't assignable to `"foo" | "bar"`
let x: keyof Thing = baz;
TypeScript 2.9 changes the behavior of keyof
to factor in both unique symbols as well as number
and numeric literal types. As such, the above example now compiles as expected. keyof Thing
now boils down to the type "foo" | "bar" | typeof baz
.
With this functionality, mapped object types like Partial
, Required
, or Readonly
also recognize symbolic and numeric property keys, and no longer drop properties named by symbols:
type Partial<T> = {
[K in keyof T]: T[K]
}
interface Thing {
foo: string;
bar: number;
[baz]: boolean;
}
type PartialThing = Partial<Thing>;
// This now works correctly and is equivalent to
//
// interface PartialThing {
// foo?: string;
// bar?: number;
// [baz]?: boolean;
// }
Unfortunately this is a breaking change for any usage where users believed that for any type T
, keyof T
would always be assignable to a string
. Because symbol- and numeric-named properties invalidate this assumption, we expect some minor breaks which we believe to be easy to catch. In such cases, there are several possible workarounds.
If you have code that’s really meant to only operate on string properties, you can use Extract<keyof T, string>
to remove symbol
and number
inputs:
function useKey<T, K extends Extract<keyof T, string>>(obj: T, k: K) {
let propName: string = k;
// ...
}
If you have code that’s more broadly applicable and can handle more than just string
s, you should be able to substitute string
with string | number | symbol
, or use the built-in type alias PropertyKey
.
function useKey<T, K extends keyof T>(obj: T, k: K) {
let propName: string | number | symbol = k;
// ...
}
Alternatively, you can revert to the old behavior under the --keyofStringsOnly
compiler flag, but this is meant to be used as a transitionary flag.
If you intend on using --keyofStringsOnly
and migrating off, instead of PropertyKey
, you can create a type alias on keyof any
, which is equivalent to string | number | symbol
under normal circumstances, but becomes string
when --keyofStringsOnly
is set.
type KeyofBase = keyof any;
Breaking changes
keyof
types include symbolic/numeric properties
As mentioned above, keyof
types (also called “key query types”) now include names that are symbol
s and number
s, which can break some code that assumes keyof T
is assignable to string
. You can correct your code’s assumptions, or revert to the old behavior by using the --keyofStringsOnly
compiler option:
// tsconfig.json
{
"compilerOptions": {
"keyofStringsOnly": true
}
}
--pretty
on by default
Also mentioned above, --pretty
is now turned on by default, though this may be a breaking change for some workflows.
Trailing commas not allowed on rest parameters
Trailing commas can no longer occur after ...rest
-parameters, as in the following.
function pushElement(
foo: number,
bar: string,
...rest: any[], // error!
) {
// ...
}
This break was added for conformance with ECMAScript, as trailing commas are not allowed to follow rest parameters in the specification. Trailing commas should simply be removed following this syntax.
Unconstrained type parameters are no longer assignable to object
in strictNullChecks
The following code now errors:
function f<T>(x: T) {
const y: object | null | undefined = x;
}
Since generic type parameters can be substituted with any primitive type, this is a precaution TypeScript has added under strictNullChecks
. To fix this, you can add a constraint of object
:
// We can add an upper-bound constraint here.
// vvvvvvvvvvvvvv
function f<T extends object>(x: T) {
const y: object | null | undefined = x;
}
never
can no longer be iterated over
Values of type never
can no longer be iterated over, which may catch a good class of bugs.
declare let foo: never;
for (let prop in foo) {
// Error! `foo` has type `never`.
}
Users can avoid this behavior by using a type assertion to cast to the type any
(i.e. foo as any
).
What’s next?
We hope you’re as excited about the improvements to TypeScript 2.9 as we are – but save some excitement for TypeScript 3.0, where we’re aiming to deliver an experience around project-to-project references, a new unknown
type, a stricter any
type, and more!
As always, you can keep an eye on the TypeScript roadmap to see what we’re working on for our next release (as well as anything we didn’t get the chance to mention in this blog post for this release). We also have nightly releases so you can try things out on your machine and give us your feedback early on. You can even try installing it now (npm install -g typescript@next
) and play around with the new unknown
type.
Let us know what you think of this release over on Twitter or in the comments below, and feel free to report issues and suggestions filing a GitHub issue.
Happy Hacking!