[官方] TypeScript 2.9 发布 | VSCode 等编辑器高度整合及 import() types 等

3,153 阅读8分钟
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.



Moving two interfaces to their own respective files, and then renaming those files while keeping all reference 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.

A parameter being grayed out as an unused declaration

Convert property to getter/setter

Thanks to community contributor Wenlu Wang, TypeScript 2.9 supports converting properties to get- and set- accessors.



Generating a get- and set-accessor from a class property declaration.

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 contents\n${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 contents\n${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 strings, 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 strings, 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 symbols and numbers, 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!

Back to
top