TypeScriptのコードレビューを依頼された人のための!と?の解説

TypeScriptのコードレビューをしていて、 !? の意味を改めて確認したら意外とややこしかったのでまとめておきます。

ご注意: 本記事では、nullとundefinedを厳密に区別せず、どちらも含めてnon-null/nullableと表現しています。

Optional Paramater

Optional Paramater

関数の引数がOptional(省略可能)であることを宣言します。コンパイラはOptional Paramaterが宣言されたとき、その引数の型を T | undefined と認識します。

勘違いしやすいのが、Nullableを宣言するものではないということです。OptionalとNullableの違いは、Optionalの場合は複数の引数があるときにOptionalの引数の後ろにOptionalでない引数を宣言できないという点です。

function correct(param1: string, param2?: string) { ... } // OK
function incorrect(param1?: string, param2: string) { ... } // NG

Non-null assertion operator

Non-null assertion operator

TypeScript2.0から導入されました。

Optional Paramaterがnon-nullであるということをコンパイラに明示します。

リリースノートのサンプルコードを引用します。この例で、processEntity関数の引数e?はOptionalなので、e.nameとするとコンパイラに「eはundefinedの可能性があると」指摘されます。しかし、この例ではvalidateEntity関数でeがnon-nullであることを保証しているので、そういう場合はe!.nameとnon-nullアサーションを付けることで、コンパイラにeはnon-nullであると明示することができます。

// Compiled with --strictNullChecks
function validateEntity(e?: Entity) {
    // Throw exception if e is null or invalid entity
}

function processEntity(e?: Entity) {
    validateEntity(e);
    let s = e!.name;  // Assert that e is non-null and access name
}

ちなみに、以下のようにeがnon-nullであることをコードで示すことができていれば、non-nullアサーションを付けなくてもコンパイラがnon-nullであることを認識してくれます。

function processEntity(e?: Entity) {
    if (e) {
        let s = e.name;
    }
}

Strict Class Initialization

Strict Class Initialization

TypeScript2.7から導入されました。

Strict Class Initialization を有効にすると、クラスのプロパティ変数がconstructor関数で初期化されていない場合にエラーとなります。しかし実際にはconstructor関数以外の場所で初期化されるケースもあります。そんなときにプロパティ変数 ! を付けると、エラーが出ないようになります。つまり、コンパイラに対してこの変数はnon-nullであると宣言しているということです。

リリースノートのサンプルコードを引用します。プロパティ変数foo!: numberはconstructor関数では初期化されていませんが、initialize()で初期化されていることがわかっているのでfooはnon-nullであると宣言します。

class C {
    foo!: number;
    // ^
    // Notice this '!' modifier.
    // This is the "definite assignment assertion"

    constructor() {
        this.initialize();
    }

    initialize() {
        this.foo = 0;
    }
}

プロパティ変数をfoo?: numberfoo: number | undefinedとすることもできますが、その場合は参照時にnon-nullアサーションを付けてfoo!としないとnullableであると判断されるので、宣言時にnon-nullを宣言できたほうが良いでしょう。

(´-`).。oO( ところでClassのプロパティ変数に?を付けられるというのが公式ドキュメントを探しても見つからない。Interfaceには書いてあるのだけど。

Definite Assignment Assertions

Definite Assignment Assertions

同じく、TypeScript2.7から導入されました。

Strict Class Initializationと同じように、関数内でletで変数が宣言されたとき、それが関数のスコープ内で代入されずに参照されようとしたときコンパイラはエラーとみなします。このとき、letの変数にDefinite Assignment Assertionsの!を付けることで、コンパイラにnon-nullであることを明示します。

リリースノートのサンプルコードを引用します。let x!: number!がDefinite Assignment Assertionsです。これがついていない場合は、コンパイラは「xはまだ代入されていない」としてエラーとします。

// Notice the '!'
let x!: number;
initialize();

// No error!
console.log(x + x);

function initialize() {
    x = 10;
}

なお、Definite Assignment Assertionsを付ける代わりに、参照時にNon-null assertion operatorを付けることでも回避は可能です。

let x: number;
initialize();

// No error!
console.log(x! + x!);

function initialize() {
    x = 10;
}

まとめ

基本的には?で宣言された場合はnullやundefinedの可能性がある、!がついている時はnullやundefinedの可能性はないと認識しておけば良さそうです。

個人的に紛らわしいと思うのが、?で宣言された変数がifなどでnullやundefinedでないと文脈で判断できるときに、!(Non-null assertion operator)を付けなくてもよいという点です。キャストされてないのに文脈で型が変わるというのが私には気持ち悪いと思いました。このへんはチームでポリシーを決めておくと良いかと思います。