TypeScript

TypeScript 2.7.1 変更点

こんにちはソウゾウ社@vvakameです。
今回はちょっと遅くなってしまいました…。

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

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

変更点まとめ

  • クラスのフィールドの初期化チェックをより厳密に行う Stricter class property checks
    • --strictPropertyInitialization が追加
      • --strict に含まれる
    • ? 無しの場合コンストラクタ内で初期化しないとダメ
  • 変数末尾に ! をつけることで初期化チェックを割愛できる Definite assignment assertions
  • CommonJSなモジュールのimportがbabelとか互換になる Easier ECMAScript module interoperability
    • --esModuleInterop が追加された
    • 1.8で入った --allowSyntheticDefaultImports みたいなパチモノではない
  • constなSymbol+computed propertyについて型推論が行われるようになった unique symbol types and const-named properties
    • 今まではこれがsymbolを使うの避ける理由の1つだった
  • --watch 時、コンパイルする毎にターミナルの表示をクリアする Cleaner output in --watch mode
  • --pretty 利用時のエラー表示を改善 Prettier --pretty output 1 2
    • 文字により見やすい色がつくようになった
    • ファイル名:line:column 形式にしてターミナルからエディタを開いた時に適切にジャンプできるように
  • 数値を区切る時のセパレータが増えた Numeric Separators
    • これがstage-3になったので
    • 123_456_789 的なやつ
  • タプルの互換性検査がより厳しくなった Fixed Length Tuples
    • BREAKING CHANGEです
    • [number, string, string][number, string] に突っ込めなくなった
  • instanceofin の改善 in operator narrowing and accurate instanceof
    • in がtype guardsに使えるようになった 1
    • instanceof が構造的に同じな異なるクラスをより適切にハンドリングできるようになった 2
  • より良いオブジェクトリテラルからの型推論 Smarter object literal inference
    • より実用的になった
  • 全てのQuickFixの適用の追加 Apply all quick fixes in a file
    • getCombinedCodeFix がLanguageServiceに追加されたそうな
  • CommonJS形式のモジュールからESモジュールへのリファクタリング Refactors to convert CommonJS module to ES6 module
  • インクリメンタルビルドのAPIがCompiler APIに生えた Support for incremental builder compiler API
    • 今までLanguageServiceでしかできなかったincremental buildがCompiler APIでもできるようになったという話のはず
  • async を足すQuickFixの追加 Quick fix to add missing async keyword
    • たぶんasync/awaitを使ってるのに関数やメソッドにasyncがついてないのを直すやつ
  • 補完リストに文脈にそったものを優先的に上位に持ってくるようにした Completion list preselects suggested item based on context
    • 内部的には isRecommended が追加されたらしい
  • 補完リストに表示する候補の追加 Completion list includes this., brackets, and curlies for JSX
    • this. が候補になるように 1
    • . ではなく [] で囲まないといけない場合、リプレースされるように 2
    • JSXで値を補完した時に {} で囲まれるように 3

クラスのフィールドの初期化チェックをより厳密に行う

今までのTypeScriptでは、クラスのフィールドの初期化はプログラマの責務でした。
str?: string;str: string | undefined; じゃない限り、strにはstringな値が入っていなければなりません。
コンストラクタで値をセットするのはプログラマの責務であり、初期化漏れをコンパイラが検出してくれませんでした。

この変更はプログラマのこの負担をコンパイラが肩代わりしてくれるものです。
コンストラクタで初期化が漏れていた場合、コンパイラがエラーにしてくれます。やったぜ!
--strictPropertyInitialization--strict で有効にできます。
TypeScriptのZENを感じている人はきっと --strict を使っていると思いますので、2.7にしたらデフォルトで有効になっているかもしれません。
tsconfig.jsonでstrict: trueを使いつつ個別にstrictPropertyInitialization: falseにすることもできます。

class Sample {
    str: string;
    // error TS2564: Property 'num' has no initializer and is not definitely assigned in the constructor.
    num: number;

    // 後述する初期化チェックの割愛
    bool!: boolean;

    constructor() {
        this.str = "";
        // num が初期化されてない…!
    }
}

