TypeScript 3.7 の Assertion Functions 注意点

Announcing TypeScript 3.7 Beta に “Optional Chaining” と “Nullish Coalescing” という待望の機能とともに “Assertion Functions” が登場しています。
これは従来からある User-Defined Type Guard と似ていて、こちらは条件に合わない場合に例外を投げるような、別言語での assert 構文・関数と似たものを TypeScript のフロー解析に入れる機能追加です。

Assertion Functions については TypeScript 3.7の asserts x is T 型はどのように危険なのか が全体的に詳しいです。

この投稿の本題は、それの制約に関するメモです。 Assertion Functions を使ってトリッキーなことをしようとして阻まれたわけですが、日本語の記事により救われる時間があると思ったので紹介します。

関連した Pull Request は Error when assertion function calls aren’t CFA’d です。
ここのサンプルコードが物語っていますが、 Assertion Functions の(現時点での)制約に関するものです。コードを引用します。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Test {
assert(value: unknown): asserts value {}
}

function f20(x: unknown) {
const assert1 = (value: unknown): asserts value => {}
assert1(typeof x === "string"); // Error
const assert2: (value: unknown) => asserts value = () => {};
assert2(typeof x === "string"); // Ok
const t1 = new Test();
t1.assert(typeof x === "string"); // Error
const t2: Test = new Test();
t2.assert(typeof x === "string"); // Ok
const getAssert = () => assert1;
getAssert()(typeof x === "string"); // Error
}
t.ts(7,5): error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
t.ts(11,5): error TS2775: Assertions require every name in the call target to be declared with an explicit type annotation.
t.ts(15,5): error TS2776: Assertions require the call target to be an identifier or qualified name.

ErrorOk の例が列挙されています。一言で言えば「明示的に型を指定している変数・関数でない Assertion Functions はエラーになる」ということです。
assert1assert2 は似ていますが、前者は代入の右辺に型を、後者は代入の左辺に型を書きており、後者は「明示的に」変数に型を指定しています。(このくらい許して)
t1t2 はどちらもクラスインスタンスですが、これも明示的かどうかという点で同じです。(このくらい許して)
getAssert() は、変数(や関数)でないのが原因です。サンプルを拡張してみます。

1
2
3
4
5
6
7
8
9
10
11
type Assert = (value: unknown) => asserts value;

function f(x: unknown) {
const assert1 = (value: unknown): asserts value => {}
const getAssert = () => assert1;
(<Assert>getAssert())(typeof x === "string"); // Error
const alpha = getAssert();
alpha(typeof x === "string"); // Error
const beta: Assert = getAssert();
beta(typeof x === "string"); // OK
}

<Assert> のようにキャストなどで型を明示的にしても駄目で、変数(や関数)になっていることが条件です。

例えば Jest みたいな expect(a).toBe(b) のような書き方の場合に a に対して直接 Assertion Functions を適用したくてもコンパイルエラーでできず、 const foo: Expect<A> = expect(a); foo(b); という手順を踏まなければ Assert は機能しません。(当然ながら) foo への Assert でしかないので a の型が変わるということはないので、やりたいことができず意味がありません。

Assertion Functions 自体のPull Request では this 型を自己編集する 例を始めトリッキーな用法ができるのではと示されていましたが、このような推論も起こりません。今後の開発次第ですが、 Assert 目的外のトリッキーなことは難しそうです