JavaScript
TypeScript

読み手にやさしい if 文を書きたい

JavaScript(TypeScript)です。


読みやすい if


比較したい変数は比較演算子の左側に書く

以下のコードでは「年齢が10歳以上か」と自然に読むことができます。


比較対象を左側に置く

if (age >= 10) { }


「10歳が年齢以下か」とは人間の思考的には考えにくいです。


比較対象を右に置く

if (10 <= age) { }



範囲内にあるかないかを調べるには数直線上に並べる

数直線上に並べると視覚的にわかりやすくなります。


数直線上に並べた比較

// 1~100のランダムな値を得る

const getRandomScore = (): number => Math.floor(Math.random() * 101);
const value = getRandomScore();

// 以下の様に書けば視覚的にすぐに分かる
if (20 <= value && value <= 80) {
console.log(`値は範囲内です:${value}`);
} else {
console.log(`値は範囲外です:${value}`);
}


比較対象が常に左にあると視覚的にわかりづらくなります。


比較対象を常に左に置いた比較

if (value >= 20 && value <= 80) { }



truefalseと比較しない


真偽値がtrueが判定

if (word.includes('foo') /*文字列にfooが含まれるか*/) { 

// 含まれる場合の処理...
}

真偽値を返すメソッドやプロパティを判定する際、わざわざtrue(false)と比較するのは冗長です。

if (word.includes('foo') === true) { } 


早期リターンやガード節を使う

条件を満たさないケースをreturn文を使って早めにふるい落とします。

条件を忘れていくことが可能なので脳内メモリにやさしいです。


モデルの検証を行うメソッド

