🧑‍💻

TypeScriptでよく見る「?」「!」と仲良くしたい

2021/12/16に公開約4,700字8件のコメント

どうも、株式会社iCAREフロントエンドエンジニアのoreoです。

弊社フロントエンドは、Vue.js, TypeScriptで開発をしています。あるPRで、弊社技術顧問の方から、下記レビューを頂きました。

リファクタするときに気になったんですが、不要なところに?がついているのを散見しましたキャスト! | ? | asはどれも危険なコードでレビュワーの負担も大きくなるので、きちんと必要なところだけに入れるように気をつけてみてください

今までを安易に使っていたので、彼らの挙動について整理してみたいと思います!

?について

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

概要

引数にオプションパラメーターをつけると、その引数が省略可能になります。例1.1で、test()を実行した場合、Expected 1 arguments, but got 0. と怒られますが、オプションパラメーターをつけると(例1.2)、怒られなくなります。

//例1.1
const test = (x:number,y:number):void=>{
  console.log(x);
};
test(1); //エラー Expected 2 arguments, but got 1.

//例1.2
const test = (x:number,y?:number):void=>{
  console.log(x);
}
test(1);  //OK

注意点

  • 例1.2において、オプションパラメーターを使用されると、引数yは、number | undefined のユニオン型と認識されてしまいます。
  • また、オプションパラメーターは必ず引数の最後に書かないといけません(例1.3)。
//例1.3
const test = (x?:number,y:number):void=>{
  console.log(x,y);
};

//エラー A required parameter cannot follow an optional parameter.

補足:デフォルトパラメーター

👇のような書き方で、オプションパラメーターにデフォルト値を与えることもできます。また、デフォルトパラメーターは、引数の最後に書かなくてもOKです。

//例1.4
const test = (x: number, y = 3): void => {
  console.log(x+y);
  console.log(x);
};

test(1); //OK

// console.log(x+y)は4と出力
// console.log(x)は1と出力

オプショナルチェイニング(?.)

概要

TypeScriptの 3.7以降で使用可能。nullundefienのプロパティに対して.で参照するとエラーが発生する(例2.1)が、?.を使うと、エラーは発生せず、undefiendが返ります(例2.2)

//例2.1
type Address = {
      prefecture: {
        name: string
        city?: {
          name: string
        }
      }
    }
    
const address: Address = {
      prefecture: {
        name: 'tokyo',
      },
    }
const targetAddress = address.prefecture.city.name //エラー Object is possibly 'undefined'.
console.log(targetAddress)

//例2.2
type Address = {
      prefecture: {
        name: string
        city?: {
          name: string
        }
      }
    }
const address: Address = {
      prefecture: {
        name: 'tokyo',
      },
    }
const targetAddress = address.prefecture.city?.name //OK
console.log(targetAddress)

注意点

  • ?.を使用した場合すると、短絡評価されます。短絡評価では、?.よりも左側がnullundefiendの場合、?.より右側は評価(実行)されなくなります。

Null合体演算子(??)

概要

TypeScriptの 3.7以降で使用可能。左辺がnullまたはundefiendの場合に右の値を返し、それ以外の場合は左の値を返します(例3.1)。

//例3.1
const name = null ?? 'Yamada Taro';
console.log(name);
//  'Yamada Taro'が出力

注意点

??は、OR演算子||とは違って、nullまたはundefiend以外のfalsyな値の場合(例えば、''0)、左の値を返します

//例3.2 OR演算子
const age = 0 || 18;
console.log(age);
// 18が出力

//例3.3 Null合体演算子
const age = 0 ?? 18;
console.log(age);
// 0が出力

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

概要

TypeScriptの 4.0以降で使用可能。左辺がnullまたはundefiendの場合に代入します(例4.1)。

//例4.1
const pilot = { name: null };

pilot.name ??= "shinji";
console.log(pilot.name); //shinjiが出力

//例4.2 
const pilot = { name: "shinji" };

pilot.name ??= "asuka";
console.log(pilot.name); //shinjiが出力。asukaが代入されない。

例4.3の2行は同じ結果となります。

const pilot = { name: null };

//例4.3
pilot.name ??= "shinji"; //Null合体代入演算子
pilot.name = pilot.name ?? "shinji"; //Null合体演算子

注意点

左辺がnullまたはundefiend以外のfalsyな値の場合(例えば、''0)、代入されません

//例4.4
const pilot = { age: 0 };
pilot.age ??= 14; 
console.log(pilot.age) //0が出力

!について

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

概要

null許容型(T | null またはT | null | undefiend)に対して、非nullアサーション(!)を使用することで、その型がnull | undefiendではなくTであることをコンパイラに明示できます。

//例5.1
type Dog = { name: string };
const mydog = { name: "sora" };

const getDog = (dog?: Dog) => {
  console.log(dog.name); //エラー Object is possibly 'undefiend'
};
getDog(mydog);

//例5.2
type Dog = { name: string };
const mydog = { name: "sora" };

const getDog = (dog?: Dog) => {
  console.log(dog!.name); //OK
};
getDog(mydog);

注意点

強制的にnull | undefiendではないことを明示してしまい、型の変化に気づけなくなる可能性があるので、基本的にあまり使わない方が良いかも。

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

概要

例6.1のように変数に値が割り当てらていない可能性がある場合(TypeScript が静的に検出できない場合)、その変数を使用すると怒られます。しかし、!を使うことで、変数を使う時までにその変数に値が割り当てられていることをコンパイラに明示できます。

