51
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

今さら分割代入知らないなんて言えないのでTypeScriptでこっそり勉強する

Last updated at Posted at 2020-01-18

({ id }) => {} とか { id } = params とか、あーこういう書き方できるよねってぼんやりした知識はあったんですが、分割代入って何?何ができるの?って言われてみるときちんと知りませんでした。今さら分割代入知らないとか言えないので、こっそり MDN のページを読みつつ TypeScript と絡めて勉強します。

MDN web docs 分割代入
https://developer.mozilla.org/ja/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment

環境(2020/01/19追記)

下記のソースコードは TypeScript playground(v3.7)で動作を確認しました。
https://www.typescriptlang.org/play/#

(1)配列

基本的な使い方

基本的な使い方はこのような感じです。変数宣言と同時に配列の要素の値を代入する事ができます。

const [a, b] = [1, 2];

a; // 1
b; // 2

let で宣言すると再代入が可能です。

let [a, b] = [1, 2];
a = 100;

a; // 100

型推論

右辺の配列リテラルは TypeScript ではタプル型とみなされます。

let [a, b, c] = [1, 'abc', true]; // [number, string, boolean]

つまり、このような代入をしているのと同じです。

const tuple: [number, string, boolean] = [1, 'abc', true];

let a = tuple[0]; // number
let b = tuple[1]; // string
let c = tuple[2]; // boolean

コンパイラにより型がチェックされるため、違う型の値を再代入する事はできません。

let [a, b, c] = [1, 'abc', true];

// コンパイルエラー
a = 'some text'; // Type '"some text"' is not assignable to type 'number'.
b = true; // Type 'true' is not assignable to type 'string'.
c = 100; // Type '100' is not assignable to type 'boolean'.

代入先の要素数より配列リテラルの要素数が少ない場合

コンパイルエラーが発生します。

// Tuple type '[number]' of length '1' has no element at index '1'.
const [a, b] = [1];

コンパイルエラーはデフォルト値を設定する事で回避できます。

const [a, b = 'none'] = [1];

a; // 1
b; // 'none'

代入先の要素数より配列リテラルの要素数が多い場合

残りの要素は代入されません。

const [a, b] = [1, 2, 3, 4, 5];

a; // 1
b; // 2

スプレッド構文を使うと、残りの要素をまとめて拾う事ができます。

const [a, b, ...c] = [1, 2, 3, 4, 5];

a; // 1
b; // 2
c; // [3, 4, 5]

要素を無視する

興味のない要素は、変数名を省略する事で無視する事ができます。

const [a,,,, b] = [1, 2, 3, 4, 5];

a; // 1
b; // 5

ネストした配列

このような記述で値をフラットに取得できます。

const [a, b, [c, d]] = [1, 2, [3, 4]];
a; // 1
b; // 2
c; // 3
d; // 4

配列の分割代入を使用する例

実際の運用コードでは [1, 2] のような要素が固定された配列はあんまり登場しませんよね。では、どんなときに配列の分割代入を使用するメリットがあるのでしょうか?

それはたぶん 配列の順番に意味がある時 ではないかと思います。例えば下記のように、同じ情報が繰り返し出現するテキストがあるとします。

const log = `
* Version: v1.0
* Date: 2020-01-31
* Changelog:
  - bugfix
  - update deps

* Version: v0.9
* Date: 2020-01-22
* Changelog:
  - bugfix

* Version: v0.8
* Date: 2019-12-15
* Changelog:
  - add feature
`;

テキストは「バージョン」「日付」「アップデート内容」の順番で、情報が繰り返されます。ここから「日付」だけをピックアップしてみましょう。

function getHistory (logs: string[]): string[] {
  const [, date, , ...rest] = logs;

  return rest.length ?
    [date].concat(getHistory(rest)) :
    [date]
  ;
}