const isValid = (model: Model) => {

if (model === null) {
return false;
}
if (model.name.trim() === "") {
return false;
}
if (model.name.length > 20) {
return false;
}
if (!(0 <= model.age && model.age <= 150)) {
return false;
}
if (!/^[a-zA-Z0-9.!#$%&'*+/=?^_`{|}~-]+@[a-zA-Z0-9-]+(?:\.[a-zA-Z0-9-]+)*$/.test(model.email)) {
return false;
}
return true;
};


素数か判定するメソッド

const isPrime = (num: number): boolean => {

if (num <= 1) {
return false;
}
if (num === 2) {
return true;
}
const boundary = Math.floor((Math.sqrt(num)));
for (let i = 0; i <= boundary; ++i) {
if (num % i === 0) {
return false;
}
}
return true;
};

以下のようにreturn文を一つにするとネストがどんどん深くなります。

さらに、コードを読む際に条件を全ておぼえておかなければなりません。

途中で条件を加えるたびにコードはますます複雑になります。

const isValid = (model) => {

let blnrtn = false;
if (model != null) {
if (model.name.trim !== "") {
if (model.name.length <= 20) {
if (mailPattern.test(model.email)) {
blnrtn = true;
}
// if …
}
// if …
}
}
return blnrtn;
};

上記のように「奥へ突き進む」型のコードならまだわかりやすいですが、

ブロックを閉じる位置などにif文等が加わると保守しにくいコードになっていきます。

また、以下のように return を無理やり一つにしようとすると余計なフラグ変数を用意しなければならなくなったり、ネストが複雑になります。

const isPrime = (num) => {

let blnrtn = true;
if (Number.isNaN(num)) {
blnrtn = false;
} else if (num <= 1) {
blnrtn = false;
} else if (num === 2) {
blnrtn = true;
} else {
const boundary = Math.floor((Math.sqrt(num)));
for (let i = 0; i <= boundary; ++i) {
if (num % i != 0) {
blnrtn = true;
break;
}
}
}
return blnrtn;
};


即時関数を使う

即時関数を使うことでメソッド内に変数のスコープを限定出来ます。

(即時関数・・・匿名メソッドを定義してそのまま実行することを一般的に即時関数と呼ぶようです。)


匿名関数を即時実行してスコープを限定する

// ランダムなスコア(1~100)を得る

const getRandomScore = (): number => Math.floor(Math.random() * 101);

const rank: string = ((score) => {
if (score >= 80) {
return 'A';
} else if (score >= 60) {
return 'B';
} else if (score >= 40) {
return 'C';
} else {
return 'D';
}
})(getRandomScore());
console.log(rank);


以下のように書くと score のスコープがグローバルになってしまい、 rank も定数でなくなったために、修正を繰り返すうちに再代入されて思いもよらない値が入る可能性があります。

さらに、リファクタリングを繰り返すうちに元の目的とは関係のないのコードが if 文にまぎれ可読性を損なうことがあります。

const score = getRandomScore();

let rank = '';
if (score >= 80) {
rank = 'A';
// ~~~~ ランク判定とは関係のないコード ~~~~
} else if (score >= 60) {
rank = 'B';
} else if (score >= 40) {
rank = 'C'
// ~~~~ ランク判定とは関係のないコード ~~~~
} else {
// ~~~~ ランク判定とは関係のないコード ~~~~
rank = 'D';
}

score = 10; // ここでもアクセスできてしまう。

// ~~~~~~~~何か長い処理 ~~~~~~~~~

rank = 'S'; // 意図せぬ値が再代入
console.log(`${rank}です。`);


そもそも if 文を書かない


真偽値は直接それを返す

2つの値をその結果をtruefalseで返したい場面があります。

その場合、評価した結果を直接返します。


真偽値を直接返す

return foo === bar;


以下のようにフラグ変数を用意したりif文を使用するのは冗長です。

const flag = (foo === bar);

return flag;

if (foo === bar) {

return true;
} else {
return false;
}


三項演算子を使う

三項演算子を使うと余計なローカル変数を定義しなくても良く、if 文と違って余計なコードがまぎれる心配もありません。

const time: number = new Date().getHours();

const ampm: string = time <= 12 ? '午前' : '午後';
console.log(`今は${ampm}です。`);

三項演算子は式なので直接、関数の引数に書くことも出来ます。

以下の例は配列 words に文字列 'foo' が含まれている場合に1を、そうでない場合に0を引数に渡す例です。

class Foo {

static doSomething(n: number): string {
return `${n} が引数として渡されました`;
}
}
const words: string[] = ['foo', 'bar', 'buz'];

const result: string = Foo.doSomething(words.some(word => word === 'foo') ? 1 : 0);
console.log(result); // 1 が引数として渡されました


配列の反復メソッドを使う

以下のような Book クラスとその配列の myBookShelf があったとします。

class Book {

constructor(
public name: string,
public price: number
) { }
public includedTax(): number {
return Math.floor(this.price * 1.08)
}
}
const myBookShelf: Array<Book> = [
new Book('JavaScript入門', 1750),
new Book('基礎からのTypeScript', 3200),
new Book('AnglarとTypeScriptでアプリ製作', 2550),
new Book('実践! Node.js', 2980),
// ...以下略
];

myBookShelf に税込み価格2000円以上の本がいくつあるかを求めます。

forループとif文を組み合わせた方法で求めようとすると以下のようになります。


forループとif文を使った方法

let count: number = 0;

for (const book of myBookShelf) {
if (book.includedTax() >= 2000) {
count++;
}
}
console.log(`私は税込み2000円以上の本を${count}冊持っています。`);

配列の反復メソッドを使用すると以下のように簡潔に記述できます。

何がしたいのかが一目でわかります。


filterを使った方法

const count: number = myBookShelf

.filter(book => book.includedTax() >= 2000)
.length;

console.log(`私は税込み2000円以上の本を${count}冊持っています。`);


Array には以下のように if を使わずに読みやすく書けるメソッドが用意されています。



  • filter 条件に合うものだけフィルタリングした配列を返す


  • find 配列内の条件を満たす最初の要素を返す


  • every 全ての要素が条件を満たすかを true または false で返す


  • includes 特定の要素が配列に含まれているかどうかを true または false で返す


  • some 配列内に条件を満たす要素があるかをtrue または false で返す

以上です。ここまで読んでいただきありがとうございました。


SI 企業所属の2年目プログラマーです。

エンジニアの方とつながれると嬉しいです!。:grinning:

twitter: のさ@nosa_programmer