TypeScript

TypeScript 2.9.1 変更点

こんにちはメルペイ社@vvakameです。

TypeScript 2.9.1がアナウンスされました。

What's new in TypeScriptも更新されているようです。
破壊的変更もあるよ!

この辺に僕が試した時のコードを投げてあります。

ちなみに、次のバージョンは2.10じゃなくて3.0らしいです。

変更点まとめ

  • ファイル名のリネームのサポート Add 'renameFile' command to services
    • ファイル名をリファクタリングできるようになった
  • 選択範囲を別ファイルに切り出す操作のサポート Add 'move to new file' refactor
    • 定義を別ファイルに切り出すリファクタリングができるようになった
  • 使ってない定義があったら教えてくれるようになった Show unused declarations as suggestions
    • --noUnusedLocals--noUnusedParameters でエラーになる箇所について、これらを指定しない場合警告として表示してくれるようになった
  • プロパティをgetter/setterに変換できるようになった Convert property to getter/setter
    • そのまんま
  • 型として import(...) をどこでも使えるようになった Allow import(...)-ing types at any location
    • let p: import("./foo").Person とか書ける
    • ついでにimportした関数とかから作った要素をexportする時の縛りがゆるくなった
  • --pretty がデフォルトで有効になった --pretty error output by default
    • tsc --pretty とかするとエラー表示が見やすくなるオプションがあった これがデフォルト有効に
    • --pretty false で今までと同じ出力
  • .json をimportした時にいい感じの型が付くようになった New --resolveJsonModule
    • --resolveJsonModule が生えた
    • moduleResolutionnode のとき、jsonをimportするとjsonの中身に応じた型が自動的に付く
  • タグ付きテンプレートリテラルにgenericsの型を指定できるようになった Support for passing generics to tagged template calls
    • styledComponent<MyProps>`font-size: 1.5em;`こんな感じ
  • keyofとかMapped typesでstring以外のnumberとかsymbolも出てくるようになった Support number and symbol named properties with keyof and mapped types
    • keyof Tstring | number | symbol が得られるようになった
    • --keyofStringsOnly も用意された
  • JSXのタグの中でComponentにgenericsの型を指定できるようになった Support for passing generics to JSX elements
    • <MyComponent<number> data={12} /> こゆ感じ
  • import.meta がサポートされた Support for import.meta
  • .d.ts.map が出力できるようになった Declaration source maps and code navigation via them
    • --declarationMap が導入された
    • --declaration と併用する
    • .d.ts.map が生成され、 .d.ts に飛んだ時にオリジナルの .ts コードを見ることができる
    • --inlineSource とかは反映されないのでつらい
  • "提案"レベルの情報の追加 Show suggestion diagnostics for open files
    • error, warning の他に suggestion(info) が追加された
  • require 使ってたら import に変換してくれる Convert require to import in .ts files
    • 1個前の仕組みを使っている
    • .ts 限定らしい
    • 時代を感じますねぇ(互換性があるという判断が行われるようになった)
  • Quick Fixとかで使われるクォートを " と ' のどっちかに指定できるオプション Support setting quote style in quick fixes and refactorings
    • とか言ってるけど実は(Language) Serviceに対して設定を追加できるAPIが生えた
      • 裏で UserPreferences というのが生えた
    • 今できるのはクォートの統一とモジュールをimportする時に相対パスにするか絶対パスにするかの設定ができる
      • typescript.preferences.quoteStyle
      • typescript.preferences.importModuleSpecifier
  • Node.jsのビルトインモジュールを使おうとしたら @types/node 入れてくれる Install @types/node for built-in node modules
    • まぁあったほうが便利ですよね
  • strictNullChecksobject を底にしてない型パラメータの値を object に割り振り不可にした Unconstrained type variable not assignable to 'object'
    • 一貫性を破壊できたのが修正された
  • neverfor-in とか for-of とかで繰り返し処理しようとした時にエラーになるようになった Don't allow to iterate over 'never'
    • そのまんま

破壊的変更!

