Edited at

TypeScript Handbook の Advanced Types をちょっとだけ掘り下げる - その2 Nullable types

More than 1 year has passed since last update.


はじめに

本記事は TypeScript HandbookAdvanced Types に書かれているものをベースに、説明されている内容をこういう場合はどうなるのかといったことを付け加えて ちょっとだけ 掘り下げます。完全な翻訳ではなく、若干元の事例を改変しています。

今回は Nullable types について掘り下げます。

その1 Type Guards and Differentiating Types は こちら

その3 Type Aliases は こちら

その4 String Literal Types / Numeric Literal Types は こちら

その5 Discriminated Unions は こちら

その6 Index types は こちら

その7 Mapped types は こちら


Nullable types

デフォルト の TypeScript では nullundefined は何に対しても代入可能になっています。

let s = 'foo';

s = null; // エラーなし
s = undefined; // エラーなし

function f(s: string) {
// ...
}

f(null); // エラーなし
f(undefined); // エラーなし

これは TypeScript が <Type> と宣言もしくは推測されたものを自動で <Type> | null | undefined として扱うようにしているためです。

宣言もしくは推測された型のみに限定し、 null および undefined を許容しないようにするには コンパイラオプション--strictNullChecks を有効にします。

let s = 'foo';

s = null; // エラー
s = undefined; // エラー

function f(s: string) {
// ...
}

f(null); // エラー
f(undefined); // エラー

このオプションが有効な状態で null や undefined を許容したい場合には、許容したい内容を宣言する必要があります。

let s: string | null | undefined = 'foo';

s = null; // エラーなし
s = undefined; // エラーなし

function f(s: string | null | undefined) {
// ...
}

f(null); // エラーなし
f(undefined); // エラーなし

null と undefined は別物なので、 string | nullstring | undefined も別物になります。

let s: string | null = 'foo';

s = null; // エラーなし
s = undefined; // コンパイルエラー

function f(s: string | undefined) {
// ...
}

f(null); // コンパイルエラー
f(undefined); // エラーなし


Optional parameters and properties

オプショナルなパラメータやプロパティについての扱いは、デフォルトの場合、以下のようになります。

function f(x: string, y?: string) {

return `${x}:${y}`;
}
f('1', '2'); // '1:2' が戻る。
f('1', null); // y は 'string | null | undefined' なので問題なし。 '1:null' が戻る。
f('1', undefined); // y は 'string | null | undefined' なので問題なし。 '1:undefined' が戻る。
f('1'); // 引数の値なしで呼び出された場合、 y は undefined として扱われ '1:undefined` が戻る。

interface I {
a?: number;
}
let i: I = {}; // この時の i.a は undefined
i.a = 12; // 'number | null | undefined' なので問題なし。
i.a = null; // 'number | null | undefined' なので問題なし。
i.a = undefined; // 'number | null | undefined' なので問題なし。

--strictNullCheck を有効にすると、オプショナルなパラメータの型には | undefined が自動で足され以下のようになります。

function f(x: string, y?: string) {

return `${x}:${y}`;
}
f('1', '2'); // '1:2' が戻る。
f('1', null); // コンパイルエラー。 y は 'string | undefined'
f('1', undefined); // y は 'string | null | undefined' なので問題なし。 '1:undefined' が戻る。
f('1'); // 引数の値なしで呼び出された場合、 y は undefined として扱われ '1:undefined` が戻る。

interface I {
a?: number;
}
let i: I = {}; // この時の i.a は undefined
i.a = 12; // 'number | null | undefined' なので問題なし。
i.a = null; // コンパイルエラー。 i.a は 'number | undefined'
i.a = undefined; // 'number | null | undefined' なので問題なし。


Type guards and type assertions

null もしくは undefined を許容している場合に null もしくは undefined を除外するには JavaScript と同様に以下のように書きます。

function f(sn: string | null | undefined): string {

if (sn === null || sn === undefined) {
return 'default';
} else {
return sn;
}
}

以下のように書けばもっと簡潔に書けます。

function f(sn: string | null | undefined): string {

return sn || 'default';
}

--strictNullCheck が有効な場合、 null もしくは undefined の可能性を除外できない状態の変数のプロパティにアクセスしようとするとエラーが発生します。

function broken(name: string | null | undefined): string {

function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // コンパイラが name が null もしくは undefined になる可能性があるというエラーを出す。
}
name = name || 'Bob';
return postfix('great');
}

この例では、 name = name || 'Bob'; という一行があるので、 name の値が null もしくは undefined になることは現実的にはありませんが、エラーになります。(この理由は後述。)

こういう場合には、コンパイラにこの変数が null もしくは undefined になることはないということを教えてやる必要があります。やり方は変数の後に ! をつけてやることです。

function fixed(name: string | null): string {

function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // ok
}
name = name || 'Bob';
return postfix('great');
}

さて、前述の例でエラーが出る理由は、コンパイラがネストした関数のすべての呼び出しを追跡できないために null もしくは undefined の可能性を除外できないからです。

function broken(name: string | null | undefined): string {

function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet;
}
name = name || 'Bob';
name = undefined;
return postfix('great');
}

順番を変えたところでエラーが出ることに変わりません。

function broken(name: string | null | undefined): string {

name = name || 'Bob';
function postfix(epithet: string) {
return name.charAt(0) + '. the ' + epithet; // 相変わらずエラー
}
return postfix('great');
}

ただし、即時実行関数の場合はネストされていても判断できます。

function iife(name: string | null | undefined) {

name = name || "Bob";
(function () {
return name.charAt(0) + '. the great'; // エラーなし
})();
name = undefined;
(function () {
return name.charAt(0) + '. the great'; // エラー
})();
}

なお、 ! による null もしくは undefined の除外はコンパイラによるチェック自体をさせなくするようなものなので、実行時エラーの可能性が高まります。

function broken(name: string | null | undefined): string {

function postfix(epithet: string) {
return name!.charAt(0) + '. the ' + epithet; // コンパイルエラーは出なくなったが、実行時エラー
}
name = name || 'Bob';
name = undefined;
return postfix('great');
}


まとめ

--strictNullCheck を使うことで、ガード処理を書かずに予期しない null や undefined な変数のプロパティへのアクセスを防ぐことができる。

コンパイラが null もしくは undefined の可能性があると言っている場合でも、 ! を変数の後ろに付けることでエラーを回避することが可能。ただし、実行時例外の可能性が高まるので慎重に使う必要あり。