const history = getHistory(log.split('*').splice(1));
// [
//   'Date: 2020-01-31', 
//   'Date: 2020-01-22', 
//   'Date: 2019-12-15'
// ]
// ※補足:splitした時に先頭に改行が入るので、spliceで除外

「配列の順番に意味がある時」って他にもありますよね。例えば正規表現によるテキストのマッチングにも使えそうです。

const regExp = new RegExp('[0-9]+', 'g');
const userInput = '0000-111-2222';

const [
  areaCode, 
  localAreaCode, 
  subscriberNumber
] = (userInput.match(regExp) || []) as string[];

areaCode + localAreaCode + subscriberNumber;
// 00001112222

どうでしょう。 [0][1] などの添字を使った従来のアクセスより、シンプルで意図が伝わるコードになったのではないでしょうか。

(2)オブジェクト

基本的な使い方

配列と同じく、変数宣言と同時にオブジェクトの要素の値が代入できます。

const { a, b } = { a: 1, b: 'abc' };

a; // 1
b; // 'abc'

let で宣言すると再代入が可能です。

let { a, b } = { a: 1, b: 'abc' };
a = 100;

a; // 100

変数宣言と代入を別の行に分ける事もできます。その際、左辺の {} がブロックとみなされシンタックスエラーになるため、行全体に () を使ってオブジェクトリテラルとみなされるようにします。

let a: number, b: string;

// シンタックスエラー
{ a, b } = { a: 1, b: 'abc' };

// OK
({ a, b } = { a: 1, b: 'abc' });

型推論

オブジェクトの分割代入は、このような代入をしているのと同じです。

let a = 1;
let b = 'abc';

コンパイラにより型がチェックされるため、違う型の値を再代入する事はできません。

let { a, b } = { a: 1, b: 'abc' };

// コンパイルエラー
a = 'some text'; // '"some text"' is not assignable to type 'number'.
b = 100; // Type '100' is not assignable to type 'string'.

興味のある値のピックアップ

このようなオブジェクトから値をピックアップしてみましょう。

const user = {
  id: 1,
  userName: 'ringtail003',
  email: 'ringtail003@gmailcom',
  config: {
    plan: {
      planName: 'Basic',
      paymentMethod: 'visa',
    },
  },
};

トップの階層から値をピックアップする時は、キー名と同じ名前の変数を宣言するだけです。

const { id, userName } = user;

id; // 1
userName; // 'ringtail003'

下の階層から値をピックアップする時は、構造を示します。

const { 
  config: { 
    plan: { 
      planName,
    }
  }
} = user;

planName; // 'Basic'

代入元のオブジェクトに存在しない変数を指定した場合、コンパイルエラーが発生します。型安全でいいですね。

const { 
  config: { 
    plan: { 
      // コンパイルエラー
      // Property 'point' does not exist on type '{ planName: string; paymentMethod: string; }'.
      point,
    }
  }
} = user;

デフォルト値

分割代入の左辺には型アノテーションを指定する事ができます。

let { a, b }: { a: number, b: string } = { a: 1, b: 'abc' };

これを利用すると、オプショナルな値にデフォルト値を設定する事ができます。

interface User { 
  id: number;
  userName: string;
  email?: string; // Optional
}

const user = {
  id: 1,
  userName: 'ringtail003',
};

const {
  id,
  userName,
  email = '設定されていません',
}: User = user;

email; // '設定されていません'

リネーム

分割代入の左辺では、代入元・代入先の変数の名前を別々に指定する事ができます。

let { a: myValue, b } = { a: 1, b: 'abc' };
myValue; // 1

これを利用すると、オブジェクトのキー名のリネームが簡単に処理できるようになります。

const ajaxResponse = {
  id: 1,
  user_name: 'ringtail003',
  email_address: 'ringtail003@gmail.com',
};

const {
  id,
  user_name: userName,
  email_address: email,
} = ajaxResponse;

const model = { id, userName, email };

model;
// {
//   id: 1,
//   userName: 'ringtail003',
//   email: 'ringtail003@gmail.com',
// }

