TypeScript v3.8 の新機能メモ

TypeScript v3.8 の新機能をざっと試しながら確認したのでメモ。

タイプのみのインポートとエクスポート

  • 「この機能は、ほとんどのユーザーが考える必要がないもの」とのこと
  • 次のように import している MyThing が値なのか型なのかぱっと見分からない...
import { MyThing } from "./some-module.js";
export { MyThing };
  • 型であった場合、生成される js からは import 処理が省略されるため、副作用のあるモジュールだと(たぶんモジュール内のトップレベルになにかしらの処理を記述してるケースを言ってるのだと思う)正常に処理されなくなるので、次の 2 行目のような import ステートメントが必要になる
//インポートの省略により、このステートメントは消去されます。
import  {  SomeTypeFoo 、 SomeOtherTypeBar  }  from  "./module-with-side-effects" ;

//このステートメントは常に使用されます。
import  "./module-with-side-effects" ;
  • v3.8 では次のように明示的に型のみを import あるいは export する構文が追加された
import type { SomeThing } from "./some-module.js";
export type { SomeThing };
  • 試したところ、tsc でコンパイルは通ったが、VS Code 1.42.1 だと構文エラー扱いになった
    • (VS Code1.43.0 のリリースで対応されたっぽい)
  • この書式でクラスを import した場合は型のみが import 対象になるので継承はできない
import type { Component } from "react";

interface ButtonProps {
    // ...
}

class Button extends Component<ButtonProps> {
    //               ~~~~~~~~~
    // error! 'Component' only refers to a type, but is being used as a value here.

    // ...
}
  • 当然以下のように new Foo() もできない
import type { Foo } from "./Foo";

// error
const foo = new Foo();

ECMAScript プライベートフィールド

stage-3 class fields proposalのプライベートフィールドが使える

class Person {
    #name: string

    constructor(name: string) {
        this.#name = name;
    }

    greet() {
        console.log(`Hello, my name is ${this.#name}!`);
    }
}

let jeremy = new Person("Jeremy Bearimy");

jeremy.#name // アクセス不可
  • TS の private 修飾子の場合、コンパイルの段階で弾いてるだけなので JS 化されればアクセスできる
class C {
    private foo = 10;
}
console.log(new C()["foo"]); // prints '10'
  • プライベートフィールドの場合は JS の仕様なのでアクセスできない(ハードプライバシーと呼ぶらしい、private 修飾子の方はソフトプライバシー)
  • すべてのプライベートフィールド名は、それを含むクラスに一意にスコープされる
  • 通常のプロパティ宣言はサブクラスで上書きされてしまう
class C {
    foo = 10;

    cHelper() {
        return this.foo;
    }
}

class D extends C {
    foo = 20;

    dHelper() {
        return this.foo;
    }
}
let instance = new D();
console.log(instance.cHelper()); // prints '20'
console.log(instance.dHelper()); // prints '20'
  • プライベートフィールドでは、各フィールド名が含まれるクラス毎に一意に保たれる
class C {
    #foo = 10;

    cHelper() {
        return this.#foo;
    }
}

class D extends C {
    #foo = 20;

    dHelper() {
        return this.#foo;
    }
}

let instance = new D();
console.log(instance.cHelper()); // prints '10'
console.log(instance.dHelper()); // prints '20'
  • TS の場合、構造的部分型を採用してるけど、プライベートフィールドを含む以下のようなケースはエラーになる
class Square {
    #sideLength: number;

    constructor(sideLength: number) {
        this.#sideLength = sideLength;
    }

    equals(other: any) {
        return this.#sideLength === other.#sideLength;
    }
}

const a = new Square(100);
const b = { sideLength: 100 };
// Boom!
// TypeError: attempted to get private field on non-instance
// This fails because 'b' is not an instance of 'Square'.
console.log(a.equals(b));