上記リストのうち、破壊的変更を伴うのは次のものになります。

  • keyofとかMapped typesでstring以外のnumberとかsymbolも出てくるようになった
  • --pretty がデフォルトで有効になった
  • 関数の引数に可変長引数がある場合、末尾のカンマを許さないように変更
  • strictNullChecksobjectobject を底にしてない型パラメータの値を割り振り不可にした
  • neverfor-in とか for-of とかで繰り返し処理しようとした時にエラーになるようになった

ファイル名のリネームのサポート

そのまんまです。
VSCodeのファイルエクスプローラでファイル名を変更しようとすると、変更しようとしたファイルを参照しているコードのimportを書き換える?と聞かれます。
No, never update imports, Yes, always update imports, No, Yes の選択肢が現れます。
Yesを選ぶと自動的にimport句が更新されます。

仕様上、ファイルをrenameした後にダイアログを出しているようで、renameをキャンセルすることは(現時点では)できないようです。

Yes, always update imports を選ぶとUser Settingsに次の設定が追加されました。

{
    "typescript.updateImportsOnFileMove.enabled": "always"
}

大変便利なので常時Yesでいいんじゃないでしょうか。

選択範囲を別ファイルに切り出す操作のサポート

そのまんまです。
次のようなファイルがあるとします。

interface Foo {
    name: string;
}

let f: Foo = { name: "test" };

export { }

1〜3行目を選択して、Quick Fixで Move to a new file を選ぶと該当行を別ファイルに切り出してくれます。

import { Foo } from "./Foo";

let f: Foo = { name: "test" };
export interface Foo {
    name: string;
}

こんな感じですね。
ちゃんとexportを付与してくれたりimport句も作ってくれたりして偉いです。

元のファイルで export {} 的なESモジュールであると示しておかないと意図通りに動いてくれないので少しだけ注意が必要です。

使ってない定義があったら教えてくれるようになった

--noUnusedLocals--noUnusedParameters というのがすでにあるんですが、これを使っていない時でも、エディタ上で使ってない変数とかをグレーアウトしてほんのりと使ってないことを教えてくれるようになった、という奴です。
JetBrains系のIDEではこういうほんのりと教えてくれる奴が充実していて役に立っているので嬉しいですね。
まぁ僕は上記オプションをONにしてコンパイルエラーにしますが…。

プロパティをgetter/setterに変換できるようになった

そのまんまですね。
クラスのプロパティ名を選択してQuick Fixでgetter/setterに変換できるようになりました。

次のコードのaとbを変換してみます。

class Hoge {
    a?: string;
    b: string | undefined;
}

すると、こうなります。

class Hoge {
    private _a?: string;
    public get a(): string { // ← string | undefined じゃないのでコンパイルエラーになる
        return this._a;
    }
    public set a(value: string) {
        this._a = value;
    }
    private _b: string | undefined;
    public get b(): string | undefined {
        return this._b;
    }
    public set b(value: string | undefined) {
        this._b = value;
    }
}

ちょっとおバカですね…。
でも実用上の役にはしっかり立ちそうです。

型として import(...) をどこでも使えるようになった

ECMAScriptにdynamic importという仕様があります。
それと同じsyntaxで型注釈が書けるようになった…というものです。

export function hello(name: string) {
    return `Hello, ${name}!`;
}

export interface Data {
    id: string;
    content: any;
}
type fooType = typeof import("./foo");

function f(fn: typeof import("./foo").hello) {
    fn("import()");
}

const fn1: typeof import("./foo").hello = name => `Hi, ${name}`;
fn1("import()");

const fn2: fooType["hello"] = name => `Hi, ${name}`;
fn2("import()");


const data1: import("./foo").Data = {
    id: "foo",
    content: "bar",
};

type Data = import("./foo").Data;
interface Data1 extends Data { }
// この書き方はinterfaceのsyntax的にダメ
// interface Data2 extends import("./foo").Data { }

いろいろな書き方ができますね。

TypeScriptでは import ... from "..."; で型をimportしても、値として利用しなければコンパイル後には消えてしまいます。
よって、この書き方ができなくても実用上問題はなかった気もしますが、わかりにくい仕様だったことに変わりはありません。
ここで、 import(...) と同様の書き方で型としても参照できるようになり、わかりやすさは改善されたと言えるでしょう。

使い所がいまいちわからない…。

また、この記法の導入により型定義を生成するときの制限が緩和されました。

