📚

JS and TS Tips

2023/09/08に公開

はじめに

のんびり更新していきます。

概要

JSとTSのTipsをまとめます。
簡単なものから難しいものまでをまとめています。

分割代入(デストラクチャリング)

分割代入で、オブジェクトや配列の中身を取りだすと同時に変数に代入できます。

連想配列の場合

連想配列の場合は、キーを指定して取りだすことができます。
キーに別名をつけることもできます。

destructuring.js
const obj = { a: 1, b: 2, c: 3 };
const { a, b, c: d } = obj; // cをdとして取り出す

console.log(a, b, d); // 1 2 3
TypeScript
destructuring.ts
const obj = { a: 1, b: 2, c: 3 };
const { a, b, c: d } = obj; // cをdとして取り出す

console.log(a, b, d); // 1 2 3

配列の場合

配列の場合は、順番に取りだすことができます。
必ずしも全ての要素を取りだす必要はありません。
いくつかの書き方があります。

  • [a, b] のように、取り出したい要素のみを指定して取りだす
  • [a, ...b] のように、... を使って残りの要素を取りだす
  • [, , c] のように、取り出したい要素の前に , をつけることで、その要素をスキップする
destructuring.js
const arr = [1, 2, 3];

const [a, b] = arr; // 1, 2を取り出す
const [, , c] = arr; // 3を取り出す
const [, ...d] = arr; // 1以外を取り出す

console.log(a, b, c, d); // 1 2 3 [2, 3]
TypeScript
destructuring.ts
const arr = [1, 2, 3];

const [a, b] = arr; // 1, 2を取り出す
const [, , c] = arr; // 3を取り出す
const [, ...d] = arr; // 1以外を取り出す

console.log(a, b, c, d); // 1 2 3 [2, 3]

浅いコピーと深いコピー

浅いコピーと深いコピーはC言語をやったことがある人なら、馴染みのある言葉だと思います。

浅いコピー

浅いコピーは、オブジェクトの参照をコピーします。
変数はよく箱と例えられますが、参照は箱がある場所を指していると考えるとわかりやすいと思います。

shallowCopy.js
const obj = { a: 1, b: 2 };
const shallowCopy = obj;

shallowCopy.a = 2;

console.log(obj); // { a: 2, b: 2 }
TypeScript
shallowCopy.ts
const obj = { a: 1, b: 2 };
const shallowCopy = obj;

shallowCopy.a = 2;

console.log(obj); // { a: 2, b: 2 }

箱の場所をコピーしているため、shallowCopy を変更すると、obj も変更されます。
場所しか知らないので、同じ箱に書き込んでしまったと考えるとわかりやすいと思います。

深いコピー

深いコピーは、オブジェクトの値をコピーします。
箱自体を増やすと考えるとわかりやすいと思います。

deepCopy.js
const obj = { a: 1, b: 2 };
const deepCopy = { ...obj };

deepCopy.a = 2;

console.log(obj); // { a: 1, b: 2 }
TypeScript
deepCopy.ts
const obj = { a: 1, b: 2 };
const deepCopy = { ...obj }; // or Object.assign({}, obj)

deepCopy.a = 2;

console.log(obj); // { a: 1, b: 2 }

{...obj} の ... は、スプレッド構文と呼ばれ、配列やオブジェクトを展開できます。
Object.assign を使っても同じことができますが、分割代入の方が簡潔に書けるので、こちらを使うことをおすすめします。

有理数であるか判定する

有理数かどうかを判定するときに、以下のようなコードでは不具合が発生します。