どっち使う?

  • ソフトプライバシーはコンパイル時に外部アクセスを弾いてるだけなので、どのランタイムで動くというメリットあり
  • ハードプライバシーはクラス外アクセス不可状態を厳密に保証したい場合や、サブクラスにおけるフィールド名の衝突を心配したくない場合は使うと良い(ただ、アクセス速度はこっちのほうが遅いらしい)

export * as ns 構文

  • 別モジュールのすべてのメンバーをまるっとエクスポートするのを 1 行で書けるようになった。従来以下のように記述してたところを・・・
import * as utilities from "./utilities.js";
export { utilities };
  • 以下のように書けるようになった
export * as utilities from "./utilities.js";

トップレベル await

  • 従来は async/await 使うために、async 付き関数を書く必要があった
async function main() {
    const response = await fetch("...");
    const greeting = await response.text();
    console.log(greeting);
}
main()
    .catch(e => console.error(e))
  • 「トップレベル await」という ECMAScript の機能が使えるようになった
  • モジュール内のトップレベルならいきなり await しちゃっても OK
const response = await fetch("...");
const greeting = await response.text();
console.log(greeting);

// exportが無いとモジュールとみなされないのでこれ必要...
export {};
  • 試したところやっぱり VS Code 上には警告がまだ出る
    • (VS Code 1.43.0 で対応したっぽい)
  • あと、コンパイルオプションに以下条件があるので"target": "ES2017", "module": "ESNext"とかにしとく必要あり

Top-level 'await' expressions are only allowed when the 'module' option is set to 'esnext' or 'system', and the 'target' option is set to 'es2017' or higher.

JSDoc プロパティ修飾子

  • JavaScript のコードに JSDoc で型付けできるやつ(あまり興味ない...
  • @public、@private、@protected、@readonly を指定できる
// @ts-check

class Foo {
    constructor() {
        /** @private */
        this.stuff = 100;
    }

    printStuff() {
        console.log(this.stuff);
    }
}

new Foo().stuff;
//        ~~~~~
// error! Property 'stuff' is private and only accessible within class 'Foo'.

Better Directory Watching on Linux and watchOptions

  • TS コンパイラが担う依存関係を検出するためのディレクトリ監視処理に、watchOptions というオプションを指定できるようになった(監視にネイティブのファイルシステムイベントを使ったりとか指定できるっぽい)
{
    // Some typical compiler options
    "compilerOptions": {
        "target": "es2020",
        "moduleResolution": "node",
        // ...
    },

    // NEW: Options for file/directory watching
    "watchOptions": {
        // Use native file system events for files and directories
        "watchFile": "useFsEvents",
        "watchDirectory": "useFsEvents",

        // Poll files for updates more frequently
        // when they're updated a lot.
        "fallbackPolling": "dynamicPriority"
    }
}

“Fast and Loose” Incremental Checking

  • assumeChangesOnlyAffectDirectDependencies オプションを有効にすると、コンパイル時に、変更されたファイルとそれらを直接インポートするファイルのみが再チェック/再構築され、高速化が望めるようになる。

Editor Features Convert to Template String

  • VS Code だと以下の記述が・・・
"I have " + numApples + " apples"
  • 以下のように変換される
`I have ${numApples} apples`

呼び出し階層

  • TS 使う大きなメリットである VS Code でいうところの「Find All References」を視覚的に見やすくした機能
  • 例えば以下処理は...
function frequentlyCalledFunction() {
    // do something useful
}

function callerA() {
    frequentlyCalledFunction();
}

function callerB() {
    callerA();
}

function callerC() {
    frequentlyCalledFunction();
}

function entryPoint() {
    callerA();
    callerB();
    callerC();
}
  • 以下のような参照関係にある
frequentlyCalledFunction
 │
 ├─callerA
 │  ├─ callerB
 │  │   └─ entryPoint
 │  │
 │  └─ entryPoint
 │
 └─ callerC
     └─ entryPoint
  • これをエディタ上で次のように表現してくれる

VS CodeのCall Hierarchyによる表示]

  • 対象の関数名なりを選択して、右クリック -> 「Show Call Hierarchy」で表示することができた