TypeScriptの「?」と「!」とは?使い方や注意点をわかりやすく解説!

TypeScriptのコードを見ていると、?!といった記号をよく見かけます。

でも、「この記号って何をしてるの?」と思ったことはありませんか?

この記事では、TypeScriptでよく使われる以下の記号について、それぞれの使い方や注意点をサンプルコードを用いてわかりやすく解説します。

  • ?(オプションパラメーター・オプショナルチェーン)
  • ??(Null合体演算子)
  • ??=(Null合体代入演算子)
  • !(非nullアサーション演算子・明確な割り当てアサーション)

「?」に関する機能と使い方

?には、以下のような機能があります。それぞれの用途や使い方について、順番にわかりやすく解説していきます。

  • オプションパラメーター(?
    • 関数の引数を省略可能にし、指定されなかった場合はundefinedとして扱う記号
  • オプショナルチェーン(?.)
    • オブジェクトのプロパティにアクセスする際、途中でnullundefinedがあっても安全にアクセスできる記号
  • Null合体演算子(??
    • 左辺の値がnullまたはundefinedのときに、右辺のデフォルト値を返す演算子
  • Null合体代入演算子(??=
    • 変数の値がnullまたはundefinedのときだけ、右辺の値を代入する演算子

オプションパラメーター(?)

オプションパラメーター(?)は、関数の引数を省略可能にするための記号です。

通常、TypeScriptでは関数に定義されたすべての引数を渡す必要があります。しかし、引数にオプションパラメーター(?)を付けることで、その引数が省略可能になります。省略した場合は、undefinedとして扱われます。

オプションパラメーター(?)を使用しない場合使用した場合の動作をサンプルコードで確認してみましょう。

オプションパラメーター(?)を使用しない場合

function greet(name: string, age: number) {
  console.log(`${name} is ${age} years old.`);
}

greet('Taro'); // エラー:Expected 2 arguments, but got 1.

オプションパラメーター(?)を使用しない場合、引数ageを省略してgreet関数を呼び出すと、「引数が足りません」というエラーが発生します。

オプションパラメーター(?)を使用した場合

function greet(name: string, age?: number) {
  console.log(`${name} is ${age} years old.`);
}

greet('Taro'); // Taro is undefined years old.
greet('Hanako', 25); // Hanako is 25 years old.

引数ageにオプションパラメーター(?)をつけることで、引数ageを省略してgreet関数を呼び出しても、エラーにならなくなります。引数を省略した場合は、undefinedとなっていることも確認できます。

オプションパラメーター(?)の注意点

オプションパラメーター(?)を使うと、その引数の型は「型 | undefined」のようなユニオン型になります。

function greet(name: string, age?: number) {
  // ageは number | undefined になる
  console.log(`${name} is ${age} years old.`);
}

また、オプションパラメーター(?)は、必ず引数の「最後」に書く必要があります。

function greet(age?: number, name: string) {
  console.log(`${name} is ${age} years old.`);
}
// エラー:A required parameter cannot follow an optional parameter.

必須パラメーターの前にオプションパラメーター(?)があると、TypeScriptはエラーを出します。

オプショナルチェーン(?.)

オプショナルチェーン(?.)は、オブジェクトのプロパティアクセス時にnullundefinedの可能性がある場合でも、安全にアクセスできる記号です。

通常、TypeScriptでは、オブジェクトの中にある深いプロパティを参照するときに、どこかの階層がnullundefinedだとエラーになります。しかし、オプショナルチェーン(?.)を使うと、途中のプロパティがnullundefinedのときには、エラーを出さずにundefinedを返してくれます

オプショナルチェーン(?.)を使用しない場合使用した場合の動作をサンプルコードで確認してみましょう。

オプショナルチェーン(?.)を使用しない場合

type User = {
  name: string;
  contact?: {
    email: string;
  };
};

const user: User = {
  name: 'Yamada',
};

const email = user.contact.email; // エラー:'user.contact' is possibly 'undefined'.
console.log(email);

contactが存在しない可能性があるにもかかわらず、user.contact.emailと直接アクセスしようとすると、TypeScriptはcontactundefinedかもしれないのに、その中のemailを読もうとしている」と判断し、エラーを出します。

オプショナルチェーン(?.)を使用した場合

type User = {
  name: string;
  contact?: {
    email: string;
  };
};

const user: User = {
  name: 'Yamada',
};

const email = user.contact?.email; // OK:undefined(エラーなし)
console.log(email); // 出力: undefined

contactが存在しない場合でも、user.contact?.emailのように?.を使えば、エラーにはなりません。この書き方では、contactがなければundefinedが返ってくるだけなので、安全にアクセスできます。このように、オプショナルチェーン(?.)を使うと、安心してプロパティを参照することができます。

注意点

オプショナルチェーン(?.)は、短絡評価(ショートサーキット)されます。つまり、左側(?.より前)がnullまたはundefinedの場合、右側は評価されません。

const user = undefined;
const email = user?.contact.email;

この例では、userundefinedなので、右側(contact.email)は評価されません。そのため、エラーにはならず、結果としてemailundefinedになります。

あわせて読みたい

オプショナルチェーン(?.)については下記の記事で詳しく説明しています。興味のある方は下記のリンクからぜひチェックをしてみてください。

Null合体演算子(??)

Null合体演算子(??)は、左側がnullまたはundefinedのときに、右側のデフォルト値を返す演算子です。変数がnullundefinedのときに「デフォルト値」を使いたい場合に便利です。

Null合体演算子(??)を使用した場合の動作をサンプルコードで確認してみましょう。

// 例1:inputName が null の場合
const inputName1 = null;
const name1 = inputName1 ?? 'Guest';
console.log(name1); // 出力: 'Guest'

// 例2:inputName に 'Yamada' が入っている場合
const inputName2 = 'Yamada';
const name2 = inputName2 ?? 'Guest';
console.log(name2); // 出力: 'Yamada'

inputName1nullなので、??の右側"Guest"が使われます。一方、inputName2"Yamada"が代入されているので、"Yamada"がそのまま使われます。

注意点

Null合体演算子(??)は、OR演算子(||)と似ていますが、対象としているものが違います。

  • ??nullundefinedのみを対象にします
  • ||はfalsy(falseっぽい値)全体を対象にします

具体的な違いをサンプルコードで見てみましょう。

// OR演算子(||)の場合
const age = 0 || 18;
console.log(age); // 出力: 18 (0 は falsy のため、右側が使われる)

上の例では、0はfalsyなので、??の右側の18を返します。

// Null合体演算子(??)の場合
const age = 0 ?? 18;
console.log(age); // 出力: 0 (null / undefined ではないため、左側が使われる)

上の例では、0はfalsyですが、nullundefined ではないので、??の左側の0を返します。

Null合体代入演算子(??=)

Null合体代入演算子(??=)は、左辺の変数がnullまたはundefinedのときだけ、右辺の値を代入する演算子です。

Null合体代入演算子(??=)を使用した場合のサンプルコードで動作を確認してみましょう。

// 左辺の変数が null の場合
const user = {
  name: null,
};

user.name ??= 'Guest';

console.log(user.name); // 出力: 'Guest'

この例では、user.namenullなので、右辺の"Guest"が代入されます。

// 左辺の変数が null でも undefined でもない場合
const user = {
  name: 'Alice',
};

user.name ??= 'Guest';

console.log(user.name); // 出力: 'Alice'

user.name"Alice" が入っているため、右辺の"Guest"は代入されません。

注意点

Null合体代入演算子(??=)は左辺の変数がnullまたはundefinedのときだけ、右辺の値を代入するため、他のfalsy値(0, '', falseなど)では代入されません。

const user = { age: 0 };

user.age ??= 14;
console.log(user.age); // 出力: 0

上の例では、ageの値は0でfalsyですが、nullundefinedではないので、14は代入されません。

「Null合体代入演算子(??=)」と「Null合体演算子(??)」の違い

Null合体代入演算子(??=)は、Null合体演算子(??)を使った代入とほぼ同じ意味になります。以下のコードを見てみましょう。

const user = {
  name: null,
};

// Null合体代入演算子(??=)を使った書き方
user.name ??= 'Guest';

// Null合体演算子(??)を使った書き方(上記と同じ意味)
user.name = user.name ?? 'Guest';

どちらも、「user.namenullまたはundefinedのときだけ、"Guest"を代入する」という意味になります。

「!」に関する機能と使い方

!には、以下のような機能があります。それぞれの用途や使い方について、順番にわかりやすく解説していきます。

  • 非nullアサーション演算子(!
    • 値がnullやundefinedではないことを開発者が主張する記号
  • 明確な割り当てアサーション(!
    • 変数が後で必ず初期化されることをコンパイラに伝える記号

非nullアサーション演算子(!)

非nullアサーション演算子(!)は、「この値は絶対にnullでもundefinedでもない」と開発者がコンパイラにアサート(主張)するための記号です。nullundefinedを含む型(たとえばT | nullT | null | undefined)に対して、非nullアサーション演算子(!)を使うと、型はTであることをコンパイラに明示できます。

TypeScriptでは、変数や引数がnullundefinedを含む可能性がある場合、自動的に型安全のためのエラーを出してくれます。ですが、「このタイミングでは絶対にnullundefinedではない」とわかっているときに非nullアサーション演算子(!)を使うことで、エラーを回避できます。

非nullアサーション演算子(!)を使用しない場合使用した場合のサンプルコードで動作を確認してみましょう。

非nullアサーション演算子(!)を使用しない場合

type User = {
  username: string;
};

function showUsername(user?: User) {
  console.log(user.username); // エラー:'user' is possibly 'undefined'.
}

showUsername({ username: 'Taro' });

この例では、関数の引数userが省略される可能性がある(User | undefined)ので、TypeScriptは「userundefinedかもしれない」とエラーを出します。

非nullアサーション演算子(!)を使用した場合

type User = {
  username: string;
};

function showUsername(user?: User) {
  console.log(user!.username); // OK:「絶対に user はある」と明示している
}

showUsername({ username: 'Taro' }); // 出力: Taro

この例のようにuser!と書くことで、「この変数は絶対にnullundefinedではない」と主張できます。その結果、TypeScriptはエラーを出さずに通してくれます。

注意点

非nullアサーション演算子(!)の使用には注意が必要です。

  • 実行時にnullundefinedが入っていると、クラッシュの原因になります。
  • コンパイラの型チェックを強制的に無効にしてしまうため、型の変化に気づけなくなる可能性があります。
const text: string | null = null;

console.log(text!.length); // 実行時エラー:Cannot read properties of null (reading 'length')

この例ではtextに実際にはnullが入っているので、.lengthを読み込もうとしたときにエラーが発生してクラッシュします。

あわせて読みたい

非nullアサーション演算子(!)については下記の記事で詳しく説明しています。興味のある方は下記のリンクからぜひチェックをしてみてください。

明確な割り当てアサーション(!)

明確な割り当てアサーション(!)は、「この変数は使う前に必ず初期化される」とコンパイラに伝えるための記号です。

TypeScriptでは、strictPropertyInitializationが有効な場合(デフォルトでON)、変数やプロパティが確実に初期化されていないとエラーになります。ですが、どうしてもコンパイラに検出できないタイミングで代入される場合、開発者が明確な割り当てアサーション演算子(!)をつけることで、「この変数は使う前に必ず初期化される」と明示できます。

明確な割り当てアサーション演算子(!)を使用しない場合使用した場合のサンプルコードで動作を確認してみましょう。

明確な割り当てアサーション(!)を使用しない場合

let title: string;

function setTitle(value: string) {
  title = value;
}

setTitle('Hello TypeScript');

console.log(title); // エラー:Variable 'title' is used before being assigned.

この例では、titleは関数の中で値が代入されるため、見た目には初期化されているように見えます。でもTypeScriptは、関数setTitleが先に呼ばれていることを静的には判断できないためエラーを出してきます。

明確な割り当てアサーション(!)を使用した場合

let title!: string; // 明確な割り当てアサーションを使う

function setTitle(value: string) {
  title = value;
}

setTitle('Hello TypeScript');

console.log(title); // 出力: Hello TypeScript

title!と明確な割り当てアサーション演算子(!)を付けることで、「この変数titleは使う前に必ず初期化される」とTypeScriptに伝えることができます。その結果、TypeScriptはエラーを出さずに通してくれます。

注意点

初期化漏れに注意してください。

let title!: string; // 初期化されると信じ込ませているが...

// title = 'Hello TypeScript'; ← 初期化し忘れ!

console.log(title.length);
// 実行時エラー:Cannot read properties of undefined (reading 'length')

上記のコードでは、title!をつけて「この変数titleは使う前に必ず初期化される」とTypeScriptに伝えていますが、実際には何も代入していないため、実行時にエラーが発生します。

本記事のまとめ

この記事ではTypeScriptでよく見かける?!といった記号について解説しました。

各記号の違いをまとめると以下のようになります。

記号機能名主な用途JavaScript
で使える?
TypeScript
対応バージョン
?オプションパラメーター関数の引数を省略可能にする❌(TS限定)✅ TypeScript 1.1〜
?.オプショナルチェーン安全にプロパティアクセス✅ ES2020〜 対応✅ TypeScript 3.7〜
??Null合体演算子デフォルト値を使う✅ ES2020〜 対応✅ TypeScript 3.7〜
??=Null合体代入演算子nullundefined のときだけ代入✅ ES2021〜 対応✅ TypeScript 4.0〜
!非nullアサーション演算子nullチェックを強制スキップ❌(TS限定)✅ TypeScript 2.0〜
!明確な割り当てアサーション後で初期化されると明示する❌(TS限定)✅ TypeScript 2.7〜

お読み頂きありがとうございました。

スポンサーリンク