JS and TS Tips
はじめに
のんびり更新していきます。
概要
JSとTSのTipsをまとめます。
簡単なものから難しいものまでをまとめています。
分割代入(デストラクチャリング)
分割代入で、オブジェクトや配列の中身を取りだすと同時に変数に代入できます。
連想配列の場合
連想配列の場合は、キーを指定して取りだすことができます。
キーに別名をつけることもできます。
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
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]のように、取り出したい要素の前に,をつけることで、その要素をスキップする
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
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言語をやったことがある人なら、馴染みのある言葉だと思います。
浅いコピー
浅いコピーは、オブジェクトの参照をコピーします。
変数はよく箱と例えられますが、参照は箱がある場所を指していると考えるとわかりやすいと思います。
const obj = { a: 1, b: 2 };
const shallowCopy = obj;
shallowCopy.a = 2;
console.log(obj); // { a: 2, b: 2 }
TypeScript
const obj = { a: 1, b: 2 };
const shallowCopy = obj;
shallowCopy.a = 2;
console.log(obj); // { a: 2, b: 2 }
箱の場所をコピーしているため、shallowCopy を変更すると、obj も変更されます。
場所しか知らないので、同じ箱に書き込んでしまったと考えるとわかりやすいと思います。
深いコピー
深いコピーは、オブジェクトの値をコピーします。
箱自体を増やすと考えるとわかりやすいと思います。
const obj = { a: 1, b: 2 };
const deepCopy = { ...obj };
deepCopy.a = 2;
console.log(obj); // { a: 1, b: 2 }
TypeScript
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 を使っても同じことができますが、分割代入の方が簡潔に書けるので、こちらを使うことをおすすめします。
有理数であるか判定する
有理数かどうかを判定するときに、以下のようなコードでは不具合が発生します。
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
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の仕様で、isFinite は Infinity or NaN or undefined or連想配列({}) の場合に false を返します。
それ以外の場合は、true を返します。
このような場合は、以下のように判定する必要があります。
const isNumber = (num) => {
if (Number.isFinite(num)) {
console.log("num is truthy");
} else {
console.log("num is falsy");
}
};
TypeScript
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 NaN は false を返します。
typeof ではダメなのか
typeof でも判定できそうですが、以下のような落とし穴があります。
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
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
Infinity と NaN は number 型であるため、true を返します。
そのため、有理数かどうかを判定する場合は、Number.isFinite を使う必要があります。
型を絞り込めるようにする
以下の関数では、x が number 型であることを保証できません。
人の目で見れば、x は number 型であることがわかりますが、コンパイラは x が unknown 型であると判断します。
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
}
x が number 型であることを保証するには、以下のように型アサーションを使います。
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 を使うことで、左辺が右辺の型であることを保証できます。
配列を判定する
配列は typeof で object と判定されます。
一見すると、判定できそうです。
ですが、typeof には以下のような落とし穴があります。
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
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 であるためです。
このような場合は、以下のように判定する必要があります。
const isArray = (arr) => {
if (Array.isArray(arr)) {
console.log("arr is truthy");
} else {
console.log("arr is falsy");
}
};
TypeScript
const isArray = (arr: unknown) => {
if (Array.isArray(arr)) {
console.log("arr is truthy");
} else {
console.log("arr is falsy");
}
};
Array.isArray は、typeof と異なり、null や連想配列({})を false として判定します。
デフォルト引数の仕様
デフォルト引数は、関数が呼び出された時に評価される
デフォルト引数は、関数が呼び出された時に評価されます。
言い換えるなら毎回初期化されるということです。
const defaultArgs = (value, arr = []) => {
arr.push(value);
console.log(arr);
};
defaultArgs(1); // [1]
defaultArgs(2); // [2]
TypeScript
const defaultArgs = (value: number, arr: number[] = []) => {
arr.push(value);
console.log(arr);
};
defaultArgs(1); // [1]
defaultArgs(2); // [2]
前の引数を後のデフォルト引数で使える
前の引数を使ってデフォルト引数を設定できます。
const defaultArgs = (a, b = a) => {
console.log(a, b);
};
defaultArgs(1); // 1 1
defaultArgs(1, 2); // 1 2
TypeScript
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には数値の判定系メソッドがたくさんありますが、その中で、isFinite と Number.isFinite は float 型を判定できます。
今回は数値の float 型を判定したいので、Number.isFinite を使います。
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
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 は、整数を判定するメソッドです。
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
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.isFinite と Number.isInteger を使って判定できます。
計算量を減らす
例えば、以下のような配列があるとします。
const arr = [
1,
2,
undefined,
3,
4,
undefined,
5,
6,
undefined,
7,
8,
undefined,
9,
10,
];
この配列から、undefined を取り除き、すべての要素を2倍した配列を作成したいとします。
呼びだすメソッドの順番で計算量が変わります。
// 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
// 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)
Discussion