//例6.1
let name: string;

const addName = (val: string) => {
  name = val;
};
addName("shinji");

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

//例6.2
let name!: string;

const addName = (val: string) => {
  name = val;
};
addName("shinji");

console.log(name); //OK

注意点

コードの変更などで、変数が割り当てられななった場合に気づけなくなるので、こちらも基本的にあまり使わない方が良いかも。

最後に

改めてまとめることで、頭の中が整理されました。?!は、とても便利ですが、その危険性(型の変化等に気づけなる)ことをしっかり理解した上で、使用すべきですね。また、チームメンバー間での、?!についての理解度のすり合わせやコーディングルール明確化も重要なのかなとも思いました。

参考

株式会社オライリー・ジャパン「プログラミングTypeScript」

https://typescript-jp.gitbook.io/deep-dive/
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators
https://qiita.com/uhyo/items/6cd88c0ea4dc6289387a

Discussion

いい記事!

ここやけど多分yじゃないかな?

例1.2において、オプションパラメーターを使用されると、引数xは、string | undefined のユニオン型と認識されてしまいます。

あとこのオプショナルパラメーターって具体的にどういう関数で使えると思う?(クイズではなく単純に教えてほしい🙏)

コメントありがとうございます!

記載ミスしておりました..修正しております!

オプショナルパラメーターって具体的にどういう関数で使えると思う?

例えば、ライブラリのラッパー関数を作る時など、さまざまな場面で呼び出される可能性があって共通化する時に使うのかなと思っております!影響範囲が小さい(呼び出される場面が限定的)な場合は、オプションにすることはあまりないのかと。
違和感あれば教えていただけると幸いです!!

うーんまだ抽象的やからもっと具体的な例が知りたいな〜

例えば👇みたいな形で、isAscをオプショナルパラメーターとして設定してあげると、返り値になる配列の要素の並び替え(昇順)を選択できます!競プロでstring型の標準入力をnumber[]型に変換して配列操作したい時に使う想定しています(昇順に並び替える機会、そんなにないと思いますが、、、、)

//数字+半角スペースのstring型の入力値を、number[]型に変換するモジュール
//isAscをtrueにして渡すと、number[]型に変化する際に昇順に変換してくれる
const stringToNumArray = (input: string, isAsc?: boolean): number[] => {
  const numArray = input.split(" ").map((x: string): number => Number(x));
  if (isAsc) {
    return numArray.sort((a: number, b: number): number => (a < b ? -1 : 1));
  }
  return numArray;
};

console.log(stringToNumArray("1 0 9"));     //[1, 0, 9]
console.log(stringToNumArray("1 0 9", true));  //[0, 1, 9]
console.log(stringToNumArray("1 0 9", false));  //[1, 0, 9]

なるほど〜めちゃ具体的になったな!
気になったのはこれやとデフォルトパラメーターでisAsc=falseってやっても全く同じことできそうやけど、そこの使い分けってなんかあるんかな?
初めに書けば良かったんやけど、自分が一番気になってるのはそこなんよね

確かに👇みたいに、isAsc=falseにしても、同じ結果が得られますね!

const stringToNumArray = (input: string, isAsc=false): number[] => {
  const numArray = input.split(" ").map((x: string): number => Number(x));
  if (isAsc) {
    return numArray.sort((a: number, b: number): number => (a < b ? -1 : 1));
  }
  return numArray;
};

console.log(stringToNumArray("1 0 9"));     //[1, 0, 9]
console.log(stringToNumArray("1 0 9", true));  //[0, 1, 9]
console.log(stringToNumArray("1 0 9", false));  //[1, 0, 9]

オプショナルパラメーターを使うと、isAscの型がisAsc: boolean | undefinedと認識されてしまいますが、デフォルトパラメーターだと、isAscの型がisAsc: booleanとなるので、undefinedとしての型を認識させるのかどうかの視点で使い分けできそうかと思いました🤔

そうやな、いう通り

undefinedとしての型を認識させるのかどうかの視点で使い分けできそう

かな!

質問してばっかりやとあれやから自分も考えてみたけど

  • 省略できない(undefinedを許容しない)のでデフォルト値を持たせたい引数 → デフォルト引数
  • 省略できる(undefinedを許容する)引数 → オプショナルパラメータ

て感じになるんかな〜と思った。

例としては

デフォルト引数
Slackにメッセージを送信する。チャンネルを指定しなければデフォルトのチャンネルに送信される。

sendToSlack(message: string, channel: string = "default_channel") {
	client.send(message, channel)
} 

sendToSlack('こんにちは') // デフォルトのチャンネルに送信される
sendToSlack('This is log', 'log_channel') // log_channelに送信される

オプショナルパラメータ
メールを送信してエラーをコールバックで受け取りたい場合

sendMail(address: string, body: string, onError?: (error: Error) => void) {
	try {
		client.send(address, body)
	} catch(error) {
		onError?(error) ?? throw error
	}
} 

みたいな感じかな、オプショナルの方もうちょっといいのありそうやけど
TypeScriptはオーバーロードの仕組みがないからその代わりでも使えるみたい
→ ごめん勘違いしててオーバーロードの仕組みは普通にあったわ🙏

https://www.typescriptlang.org/docs/handbook/functions.html#overloads

なるほど、わかりやすい、、、!お話させて頂いて、理解が深まりました!ありがとうございます!!!

ログインするとコメントできます