isNumber.js
const isNumber = (num) => {
  if (isFinite(num)) {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

isNumber(NaN); // num is falsy
isNumber(Infinity); // num is falsy
isNumber(null); // id is truthy
isNumber("1"); // id is truthy
TypeScript
isNumber.ts
const isNumber = (num: unkown) => {
  if (isFinite(num as number)) {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

isNumber(NaN); // num is falsy
isNumber(Infinity); // num is falsy
isNumber(null); // id is truthy
isNumber("1"); // id is truthy

これは、jsの仕様で、isFiniteInfinity or NaN or undefined or連想配列({}) の場合に false を返します。
それ以外の場合は、true を返します。
このような場合は、以下のように判定する必要があります。

isNumber.js
const isNumber = (num) => {
  if (Number.isFinite(num)) {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};
TypeScript
isNumber.ts
const isNumber = (num: unknown) => {
  if (Number.isFinite(num)) {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

Number.isFinite は、isFinite と異なり、数値型に対してのみ true を返します。
Infinity or NaNfalse を返します。

typeof ではダメなのか

typeof でも判定できそうですが、以下のような落とし穴があります。

isNumber.js
const isNumber = (num) => {
  if (typeof num === "number") {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

isNumber(NaN); // num is truthy
isNumber(Infinity); // num is truthy
isNumber(null); // num is falsy
isNumber("1"); // num is falsy
TypeScript
isNumber.ts
const isNumber = (num: unknown) => {
  if (typeof num === "number") {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

isNumber(NaN); // num is truthy
isNumber(Infinity); // num is truthy
isNumber(null); // num is falsy
isNumber("1"); // num is falsy

InfinityNaNnumber 型であるため、true を返します。
そのため、有理数かどうかを判定する場合は、Number.isFinite を使う必要があります。

型を絞り込めるようにする

以下の関数では、xnumber 型であることを保証できません。
人の目で見れば、xnumber 型であることがわかりますが、コンパイラは xunknown 型であると判断します。

isNumber.ts
const isNumber = (num: unknown) => {
  if (Number.isFinite(num)) {
    return true;
  } else {
    return false;
  }
};
const x: unknown = 1;

if (isNumber(x)) {
  console.log(x.toFixed(2)); // x is unknown
}

xnumber 型であることを保証するには、以下のように型アサーションを使います。

isNumber.ts
const isNumber = (num: unknown): num is number => {
  if (Number.isFinite(num)) {
    return true;
  } else {
    return false;
  }
};

const x: unknown = 1;

if (isNumber(x)) {
  console.log(x.toFixed(2)); // x is number
}

is を使うことで、左辺が右辺の型であることを保証できます。

配列を判定する

配列は typeofobject と判定されます。
一見すると、判定できそうです。
ですが、typeof には以下のような落とし穴があります。

isArray.js
const isArray = (arr) => {
  if (typeof arr === "object") {
    console.log("arr is truthy");
  } else {
    console.log("arr is falsy");
  }
};

isArray([]); // arr is truthy
isArray({}); // arr is truthy
isArray(null); // arr is truthy
TypeScript
isArray.ts
const isArray = (arr: unknown) => {
  if (typeof arr === "object") {
    console.log("arr is truthy");
  } else {
    console.log("arr is falsy");
  }
};

isArray([]); // arr is truthy
isArray({}); // arr is truthy
isArray(null); // arr is truthy

この関数に null or連想配列({}) を渡すと、true となります。
これは、null と 連想配列({}) が object であるためです。

このような場合は、以下のように判定する必要があります。

isArray.js
const isArray = (arr) => {
  if (Array.isArray(arr)) {
    console.log("arr is truthy");
  } else {
    console.log("arr is falsy");
  }
};
TypeScript
isArray.ts
const isArray = (arr: unknown) => {
  if (Array.isArray(arr)) {
    console.log("arr is truthy");
  } else {
    console.log("arr is falsy");
  }
};

Array.isArray は、typeof と異なり、null や連想配列({})を false として判定します。

デフォルト引数の仕様

デフォルト引数は、関数が呼び出された時に評価される

デフォルト引数は、関数が呼び出された時に評価されます。
言い換えるなら毎回初期化されるということです。

defaultArgs.js
const defaultArgs = (value, arr = []) => {
  arr.push(value);
  console.log(arr);
};

defaultArgs(1); // [1]
defaultArgs(2); // [2]
TypeScript
defaultArgs.ts
const defaultArgs = (value: number, arr: number[] = []) => {
  arr.push(value);
  console.log(arr);
};

defaultArgs(1); // [1]
defaultArgs(2); // [2]

前の引数を後のデフォルト引数で使える

前の引数を使ってデフォルト引数を設定できます。

defaultArgs.js
const defaultArgs = (a, b = a) => {
  console.log(a, b);
};

defaultArgs(1); // 1 1
defaultArgs(1, 2); // 1 2
TypeScript
defaultArgs.ts
const defaultArgs = (a: number, b: number = a) => {
  console.log(a, b);
};

defaultArgs(1); // 1 1
defaultArgs(1, 2); // 1 2

float 型を判定する

JavaScriptには、float 型がありません。
全て number 型です。
JavaScriptには数値の判定系メソッドがたくさんありますが、その中で、isFiniteNumber.isFinitefloat 型を判定できます。
今回は数値の float 型を判定したいので、Number.isFinite を使います。

isFloat.js
const isFloat = (num) => {
  if (Number.isFinite(num)) {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

isFloat(1.1); // num is truthy
isFloat("1.1"); // num is falsy
TypeScript
isFloat.ts
const isFloat = (num: unknown) => {
  if (Number.isFinite(num)) {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

うまくいってそうですが、Number.isFinite は有理数を判定するメソッドなので、整数も true と判定してしまいます。
これを回避するには、Number.isInteger を使います。
Number.isInteger は、整数を判定するメソッドです。

isFloat.js
const isFloat = (num) => {
  if (Number.isFinite(num) && !Number.isInteger(num)) {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

isFloat(1.1); // num is truthy
isFloat("1.1"); // num is falsy
TypeScript
isFloat.ts
const isFloat = (num: unknown) => {
  if (Number.isFinite(num) && !Number.isInteger(num)) {
    console.log("num is truthy");
  } else {
    console.log("num is falsy");
  }
};

isFloat(1.1); // num is truthy
isFloat("1.1"); // num is falsy

float型は有理数かつ、整数でない数値となるため、Number.isFiniteNumber.isInteger を使って判定できます。

計算量を減らす

例えば、以下のような配列があるとします。

const arr = [
  1,
  2,
  undefined,
  3,
  4,
  undefined,
  5,
  6,
  undefined,
  7,
  8,
  undefined,
  9,
  10,
];

この配列から、undefined を取り除き、すべての要素を2倍した配列を作成したいとします。
呼びだすメソッドの順番で計算量が変わります。

calculation.js
// map -> filter
const arr1 = arr.map((num) => num * 2).filter((num) => !Number.isNaN(num));

console.log(arr1); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter -> map
const arr2 = arr.filter((num) => num !== undefined).map((num) => num * 2);

console.log(arr2); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]
TypeScript
calculation.ts
// map -> filter
const arr1 = arr.map((num: any) => num * 2).filter((num) => !Number.isNaN(num));

console.log(arr1); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

// filter -> map
const arr2 = arr
  .filter((num): num is number => num !== undefined)
  .map((num) => num * 2);

console.log(arr2); // [2, 4, 6, 8, 10, 12, 14, 16, 18, 20]

先に filter を呼び出して、undefined を取り除いた配列を作成すると、map で計算する要素が減るため、計算量が減ります。

この先工事中 🚧

(最終更新日: 2023-09-17)

GitHubで編集を提案
GMOメディアテックブログ

Discussion