このチェックを割愛し、今までのようにプログラマの責務として初期化を行うこともできます。
次の項目で見ていきます。

変数末尾に ! をつけることで初期化チェックを割愛できる

プロパティの初期化チェックが厳密化されたのは喜ばしいですが、コンストラクタで初期化できない場合があります。
次のように、初期化用のメソッドをコンストラクタから呼ぶ場合などです。

class Sample {
    str: string;
    // コンストラクタで初期化しないのでエラー 末尾に ! で回避可能
    num!: number;

    constructor() {
        this.str = "";
        this.initNum();
    }

    initNum() {
        this.num = 0;
    }
}

プログラマ的にはnewした後に他のメソッドが呼ばれるまでにプロパティが初期化できる、ということがわかる場合は多々あります。
そういう時のための抜け穴というわけですね。

筆者はこういう場合、num?: number; にすることが多かったため、嬉しい配慮と言えます。
なぜ ! をつけてよいかのドキュメンテーションをどうするか悩ましい気はしますね。

ちなみに、 ! を利用可能なのはクラスのプロパティに限りません。
普通の変数定義でも利用できます。

let x: number;
let y!: number;

// error TS2454: Variable 'x' is used before being assigned.
// y は ! をつけてあるので無視される
console.log(x, y);

CommonJSなモジュールのimportがbabelとか互換になる

--esModuleInterop が導入されました。
今まで、TypeScriptとBabelはCommonJSモジュールの export = ... 形式に対するESモジュール形式との表現形式に差がありました。
TypeScriptでは import * as hoge from "hoge"; で、Babelでは import hoge from "hoge"; でした。
Node.jsのCommonJS from ESModuleの実装がdefaultとしてexportされるようになったりしましたからね。このへん

tsc --init で生成されるtsconfig.jsonでもデフォルトで有効になっていますし、公式ブログでもNode.jsユーザはこのオプションを有効に活用するよう勧められています。

こういうコードが

import express from "express";

let app = express();

こうなる

"use strict";
var __importDefault = (this && this.__importDefault) || function (mod) {
    return (mod && mod.__esModule) ? mod : { "default": mod };
}
Object.defineProperty(exports, "__esModule", { value: true });
var express_1 = __importDefault(require("express"));
var app = express_1.default();

tslibも併せて更新されています。

今までも --allowSyntheticDefaultImports があったんですが、これは出力されるコードが変わるものではありませんでした。

constなSymbol+computed propertyについて型推論が行われるようになった

やったぜ。
Symbol系のインスタンスはconstであればunique symbol型として評価されるようになります。

const foo = Symbol();
const bar = Symbol();
let buzz = Symbol();

let x = {
    [foo]: 100,
    [bar]: "hello",
    [buzz]: true,
};

// ちゃんとnumberに型推論される!
let a = x[foo];
// ちゃんとstringに型推論される!
let b = x[bar];
// buzzがconstじゃないので string | number | boolean
let c = x[buzz];

今までのTypeScriptではSymbolを使った時の推論がなかったため、Symbolを使うのはあまり現実的ではありませんでした。
しかし、今回の対応で実用レベルになったと思います。
あとはIEのサポートを切るだけだ…!

なお、残念ながら Symbol.for はまだ対応してないようです。

We may choose to revisit this in the future

だそうな。

const foo = Symbol.for("a");
const bar = Symbol.for("a");

let x = {
    [foo]: 100,
    [bar]: "hello",
};

// foo === bar だけど今のところTypeScriptはこれを分かってくれない
// a は number, b は string と推論される (aもbもstringが正解)
let a = x[foo];
let b = x[bar];
// hello hello と表示される
console.log(a, b);

まぁSymbol.forはあまり使う局面が無さそうなので別にいいんじゃないっすかね…。

--watch 時、コンパイルする毎にターミナルの表示をクリアする

表題の通りです。
便利っちゃ便利。

--pretty 利用時のエラー表示を改善 Prettier

  • 文字により見やすい色がつくようになった
  • ファイル名:line:column 形式にしてターミナルからエディタを開いた時に適切にジャンプできるように

の2点です。
こんな感じ。

pretty

数値を区切る時のセパレータが増えた

