DanLevy.net

クイズ: 分割代入の魅力

分割代入の達人ですか?

それとも、あなたの破壊の交響曲ですか?

このクイズは JavaScript の分割代入に関する知識を測ります。基本的なオブジェクト構文から入れ子のデストラクチャ、デフォルト値まで網羅します。さらに TypeScript のインライン型に関するボーナス問題もあります。

まずはウォームアップから――分割代入スキルを証明してください! 👇

このコードは何を出力しますか?

const person = {
name: 'Dan Levy',
location: 'Cape Town',
};
const { name, age } = person;
console.log(`Name: ${name}, Age: ${age}`);

person に age プロパティが存在しないので、age は undefined になります。絶対に Infinity ではありません 😅

これにより次のようになります:

Name: Dan Levy, Age: undefined

このコードは何をしますか?

const person = [ 'Dan Levy', 'Cape Town' ];
const [ name, origin, age ] = person;
console.log(`Name: ${name}, Age: ${age}`);

age 変数は tuple 配列に存在しないので、undefined になります。

これにより次のようになります:

Name: Dan Levy, Age: undefined

入れ子の分割代入はどうですか?

'use strict';
const person = {
name: { first: 'Dan' },
address: { city: 'Denver' },
};
const {
name: { first },
address: { city },
birth: { place },
} = person;
console.log(
`First: ${first}, City: ${place}`,
);

birth: { place } プロパティは person に存在しないため、エラーがスローされます。 1つの解決策は、入れ子のプロパティにデフォルト値を提供することです。

入れ子のプロパティにアクセスする際は注意が必要です。エラーが見つけにくく、ブラウザやプラットフォームによってエラーメッセージが異なるため、デバッグがやや難しくなります。

最新の Chrome では: TypeError: Cannot read properties of undefined (reading 'place')

Node でも同様に TypeError になります。JavaScript が place を読み取る前に undefined から place を分割代入しようとするためです。

正確な文言はブラウザやランタイムによって異なります。

デフォルトをいくつか入れたら、これどうなる?

'use strict';
const person = {
name: { first: 'Dan' },
address: { city: 'Denver' },
};
const {
name: { first = 'Unknown' },
birth: { place = 'Unknown' },
} = person;
console.log(
`Hi ${first} from ${place}`,
);

birth が存在しないため、JavaScriptは undefined から place を分割代入しようとします。place のデフォルト値を適用する前に例外が発生します。

TypeError

正確なエラーメッセージは実行環境によって異なります。入れ子のプロパティだけでなく、外側の birth オブジェクトにも = {} のデフォルトが必要です。

これは何をするでしょうか?

const person = {
name: { first: 'Dan' },
address: { city: 'Denver' },
};
const {
name: { first = 'Unknown' },
birth: { place = 'Unknown' } = {},
} = person;
console.log(
`Hi ${first} from ${place}`,
);

person に birth プロパティが存在しないため、空オブジェクト = {} にフォールバックします。これによりデフォルト値が使用されます。

関数パラメータとして、これが何をするか?

'use strict';
function displayUser({
name = "Unknown",
age = -1,
} = { place: "Unknown" }) {
console.log(`Hi ${name} from ${place}`);
}
displayUser({ name: "Dan" });

この関数は name と age プロパティを抽出し、必要に応じてデフォルトを使用します。この場合、デフォルトオブジェクトの place キーは単なるノイズで、displayUser() 内では使用されていません。

strict モードでも挙動は変わりません: 宣言されていない place バインディングを読み取ると ReferenceError がスローされます。

undefined 値はどのように扱われますか?

'use strict';
function displayPlace({
name = "N/A",
place = "N/A",
age = -1,
} = { place: "Unknown" }) {
console.log(`${place}`);
}
displayPlace({ name: "Dan" });
displayPlace({ name: "Dan", place: undefined });
displayPlace({ name: "Dan", place: "Joburg" });

引数を省略した場合と明示的に undefined を渡した場合に、デフォルトの引数オブジェクト { place: "Unknown" } が使われます。displayPlace() と displayPlace(undefined) はどちらも Unknown を出力します。

オブジェクトを渡した場合、その place プロパティが存在しないか undefined なら、プロパティのデフォルト値 "N/A" が使われます。引数全体とプロパティのデフォルトは別の段階で適用されます。

displayPlace() // Unknown
displayPlace({ name: "Dan" }) // N/A
displayPlace({ name: "Dan", place: undefined }) // N/A

前の問題と似ているけど… null はどう扱われる?

function displayPlace({
name = "N/A",
place = "N/A",
age = -1,
} = { place: "Unknown" }) {
console.log(`${place}`);
}
displayPlace({ name: "Dan", place: null });
displayPlace({ name: "Dan", place: undefined });

1回目は place が null、2回目は undefined です。オブジェクト自体が渡されていても、place が存在しないか undefined ならプロパティのデフォルト値 "N/A" が使われます。null はデフォルト値を適用しないため、出力は null と N/A です。

ここからはTypeScript

さて、TypeScriptで… 何が起こるでしょうか?

'use strict';
function displayPlace(
{
name = 'N/A',
place = 'N/A',
}: {
name: string;
place: string;
age: number;
},
) {
console.log(`${place}`);
}
displayPlace({ name: 'Dan', place: null });

TypeScript は place が string と型付けされているのに、呼び出しで null を渡しているためエラーを報告します。また、必須の age プロパティが省略されています。

型エラーを無視すれば、コードを実行するとコンソールに null が出力されます。

リネーム/代入を試してみよう…

'use strict';
function displayPlace({
name = 'N/A',
place: location = 'N/A',
}: {
name: string;
place: string;
age?: number;
}) {
console.log(`${location}`);
}
displayPlace({ name: 'Dan', place: 'Denver' });

これによりコンソールに Denver が出力されます。関数シグネチャで place プロパティが location にリネームされています。サードパーティのデータ構造に合わせる際の一般的なパターン(分割代入時のプロパティ名変更)です。

型エラーを見つけてください:

function greet({
name: {first = "N/A", last = "N/A"},
birth: {place = "N/A"} = {},
age = -1,
}: {
name: {first?: string, last?: string};
birth: {place?: string};
age: number;
}) {
console.log(`Hi ${first} ${last} from ${place}`);
}
greet({ name: {first: 'Dan'} });

greet 関数のシグネチャにエラーがあります。渡されたオブジェクトに age と birth プロパティがないので、型定義ではそれらをオプションにすべきです。

birth プロパティはデフォルト値で分割代入されていますが、型定義では存在することが要求されています。TypeScript でプロパティをオプションにするには ? 演算子を使います。

birth?: { place?: string } は birth: { place?: string } | undefined と同じではないことに注意してください。

今度は 代入(f、l、p 変数に注目)

'use strict';
function greet(
{
name: {first: f = "N/A", last: l = "N/A"},
birth: {place: p = "N/A"} = {},
age = -1,
}: {
name: {first?: string, last?: string};
birth?: {place?: string};
age?: number;
}
) {
console.log(`Hi ${f} ${l} from ${place}`);
// What will 👆 do?
}
greet({
name: {first: 'Dan', last: 'Levy'},
birth: {place: 'Cape Town'},
});

別のエラー!もう推測し始めましたか?!

デフォルトや代入、型が絡む分割代入の階層は読みにくいです!

place を p 変数に再代入すると、console.log 文のスコープ内ではもはや定義されていません。

console.log(`Hi ${f} ${l} from ${place}`); // ❌
// to:
console.log(`Hi ${f} ${l} from ${p}`); // ✅

さらに挑戦したい方は、クイズ一覧もどうぞ。