Menu

TypeScriptのエクスクラメーション(!): 非nullアサーション

user! のように値のあとに付けるエクスクラメーションマークは非 null アサーション演算子で、実行時のチェックなしに型から null と undefined を取り除きます。x! が何をするか、確実な代入アサーション let x!: T と prop!: T、それらが危険な理由、より安全な代替手段を解説します。

このページのコードはエディタで実行できます - 編集してすぐに結果を確認できます。

式のあとに付けるエクスクラメーションマーク value! は、非 null アサーション演算子です。型から null と undefined を取り除くので、number | undefined を number として使えます。これはコンパイラーへの約束であって、チェックではありません。実行時には何も起きません。

! がなければ、tea.toFixed(2) はエラー TS18048 'tea' is possibly 'undefined' です。! があれば、キーが存在するとコンパイラーに伝えたので、コードはコンパイルできます。

x! が実行時にすること: 何もしない

! は出力から消えます。prices.get("coffee")! のコンパイル後のJavaScriptは、ただの prices.get("coffee") です。アサーションが間違っていれば、欠けている値が最初に使われた場所で、あとからエラーが表れます:

プログラムは Cannot read properties of undefined (reading 'toFixed') を出力します。クラッシュするのは ! の次の行で、実際のコードではもっと離れた場所になることもあります。undefined がオブジェクトに保存され、別のファイルで爆発するかもしれません。その距離こそが ! の危険な理由です。

確実な代入: let x!: T

同じ記号は宣言の中でも、関連する意味を持ちます。TypeScriptは変数が読まれる前に代入されるかを追跡しますが、別の関数の中で行われる代入は追えません:

index.ts(9,13): error TS2454: Variable 'config' is used before being assigned.

let config!: { port: number }; は確実な代入アサーションで、「読まれる前に必ず代入される」という意味です。これでエラーは直りますが、x! と同じ落とし穴があります。init() がスキップされることがあれば、読んだ値は undefined になります。たいていは構造を変えるほうがよく、たとえば init がオブジェクトを返すようにして const config = init(); と書きます。

クラスのプロパティ: prop!: T

strictPropertyInitialization(strict に含まれます)のもとでは、クラスのすべてのプロパティは宣言時かコンストラクターの中で初期化されなければなりません。そうでなければ TS2564: Property 'socket' has no initializer and is not definitely assigned in the constructor. になります。プロパティがあとでメソッドやフレームワークによって設定される場合、prop!: T と書けばコンパイラーは受け入れます:

最初の console.log が隙間を示しています。型では socket は常に存在しますが、open() の前は undefined です。その時点で c.socket.send を呼ぶと、コンパイルはできますが例外が投げられます。プロパティが本当に欠けることがあるなら、socket?: ... と宣言してチェックするか、コンストラクターでオブジェクトを作りましょう。prop! が標準的な書き方になっているのは主に、インスタンスの作成後にフレームワークがプロパティを埋める場合です。Angular の @ViewChild(...) child!: ChildDirective(ngAfterViewInit が実行される前に設定される)や、ライブラリが行を読み込むときにカラムを埋める ORM のエンティティクラス(MikroORM のドキュメントでは @Property() title!: string と書いています)などです。

より安全な代替手段

! のほとんどは、コンパイラーがチェックできるものや、適切な場所ではっきり失敗するチェックに置き換えられます:

どちらのヘルパーも ! と同じように型を絞り込みますが、仮定が間違っていれば、別の場所での TypeError ではなく、その場で missing HOST が出ます。assertDefined はアサーション関数(asserts value is ...)で、呼び出しのあと、コンパイラーは host を string として扱います。ほかのパターンは 型ガード にあります。

代わりに書き方値が欠けていたときに起きること
user!.nameif (user) { user.name }ブロックが飛ばされる
user!.nameuser?.nameundefined
count!count ?? 0デフォルト値が使われる
map.get(k)!must(map.get(k), "k")その行でわかりやすいエラー
let x!: Tconst x = compute()失敗する余地がない

ほかのエクスクラメーションマーク

! は置かれた場所によって意味が変わります:

コード意味
value!(式のあと)非 null アサーション。TypeScriptだけ
let x!: T、prop!: T確実な代入アサーション。TypeScriptだけ
!value(式の前)論理否定。普通のJavaScript
!!valueboolean に変換。普通のJavaScript
a !== b、a != b不等価。普通のJavaScript

コンパイル時に消えるのは最初の2つだけです。!value と !!value は実行時に動いて boolean を返します。

よくある質問

TypeScriptで変数のあとのエクスクラメーションマークはどういう意味ですか?

value! は非 null アサーション演算子です。value が null でも undefined でもないとコンパイラーに伝えるので、型からその2つのメンバーがなくなります。string | undefined は string になります。出力されるJavaScriptからは取り除かれ、実行時のチェックも追加されないので、間違っていればあとでプログラムが TypeError で失敗します。

TypeScriptの ! と ? の違いは何ですか?

x! はチェックなしに値が存在すると主張し、null でない型を与えます。x?.y は実行時にチェックし、x が null か undefined ならそこで止まって undefined を返します。宣言の中では、name?: string はプロパティを省略可能にし、name!: string は必須のプロパティがコンパイラーから見えない場所で代入されることを表します。

let x!: string はどういう意味ですか?

確実な代入アサーションです。コンパイラーが証明できなくても(たとえば代入が別の関数の中で行われる場合)、変数は読まれる前に代入されるとコンパイラーに伝えます。これがないと、変数を読むとエラー TS2454 Variable 'x' is used before being assigned になります。

「has no initializer and is not definitely assigned in the constructor」を直すには?

strictPropertyInitialization によるエラー TS2564 です。プロパティに初期値を付ける、コンストラクターで代入する、省略可能にする(prop?: T)、フレームワークや初期化メソッドが本当に使う前に設定するなら prop!: T と書く、のいずれかです。その約束は何もチェックされないので、! は最後の手段です。

非 null アサーション演算子は悪い書き方ですか?

間違いではありませんが、! はどれもチェックされない主張です。@typescript-eslint/no-non-null-assertion のような lint の設定はこれを指摘します。絞り込むチェック(if (x)、x ?? fallback、x?.y)や、わかりやすいエラーを投げるヘルパーを選びましょう。! は、コンパイラーが追えないロジックで値が保証されている場所のためにとっておきます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める