提案中の仕様見てね!
stage-3になったので導入されました。

1_000_000_000           // Ah, so a billion
101_475_938.38          // And this is hundreds of millions

let fee1 = 123_00;       // $123 (12300 cents, apparently)
let fee2 = 12_300;       // $12,300 (woah, that fee!)
let amount1 = 12345_00;  // 12,345 (1234500 cents, apparently)
let amount2 = 123_4500;  // 123.45 (4-fixed financial)
let amount3 = 1_234_500; // 1,234,500

0.000_001 // 1 millionth
1e10_000  // 1^10000 -- granted, far less useful / in-range...

let budget = 1_000_000_000_000;
let nibbles = 0b1010_0001_1000_0101;
let message = 0xA0_B0_C0;
"use strict";
1000000000; // Ah, so a billion
101475938.38; // And this is hundreds of millions
var fee1 = 12300; // $123 (12300 cents, apparently)
var fee2 = 12300; // $12,300 (woah, that fee!)
var amount1 = 1234500; // 12,345 (1234500 cents, apparently)
var amount2 = 1234500; // 123.45 (4-fixed financial)
var amount3 = 1234500; // 1,234,500
0.000001; // 1 millionth
Infinity; // 1^10000 -- granted, far less useful / in-range...
var budget = 1000000000000;
var nibbles = 41349;
var message = 10531008;

タプルの互換性検査がより厳しくなった

lengthまで完全に一致してないと型として互換性があるものと見做されなくなりました。

let x: [number, string, string] = [1, "2", "3"];
let y: [number, string] = [1, "2"];

// TypeScript 2.6までは長いほうを短いほうに突っ込むのはOKだったが…
// error TS2322: Type '[number, string]' is not assignable to type '[number, string, string]'.
//   Property '2' is missing in type '[number, string]'.
x = y;

// これはもともとダメ
// error TS2322: Type '[number, string, boolean]' is not assignable to type '[number, string]'.
//  Types of property 'length' are incompatible.
//  Type '3' is not assignable to type '2'.
y = x;

内部的にはざっくりこういう型だと思いねぇ ということだそうです。

interface NumStrTuple extends Array<number | string> {
    0: number;
    1: string;
    length: 2; // number ではなく 2
}

TypeScript 2.6までだとこんな感じでした。

interface NumStrTuple extends Array<number | string> {
    0: number;
    1: string;
}

これはBREAKING CHANGEですが、まぁArrayをtupleとして扱うのはあまり筋がよくないしこの機会にやめるといいんじゃないかと思います…。

instanceofin の改善

なぜこの2つを1トピックにまとめたのか謎…

instanceof の改善

今まで、構造が同じであれば異なる型であっても、単純な型に簡略化されていた。
それが行われなくなり、instanceof による型の絞込も実際のクラスの継承関係を考慮して行われるようになりました。

class A { }
class B extends A { }
class C extends A { }
class D extends A { c!: string }
class E extends D { }

let x1 = Math.random() < 0.5 ? new A() : new B();  // A
let x2 = Math.random() < 0.5 ? new B() : new C();  // B | C (前まで同じ構造なので B にまとめられてた)
let x3 = Math.random() < 0.5 ? new C() : new D();  // C | D (前まで同じ構造なので 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)[] (前まで同じ構造なので B[] にまとめられてた)

function f1(x: B | C | D) {
    if (x instanceof B) {
        x;  // B (前まで同じ構造なので B | D と判定されてた)

    } else if (x instanceof C) {
        x;  // C

    } else {
        x;  // D (前は never と判定されてた)
    }
}


function f2(x: B | D | E) {
    if (x instanceof D) {
        x;  // D | E (前は D と判定されてた)
    }
}

実際の振る舞いにより近くなりわかりやすくなりましたね。
実際にこれが役立つ場面は滅多にない気もしますが。

in の改善

in がtype guardsとして使えるようになった。

interface A {
    x: number;
}
interface B {
    y: string;
}

let a: A = { x: 1 };
let b: B = { y: "a" };

let q = Math.random() < 0.5 ? a : b;
if ("x" in q) {
    // x あるから A
    q.x;
} else {
    // x ないから B
    q.y;
}