TypeScript 2.8までは次のようなコードをコンパイルするとエラーになっていました。

import { createHash } from "crypto";

export const hash = createHash("sha256");
src/importAsTypes/relax.ts:3:14 - error TS4023: Exported variable 'hash' has or is using name 'Hash' from external module "crypto" but cannot be named.

3 export const hash = createHash("sha256");
               ~~~~

これのコンパイルを通すには import { createHash, Hash } from "crypto"; と書く必要がありました。

これは、 .d.ts ファイルが次のように出力されていたためです。

/// <reference types="node" />
import { Hash } from "crypto";
export declare const hash: Hash;

TypeScript 2.9からは Hash をimportしていなくてもコンパイルが通ります。
そして、次のような .d.ts ファイルが生成されます。

/// <reference path="../../node_modules/@types/node/index.d.ts" />
/// <reference types="node" />
export declare const hash: import("crypto").Hash;

…1行目はいるんですかね…?

--pretty がデフォルトで有効になった

前からあった --pretty というエラーをpretty printしてくれるオプションがありました。
今回からこれがデフォルトになり、2.8までと同様の出力にしたい場合、 --pretty false とします。

src/importAsTypes/index.ts:21:25 - error TS2499: An interface can only extend an identifier/qualified-name with optional type arguments.

21 interface Data2 extends import("./foo").Data { }
                           ~~~~~~~~~~~~~~~~~~~~

.json をimportした時にいい感じの型が付くようになった

まんまです。

import data from "../../tsconfig.json";

// OK
console.log(data.compilerOptions.target);
// こっちは存在しないのでエラーになる
console.log(data.notExists);

なお、moduleResolutionをclassicにするとこの機能は使えません。

タグ付きテンプレートリテラルにgenericsの型を指定できるようになった

タグ付きテンプレート用の関数にgenericsを使えるようになりました。
主にReactというかstyled-componentにいい感じに型をつけるために導入されたっぽいですね。

function tag<T>(strs: TemplateStringsArray, ...args: T[]) {
    console.log(strs, ...args);
}

// パラメタが全部 number なのでOK
tag<number>`hoge${1}${2}`;
// 2つ目のパラメタが number なのでエラー
// tag<string>`hoge${"A"}${2}`;
// 1つ目と2つ目のパラメタが一致してないのでエラー
// tag`${true}${1}`;

styled-componentだとこういう感じらしいです。

declare function styledComponent<Props>(strs: TemplateStringsArray): Component<Props>;

styledComponent<MyProps>`
    font-size: 1.5em;
`

interface Component<T> {
}

interface MyProps {
    name: string;
    age: number;
}

よかったですね。

keyofとかMapped typesでstring以外のnumberとかsymbolも出てくるようになった

今までは string なプロパティしかkeyとして切り出せなかったのが、numberやsymbolもkeyとして取れるようになりました。
keyof Tstring | number | symbol 的なものが取れる可能性がある、ということですね。

const sym = Symbol();

interface Foo {
    str: string;
    11: number;
    [sym]: symbol;
}

// typeof sym | "str" | 11 となる
type keyofFoo = keyof Foo;

// typeof sym
type A = Extract<keyofFoo, symbol>;
// str
type B = Extract<keyofFoo, string>;
// 11
type C = Extract<keyofFoo, number>;

// なお --keyofStringsOnly だと keyofFoo は "str" になる

// ちなみに…
type array = [1, 2, 3];
// "1", "2", "3", ...Arrayのメソッドいろいろ
type keyofArray = keyof array;

旧来の挙動にするために --keyofStringsOnly が用意されました。
部分的にやったいきたい場合、 keyof FooExtract<keyof Foo, string> に置き換えていくのがよい、とのことです。

JSXのタグの中でComponentにgenericsの型を指定できるようになった

Reactのやり込みが足りないので活用ポイントがよくわかんないですね…。
こんな感じだそうです。

import { Component } from "react";

interface Props<T> {
    data: T;
}

class MyComponent<T> extends Component<Props<T>> {
    render() {
        return <div>{this.props.data}</div>;
    }
}

<MyComponent<number> data={12} />

パーサさんも辛い気持ちになったりしているんだろうなぁ。

