DanLevy.net

クイズ: デストラクチャリングの魅力

分割代入の達人ですか?

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

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

まずはウォームアップから――デストラクチャリングスキルを証明してください! 👇

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

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

personage プロパティが存在しないので、ageundefined になります。絶対に 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 プロパティは person に存在しないので、ネストされたプロパティだけでなくオブジェクト全体にデフォルトが必要です。実質的に = {} のデフォルトが抜けています。

この書き方は「person.birthundefined の場合、placeUnknown」という意味になります。しかし person.birthundefined なので、undefined をデストラクトしようとしてエラーになります。

In modern Chrome: `TypeError: Cannot read properties of undefined (reading 'place')`
In Node, this is also a `TypeError` because JavaScript tries to destructure `place` from `undefined`.
Exact wording varies between browsers and runtimes.

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

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

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

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

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

この関数は nameage プロパティを抽出し、必要に応じてデフォルトを使用します。この場合、デフォルトオブジェクトの 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" });

displayPlace 関数は、オブジェクトが渡されない場合にのみデフォルトオブジェクトを使用します。したがって、{ place: "Unknown" } のデフォルトを取得できるのは引数なしで displayPlace() を呼び出したときだけです。

もう一つ注目すべきは、placeundefined を渡すとデフォルト値が使用される点で、JSON.stringify の挙動(undefined を無視し null を認識する)に少し似ています。

その結果は次の通りです:

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 });

この場合、最初の呼び出しでは place プロパティが null に設定され、2 回目の呼び出しでは undefined になります。place のデフォルト値はオブジェクト全体が存在しない または undefined のときだけ使用されます。null はそのまま null として渡されます。

さて、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 は placestring と型付けされているのに、呼び出しで 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 関数のシグネチャにエラーがあります。渡されたオブジェクトに agebirth プロパティがないので、型定義ではそれらをオプションにすべきです。

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

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

今度は 代入flp 変数に注目)

'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'},
});

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

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

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

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