🐶

TypeScript 4.3 Beta 変更点

2021/04/05に公開

TypeScript 4.3 Betaが出たので、公式ブログを読んで気になった点からまとめていきたいと思います。

Separate Write Types on Properties

getterとsetterで異なる型を指定できるようになりました。

v4.3 Beta 以前

例えばsetterで、文字列を受け取って数字に変換するような処理を書きたいとき。

v4.3_beta以前
class Thing {
    #size = 0;

    get size(): number {
        return this.#size;
    }
    // Error! 'get' and 'set' accessor must have the same type.
    set size(value: string | number) {
        this.#size = typeof value === 'string' ? parseInt(value) : value;
    }
}

setterの引数の型に、getterと違う型を指定すると「getterと同じ型にして!」と怒られてしまいます。

上記のコードに、無理やり"42"を入れようとしてもやはり怒られます。

v4.3_beta以前
let thing = new Thing();

thing.size = 42; // OK
thing.size = "42"; // Error! Type 'string' is not assignable to type 'number'.

v4.3 Beta 以降

v4.3 Beta では、getterは常にnumberであることを保証した上で、setterには個別の型を設定できるようになりました。
なので先程怒られていたコードも、問題なく通ります。

v4.3_beta以降
class Thing {
    #size = 0;

    get size(): number {
        return this.#size;
    }
    // OK! \(^o^)/
    set size(value: string | number) {
        this.#size = typeof value === 'string' ? parseInt(value) : value;
    }
}

let thing = new Thing();

thing.size = 42; 
thing.size = "42"; // OK! \(^o^)/

注意点として、getterとsetterで異なる型を使うときは、getterの型は常にsetterの型に割り当て可能である必要があります。

override and the --noImplicitOverride Flag

override 修飾子と--noImplicitOverride フラグが新しく追加されました。

v4.3 Beta 以前

親クラスと同名のメソッドを子クラスに定義すると、子クラスでメソッドを上書きすることができます。これをオーバーライドと呼びます。

v4.3 Beta 以前では、オーバーライドによるミスが起こりがちでした。

以下その具体例です。SpecializedComponentという子クラスが親クラスのshowhideメソッドをオーバーライドしています。

v4.3_beta以前
class SomeComponent {
    show() {
        // ...
    }
    hide() {
        // ...
    }
}

class SpecializedComponent extends SomeComponent {
    show() {
        // ...
    }
    hide() {
        // ...
    }
}

おっと!だれかが親クラスのshowhideメソッドを消して、代わりにsetVisibleメソッドに置き換えたようです。

しかし、子クラスの方は何も更新されていません...

v4.3_beta以前
 class SomeComponent {
-    show() {
-        // ...
-    }
-    hide() {
-        // ...
-    }
+    setVisible(value: boolean) {
+        // ...
+    }
 }
 class SpecializedComponent extends SomeComponent {
     show() {
         // ...
     }
     hide() {
         // ...
     }
 }

親クラスのshowhideメソッドが消されたので、子クラスのshowhideは最早オーバーライドではなくなりました。
オーバーライドではなく、こんどは子クラス独自のメソッドという扱いになります。

エラーにはなるわけではないので、見落としてしまうということはありそうです。

子クラスに残ってしまったメソッドは、元々これがオーバーライドによって定義されたものかもよく分からなくなり、もう呼ばれることもないため、無駄なメソッドになってしまいます。

v4.3 Beta 以降

v4.3 Beta では override 修飾子の追加で、これを回避できるようになります。

この修飾子が付いている場合、TypeScriptは親クラスに同名のメソッドが存在するか確認してくれます。なのでもし親クラスの方を誤って消してしまっても、以下のようにエラーで怒ってくれるようになります。

v4.3_beta以降
class SomeComponent {
    setVisible(value: boolean) {
        // ...
    }
}
class SpecializedComponent extends SomeComponent {
    // 親クラスに同名のメソッドが無いから、override修飾子付けちゃだめだよ!
    // Error: This member cannot have an 'override' modifier because it is not declared in the base class 'SomeComponent'.
    override show() {
        // ...
    }
}

これはめちゃ便利な気がしますが、そもそも override 修飾子をつけるのを忘れてしまったら意味ないです。

なので、v4.3 Betaでは同時に --noImplicitOverride というフラグが用意されました。
このフラグがtrueのとき、override 修飾子を付け忘れてても以下のようにエラーで怒ってくれるようになります。

--noImplicitOverrideがtrueの場合
class SomeComponent {
  show() {
    // ...
  }
}
export class SpecializedComponent extends SomeComponent {
  // 親クラスに同名のメソッドが存在するから、override修飾子付けてね!
  // Error: This member must have an 'override' modifier because it overrides a member in the base class 'SomeComponent'.
  show() {
    // ...
  }
}

このフラグを使えば、親クラスに同名のメソッドがすでに存在してて、意図せずオーバーライドしてしまうというケースも回避できそうです。

Always-Truthy Promise Checks

strictNullChecks を有効にしている場合、条件式にPromiseオブジェクトを入れると、エラーになるようになりました。

v4.3 Beta 以前

Promiseオブジェクトは truthy として評価されるらしいです。

といっても、そもそも truthy が何か自分はよく分かってなかったので調べてみます。以下mdnからの引用です。

JavaScript において、 truthy は Boolean コンテキストに現れた時に true とみなされる値のことです。 falsy として定義された値 (つまり、false, 0, -0, 0n, "", null, undefined, NaN) を除くすべての値は truthy です。

Promiseオブジェクトは、false, 0, -0, 0n, "", null, undefined, NaNのいずれにも当てはまらないので、たしかに truthy であると言えます。

その上で、以下のコードを見てみます。

v4.3_beta以前
async function foo(): Promise<boolean> {
    return false;
}

async function bar(): Promise<string> {
    if (foo()) {
        return "true";
    }
    return "false";
}

if文の条件式である foo() の戻り値はPromiseオブジェクトで、truthyとして評価されます。そのため、barの戻り値は常に"true"となります。

しかし、おそらくこのコードを書いた人は await を書き忘れているだけなので、実際書き手が期待している値は"false"だと思われます。

v4.3 Beta 以前のバージョンでは、Playground で見ても何もエラーは出てないので、見落としがちかもです。

v4.3 Beta 以降

v4.3 Betaでは、ちゃんと怒ってくれるようになりました。これで await 忘れのミスがを防げそうです!

v4.3_beta以降
async function foo(): Promise<boolean> {
    return false;
}

async function bar(): Promise<string> {
    if (foo()) {
    // Error! 
    // This condition will always return true since this 'Promise<boolean>' appears to always be defined.
    // Did you forget to use 'await'?
        return "true";
    }
    return "false";
}

Discussion