import.meta がサポートされた

import.metaがサポートされました。
利用には --target esnext--module esnext が必要です。

declare global {
    interface ImportMeta {
        foo: string;
    }
}

import.meta.foo;
// これは定義されてないのでエラー
// import.meta.notExist;

そういえばTypeScriptでmjsが出力できないのはちょっと不便ですね。

.d.ts.map が出力できるようになった

.d.ts.map は .d.ts と .ts ファイルの対応関係を記すものです。
.js.map が .js と .ts の関係を記すのと同様ですね。

コードを書いているとき、定義にジャンプすると今まで .d.ts の定義に飛んでいたのが、ちゃんと対応関係が定義されている場合、 .ts ファイルの該当部分にジャンプできます。

有効にするには --declarationMap を使います。
--declaration との併用が必須です。
なお、 --inlineSource を使っても .d.ts.map に .ts の内容は現時点では保存されません。かなしい。

.tsと.d.tsファイルが同じ場所にある場合、.tsの読み込みが優先されます。
ですので、npm publishするときは .ts ファイルをignoreするか、 --outDir で別ディレクトリに出力しているかのどちらかだと思います。
もしあなたが後者なら、このオプションを使いやすく、ユーザの役に立つでしょう。
ちなみに僕は前者スタイルです(かなしい)

"提案"レベルの情報の追加

Error, Warning の他に Suggestion が追加されました。

現時点では、次の項目である require 使ってたら import に変換してくれる が実装されています。

Language Service周りの開発に興味がある人はPRをよく読むとよさそうです。

require 使ってたら import に変換してくれる

現時点では、 CommonJSモジュールをESモジュールに変換する?という提案が実装されています。

const fs = require("fs");

↑を↓に変換してくれます

import fs from "fs";

こういう変換を行ってもよい時代になった、とTypeScript teamが判断したこと自体が、趣深いですね。

Quick Fixとかで使われるクォートを " と ' のどっちかに指定できるオプション

裏で(Language) Serviceに対して設定を追加できるAPIが生えました。
UserPreferences というのが生えてます。

とりあえず次の設定をVSCodeに対して行うとLanguage Serviceに設定が伝搬されます。
新規にQuick Fixやリファクタリングで生成された文字列リテラルがダブルクォートかシングルクォートかを指定できるというものです。

{
    "typescript.preferences.quoteStyle": "double"
}

typescript.preferences.importModuleSpecifier というのも追加され、 non-relativerelative が選べますが baseUrl の設定などに左右されてめんどくさいので割愛します。

Node.jsのビルトインモジュールを使おうとしたら @types/node 入れてくれる

そのまんまです。
今までも .d.ts が存在しないパッケージを @types/ 無しに使っていると、Quick Fixで @types/ を入れてくれていました。
この機能はNode.jsのモジュール@types/node 無しに使うと、 これを入れるか聞いてくれるというものです。

よくあるパターンなのであると嬉しいですね。

strictNullChecksobject を底にしてない型パラメータの値を object に割り振り不可にした

objectに不正にstringとかを代入できるバグがあったので塞がれたというやつです。

詳細は次のコードの通りです。

function f1<T>(x: T) {
    // 今回からエラー! T を object に代入できちゃうのはヤバい!エラーになるのは正しい!
    const y: object = x;
    console.log(y);
}
f1("string");
f1({});

// T の底を object にする
function f2<T extends object>(x: T) {
    const y: object = x;
    console.log(y);
}
// string は object ではないのでエラーになる
f2("string");
f2({});

function f3<T>(x: T) {
    // object をやめて {} にする
    const y: {} = x;
    console.log(y);
}
// 両方OK
f3("string");
f3({});

neverfor-in とか for-of とかで繰り返し処理しようとした時にエラーになるようになった

次のようなコードが両方エラーになるようになりました

const neverVar: never = (() => { throw new Error() })();

// error TS2407: The right-hand side of a 'for...in' statement must be of type 'any', an object type or a type parameter, but here has type 'never'.
for (let v in neverVar) {
    console.log(v);
}

// error TS2488: Type 'never' must have a '[Symbol.iterator]()' method that returns an iterator.
for (let v of neverVar) {
    console.log(v);
}