はい。

より良いオブジェクトリテラルからの型推論

今まで、複数のオブジェクトリテラルが絡んだときの型推論は共通の要素だけを集めた積集合でした。
2.7からは、自分が持たない要素は ? として持つ形式になります。
言葉で説明するのが厳しいので次のコードを見てください。

let obj = Math.random() ? { a: "a" } : { b: "b" };

// obj の型は次の ObjType と等価
interface A {
    a: string;
    b?: string;
}
interface B {
    a?: string;
    b: string;
}
type ObjType = A | B;

let alt: ObjType = obj;

Arrayの例。
objの各要素に . で手軽にアクセスできている。

let array = [
    { str: "a" },
    { num: 1 },
    { bool: true },
];

let obj = array[0];

// string | undefined
obj.str;
if (obj.str) {
    // string
    obj.str;
}

// number | undefined
obj.num;
// boolean | undefined
obj.bool;
// ネストした構造もイケる
let array = [
    { kind: "circle", data: { r: 1, point: { x: 2, y: 3 } } },
    { kind: "square", data: { p1: { x: 1, y: 1 }, p2: { x: 4, y: 4 } } },
];
let obj1 = array[1];

// string
obj1.kind;
// number | undefined;
obj1.data.r;
// { x:  number; y: number; } | undefined
obj1.data.p1;

// T の推論ルールは先のものと同じ
declare function f<T>(...sources: T[]): T;

// obj1 と同じ型
let obj2 = f(
    { kind: "circle", data: { r: 1, point: { x: 2, y: 3 } } },
    { kind: "square", data: { p1: { x: 1, y: 1 }, p2: { x: 4, y: 4 } } },
);

柔軟ですね。
実際にこのようなコードを書きたい場合はたまーにありますね。

全てのQuickFixの適用の追加

たぶん字面まんまのやつだと思います。
LanguageServiceに getCombinedCodeFix というメソッドが追加されているそうな。

VSCodeにはこのへんで実装が入ったっぽいですね。
VisualStudioCode Insidersで次のようなコードを突っ込んでみると試せます。

class Sample {
    method() {
        // Declare property 'x' (Fix all in file) がある
        this.x = 0;
        this.y();
    }
}

CommonJS形式のモジュールからESモジュールへのリファクタリング

require("fs"); の require の所を選択するとESモジュールへの変換のリファクタリングがサジェストされます。
見た感じ、 allowJs: true の時しか利用できないようですね。

インクリメンタルビルドのAPIがCompiler APIに生えた

watch的なAPIがCompiler APIに生えた(Language Serviceではなく!)というものっぽいです。
詳細はPRを見てください。

async を足すQuickFixの追加

awaitしたのにasyncが関数やメソッドについてなかったら足してくれるやつ。

function callAsync() {
    const v = await funcAsync();
    console.log(v);
}

function funcAsync(): Promise<{}> {
    return Promise.resolve({});
}

補完リストに文脈にそったものを優先的に上位に持ってくるようにした

見出しのまんま… のはずだけど手元では確認できてない。
VSCodeにもここで入ってるはずだけど。

補完リストに表示する候補の追加

this. が候補になるように

this. を入力しなくてもthisのコンテキストの候補も補完候補に出るようになる。
はず?
手元でVSCode Insidersで試してみたけど出なかった…。

class C {
    "foo bar": number;
    xyz() {
        // (method) C.xyz(): void
        // (property) C["foo bar"]: number
        // ↑ みたいな補完候補が出るはず… なんだけど
    }
}

function f(this: { x: number }) {
    // (property) x: number
    // ↑ みたいな補完候補が出るはず… なんだけど
}

. ではなく [] で囲まないといけない場合、リプレースされるように

const x: { " foo": 0, "foo ": 1 } 的な時に x." foo" とかが候補に出て、確定したら . ではなく [" foo"] に置き換わってくれるもの…のはず?
手元でVSCode Insidersで試してみたけど出なかった…。

JSXで値を補完した時に {} で囲まれるように

<div foo=/*1*/ />/*1*/ のとこで入力補完を実行すると、結果が {} で囲まれるようになった。