さらに型アノテーションを指定しておけば、代入元のオブジェクトの形が代わった時にコンパイルエラーが得られるので、型安全になりますね。

+ interface AjaxResponse { 
+   id: number;
+   user_name: string;
+   email_address: string;
+ }
...

const {
  id,
  user_name: userName,
  email_address: email,
- } = ajaxResponse;
+ }: AjaxResponse = ajaxResponse;

(3)関数

関数では引数に分割代入を使用する事ができます。

function getItem({ name = '' }) {
  return ({ name });
}

上記の関数は、引数として与えられたオブジェクトから name をピックアップし、新しいオブジェクトを返すものです。

const items = [
  { id: 1, name: 'foo' },
  { id: 2, name: 'bar' },
];

items.map((item) => getItem(item));
// [
//   { name: 'foo' },
//   { name: 'bar' },
// ]

分割代入で空のオブジェクトを与えると、関数の呼び出し側で引数を省略する事ができます。

function getItem({ name = 'default' } = {}) {
  return ({ name });
}

getItem();
// { name: 'default' }

関数の分割代入を使用する例

下記のコードは、会議室と自転車のレンタル情報を示すものです。

interface MeetingRoom {
  name: string;
  charge: number;
  capacity: number;
}

const meetingRooms = [
  { name: '大会議室', charge: 8000, capacity: 100 },
  { name: '小会議室', charge: 1500, capacity: 10 },
];

interface Bicycle {
  name: string;
  charge: number;
  inMaintenance: boolean;
}

const bicycles = [
  { name: 'ロードバイク', charge: 500, inMaintenance: true },
  { name: 'ママチャリ', charge: 300, inMaintenance: false },
];

会議室と自転車がごちゃまぜになった「レンタル用品リスト」があるとします。

const list: (MeetingRoom | Bicycle)[] = [
  meetingRooms[0],
  meetingRooms[1],
  bicycles[0],
  bicycles[1],
];

このレンタル用品リストから「名前」「利用料金」と、自転車にしか存在しない「メンテナンス中フラグ」を取り出してみましょう。

function getItem({ 
  name = '', 
  charge = 0, 
  inMaintenance = undefined,
}) {
  return { name, charge, inMaintenance };
}

list.map((item) => getItem(item));
// [
//   { name: '大会議室', charge: 8000, inMaintenance: undefined },
//   { name: '小会議室', charge: 1500, inMaintenance: undefined },
//   { name: 'ロードバイク', charge: 500, inMaintenance: true },
//   { name: 'ママチャリ', charge: 300, inMaintenance: false },
// ]

引数に型アノテーションを指定しておくと会議室にも自転車にも存在しないプロパティをコンパイルエラーにできるため、より安全になります。

function getItem({ 
  name = '', 
  charge = 0, 
  inMaintenance = undefined,
- } {
+ }: Partial<MeetingRoom & Bicycle>) {
  return { name, charge, inMaintenance };

分割代入を使わない場合でも同じ処理を書いてコンパイルエラーを得る事ができます。ただし「この関数がどのプロパティを必要としているか」は関数内部のコードを読まないと分からないですよね。この点、分割代入ではシグネチャでそれを示す事ができるのがメリットではないかと思います。

function getItem(item: Partial<MeetingRoom & Bicycle>) { 
  return {
    name: item.name,
    charge: item.charge,
    inMaintenance: item.inMaintenance
  };
}

おわり

以上でおわりです。
MDN のドキュメントを読んだだけでは「ふーん、そんな書き方できるんだ」とあまり新しい発見はなさそうに思ったのですが、TypeScript で書いてみると MDN のサンプルコードを書き換えないとコンパイルが通らない部分があったり、なかなかいろんな発見があって面白かったです。

これで「分割代入...知らない
|ω・`)」状態からは抜け出せそうです!

51
32
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
51
32

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?