Help us understand the problem. What is going on with this article?

TypeScript3.6の新機能

More than 1 year has passed since last update.

2019/08/19にTypeScript3.6のRC版がリリースされました
以下はリリースの公式アナウンス、Announcing TypeScript 3.6 RCです。

Announcing TypeScript 3.6 RC

本日、TypeScript3.6のRC版をリリースしたことをお知らせします。
このRCは、正式リリース版に非常に近いものとなる予定で、正式リリースまでの数週間は安定性を改善していきます。
RC版を使用するには、NuGetから入手するか、npmから以下のコマンドを使ってください。

npm install -g typescript@rc

エディタのサポートは以下のとおりです。
Visual Studio 2019/2017
Visual Studio CodeおよびSublime Text

3.6の新機能を試してみましょう。

Stricter Generators

TypeScript3.6では、イテレータとジェネレータに厳密なチェック機構が導入されました。
以前のバージョンではジェネレータを使用した際、返ってきた値がyieldされた値なのかreturnされた値なのかを区別することができませんでした。

function* foo() {
    if (Math.random() < 0.5) yield 100;
    return "Finished!"
}

let iter = foo();
let curr = iter.next();
if (curr.done) {
    // 3.5以下ではstring | number。done==trueなので本当はstringだとわかるはず。
    curr.value
}

さらに、ジェネレータではyieldの型は常にanyです。

function* bar() {
    let x: { hello(): void } = yield;
    x.hello();
}

let iter = bar();
iter.next();
iter.next(123); // 実行時エラー

TypeScript3.6では、前者はcurr.valueの型がstringであることを正しく認識し、後者の例ではnext()を呼び出す時点で正しくエラーが発生するようになります。
これは、IteratorとIteratorResultの型宣言にいくつかのパラメータを追加する変更を行ったこと、またGenerator型という新しい型を導入したことによって実現されました。

Iterator型は、yieldする型とreturnする型、さらにnextが受け入れる型を指定することができるようになりました。

interface Iterator<T, TReturn = any, TNext = undefined> {
    // 引数は0か1個。undefinedは不可
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return?(value?: TReturn): IteratorResult<T, TReturn>;
    throw?(e?: any): IteratorResult<T, TReturn>;
}

それに伴い、iterableでありreturnとthrowメソッドが常に存在する新たなIterator、Generator型が導入されました。

interface Generator<T = unknown, TReturn = any, TNext = unknown>
        extends Iterator<T, TReturn, TNext> {
    next(...args: [] | [TNext]): IteratorResult<T, TReturn>;
    return(value: TReturn): IteratorResult<T, TReturn>;
    throw(e: any): IteratorResult<T, TReturn>;
    [Symbol.iterator](): Generator<T, TReturn, TNext>;
}

yieldされた値とreturnされた値を区別できるように、TypeScript3.6ではIteratorResultがunion型に変更されます。

type IteratorResult<T, TReturn = any> = IteratorYieldResult<T> | IteratorReturnResult<TReturn>;

interface IteratorYieldResult<TYield> {
    done?: false;
    value: TYield;
}

interface IteratorReturnResult<TReturn> {
    done: true;
    value: TReturn;
}

これがどういうことかというと、イテレータの返してくる値の型を適切に絞り込むことができるということです。

また、ジェネレータ内でのyieldの使い方を見て、next()がジェネレータに渡す値の型も正しくチェックします。

function* foo() {
    let x: string = yield;
    console.log(x.toUpperCase());
}

let x = foo();
x.next(); // 初回nextの引数は無視される
x.next(42); // コンパイルエラー、stringを期待しているのにnumberが来た

yieldに渡す値の型、yieldとreturnで戻ってくる値の型をそれぞれを明示的に強制することもできます。
以下の例では、next()の引数はbooleanであり、返り値はdoneの状態によって変化し、それぞれnumberとstringです。

// yieldの型はnumber、returnの型はstring、引数の型はboolean
function* counter(): Generator<number, string, boolean> {
    let i = 0;
    while (true) {
        if (yield i++) {
            break;
        }
    }
    return "done!";
}

var iter = counter();
var curr = iter.next()
while (!curr.done) {
    console.log(curr.value);
    curr = iter.next(curr.value === 5)
}
console.log(curr.value.toUpperCase()); // 出力は0,1,2,3,4,5,"DONE!"

変更の詳細についてはこちらのプルリクを参照してください。

More Accurate Array Spread

ES2015以前では、配列スプレッドによるfor ofループの反復処理は少々とても重たい処理でした。
そのためTypeScriptは、デフォルトでは単純な配列のみ反復が可能なものとし、--downlevelIterationフラグを使用すれば他の型での反復も使用できるようになっています。
このフラグがあると、出力されるコードはより正確ですが、はるかに巨大なものになります。

--downlevelIterationフラグはデフォルトでオフになっていますが、ES5以前をターゲットとしているコードは大抵の場合は配列しか反復しないのでほぼ問題ありません。
ただし、一部のエッジケースにおいてはサポートされている配列のみを出力するため、異なる出力がなされることがあります。

以下の例を挙げます。

[...Array(5)]

これは以下の配列と同じです。

[undefined, undefined, undefined, undefined, undefined]

しかし、TypeScriptは最初のコードを以下のように変換します。

Array(5).slice();

これは[...Array(5)]と同じではありません。
Array(5)は長さ5の配列を生成しますが、中身は何も入っていません。

1 in [undefined, undefined, undefined] // true
1 in Array(3) // false

これはたいしたことのない違いに思えるかも知れませんが、実際は多くのユーザがこの挙動の違いにより望ましくない動作に遭遇していたことがわかります。
TypeScript3.6では新たな__spreadArraysヘルパーが導入され、ES2015以前の古い環境で--downlevelIterationを有効にしなかった場合にも、正確に同じ挙動をするようになりました。
__spreadArraystslibでも使用することができます。

詳細についてはこちらのプルリクを参照してください。

Improved UX Around Promises

非同期処理を扱う、最近の最も一般的な方法がPromiseです。
残念ながら、Promiseを安易に使用すると混乱することがあります。
TypeScript 3.6では、誤ったPromiseの使い方に幾つかの改善を導入します。

たとえば他の関数に渡すときに.then()を忘れたりawaitを忘れたりといった間違いは、Promiseでは非常によく見かけます。
TypeScriptはawaitが必要である旨のエラーメッセージを表示します。

interface User {
    name: string;
    age: number;
    location: string;
}

declare function getUserData(): Promise<User>;
declare function displayUser(user: User): void;

async function f() {
    displayUser(getUserData());
//              ~~~~~~~~~~~~~
// Argument of type 'Promise<User>' is not assignable to parameter of type 'User'. Did you forget to use 'await'?
}

また、.then()awaitする前に中身にアクセスする間違いもよく見かけます。
以下は最もありがちなやらかしの例です。

async function getCuteAnimals() {
    fetch("https://reddit.com/r/aww.json")
        .json()
    //   ~~~~
    // Property 'json' does not exist on type 'Promise<Response>'. Did you forget to use 'await'?
}

万一ユーザがawaitの必要性を認識していなかった場合でも、このエラーメッセージによって何を修正しなければならないか認識することができます。
この通知によってあなたの人生が楽になるだけではなく、場合によっては修正候補の提供も行います。

※画像

詳細についてはこちらのissueと、関連するプルリクを参照してください。

Better Unicode Support for Identifiers

TypeScript 3.6は、ターゲットがES2015以降である場合にかぎりUnicodeサポートを強化します。

const 𝓱𝓮𝓵𝓵𝓸 = "world"; // 以前はNGだった。'--target es2015'ならOK

import.meta Support in SystemJS

TypeScript 3.6は、モジュールのtargetがsystemである場合、import.metacontext.metaに修正します。

// このモジュール
console.log(import.meta.url)

// こうなる
System.register([], function (exports, context) {
  return {
    setters: [],
    execute: function () {
      console.log(context.meta.url);
    }
  };
});

get and set Accessors Are Allowed in Ambient Contexts

古いバージョンのTypeScriptでは、.d.tsなどのアンビエント宣言されたコンテキストにはアクセサを記述することができませんでした。
というのも、プロパティの読み書きに関しては、アクセサとプロパティで違いがなかったためです。
しかし、Class-fieldsのproposalの展開によっては、旧バージョンのTypeScriptと異なる動作になる可能性が出てきました。
そのため、この動作の違いを提供する方法が必要であることがわかりました。

その結果、TypeScript 3.6では、アンビエント宣言されたコンテキストでアクセサを記述することができるようになります。

declare class Foo {
    // 3.6以降OK
    get x(): number;
    set x(val: number): void;
}

TypeScript 3.7ではコンパイラがこの機能を使って、.d.tsファイルもアクセサを発行するようになります。

Ambient Classes and Functions Can Merge

これまでのTypeScriptでは、いかなる状況でもclassとfunctionをmergeするとエラーになっていました。
今後は、アンビエントなclassとfunction(declareされているか.d.tsに書かれてる)はmergeできます。
すなわち、以下のように記述可能です。

export declare function Point2D(x: number, y: number): Point2D;
export declare class Point2D {
    x: number;
    y: number;
    constructor(x: number, y: number);
}

もう、以下のように書く必要はありません。

export interface Point2D {
    x: number;
    y: number;
}
export declare var Point2D: {
    (x: number, y: number): Point2D;
    new (x: number, y: number): Point2D;
}

この書式の利点はコンストラクタを簡潔に記述できること、そしてnamespaceの書かれているdeclareもマージできることです。
namespace+varをマージすることはできません。

詳細についてはこちらのプルリクを参照してください。

APIs to Support --build and --incremental

TypeScript 3.0では、外部リソースを参照してビルドする--buildフラグが導入されました。
さらにTypeScript 3.4では、前回のコンパイル情報を保持しておき、更新が必要なファイルだけを再ビルドする--incrementalフラグが導入されました。
これらのフラグは、プロジェクトをより柔軟に構築し、ビルドを高速化するために役立つものです。
しかし残念ながら、GuplやWebpackといったサードパーティのビルドツールではこのフラグは機能しませんでした。
TypeScript 3.6では、プログラムのビルドで使用可能なふたつのAPIセットを公開します。

--incrementalビルドに対応するために、ユーザはcreateIncrementalProgramおよびcreateIncrementalCompilerHostのAPIを使用することができます。

さらに新規追加されたreadBuilderProgram関数を使うことで、.tsbuildinfoファイルから古いプログラムインスタンスを再生成することができます。
この関数から返されたインスタンスは変更することができません。
新しいプログラムを作成するためだけに使用されることを意図したもので、つまり他のcreate*Programに渡すoldProgramパラメータに渡されるだけのものです。

また、プロジェクト参照をさらに活用するためにcreateSolutionBuilder関数が追加されました。
これは新しい型であるSolutionBuilderインスタンスを返します。

これらの詳細についてはこちらのプルリクを参照してください。

Semicolon-Aware Code Edits

Visual StudioやVisual Studio Codeなどのエディタは、クイックフィックス、リファクタリング、他モジュールからの値の自動インポートなど、多くの機能を使用できます。
TypeScriptはこれらの変換をさらに強化するため、TypeScriptの旧バージョンは全てのステートメントの末尾に自動的にセミコロンを追加します。
何故か多くのユーザはこのコーディングガイドラインを不服とし、セミコロン自動挿入に不満を抱いていました。

今後のTypeScriptは、そのファイルが末尾にセミコロンを用いているか否かを検出するようになります。
セミコロンが使われていなさそうであれば、TypeScriptもセミコロンを追加しません。

詳細についてはこちらのプルリクを参照してください。

Smarter Auto-Import Syntax

JavaScriptにはECMAScriptスタンダード、Node.jsがサポートするCommonJS、AMD、System.jsなどなど、多種多様な異なる構文と規則が存在します。
ほとんどの場合、TypeScriptはデフォルトでECMAScriptモジュール構文を使用した自動インポートを行います。
しかしこれはコンパイラ設定が異なる一部のプロジェクトや、プレーンなJavaScriptをrequireするNodeプロジェクトなどでは不適切な構文でした。

TypeScript 3.6では、他モジュールをインポートする際に、既存のインポートを確認してインポート方法を判定します。

詳細についてはこちらのプルリクを参照してください。

Breaking Changes

互換性の失われる変更。

String-Named Methods Named "constructor" Are Constructors

ECMAScript仕様に則り、constructorという名前の関数は識別子で定義されていても文字列で定義されていても、常にコンストラクタ関数として扱われます。

class C {
    "constructor"() {
        console.log("これはコンストラクタ");
    }
}

このBCを避けることのできる面白い例外は、constructorを算出プロパティで定義することです。

class D {
    ["constructor"]() {
        console.log("これはコンストラクタではなく、ただのメソッド");
    }
}

DOM Updates

lib.dom.d.ts内で多くの変更や削除がありました。
それには次のようなものが含まれますが、これが全てではありません。

・グローバルwindowはもはやWindow型ではありません。Window & globalThis型です。
GlobalFetchは消えました。かわりにWindowOrWorkerGlobalScopeを使ってください。
Navigatorの非推奨プロパティは消滅しました。
experimental-webglはなくなりました。webglもしくはwebgl2を使ってください。

変更によって問題が発生した場合はIssueで教えてください。

JSDoc Comments Don’t Merge

JavaScriptファイルにおいては、型の判断に使用するのは直前のJSDocコメントだけになります。

/**
 * @param {string} arg
 */
/**
 * なんか別のコメント
 */
function whoWritesFunctionsLikeThis(arg) {
    // argの型はany
}

Keywords Cannot Contain Escape Sequences

前からキーワードにエスケープシーケンスを含むことはできませんでしたが、TypeScript 3.6ではエラーになります。

while (true) {
    \u0063ontinue;
//  ~~~~~~~~~~~~~
//  error! Keywords cannot contain escape characters.
}

What’s Next?

TypeScript3.6のリリースは今月末を予定しています。
RC版を試してみて、どうだったか教えてくれることを期待しています。
何か提案がある場合や問題が発生した場合は、Issueに報告してください。

Happy Hacking!

感想

元々はベータ版の記事を和訳しようとしていたのですが、ゆるゆるしてたらRCが出てきたので慌ててまとめたので適当です。

Promiseのエラーは非常に素晴らしいですね。
よく忘れたり正しくない場所にawaitって書いちゃったりするので、警告してくれると非常に助かります。
あと型推論の強化も嬉しいですね。
まあ個人的にUnion型はほとんど使わないのですが。

私はフロントエンドエンジニアではないのであまり深く使用しておらず上記以外はよくわからないのですが、おそらくバリバリ使ってるフロントエンド開発者にとってはきっと有用な変更なのだと思います。

rana_kualu
不労所得で生きたい。
https://twitter.com/rana_kualu
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
Comments
No comments
Sign up for free and join this conversation.
If you already have a Qiita account
Why do not you register as a user and use Qiita more conveniently?
You need to log in to use this function. Qiita can be used more conveniently after logging in.
You seem to be reading articles frequently this month. Qiita can be used more conveniently after logging in.
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away
ユーザーは見つかりませんでした