[PR] 本記事にはプロモーションが含まれています。

【TypeScript】文字列のデータをオブジェクトの配列に変える処理を、for文に戻して理解する|split・filter・map の流れ

TypeScript

カンマ区切りの文字列を、扱いやすいオブジェクトの配列に変える処理(パース)は、TypeScriptやJavaScriptでよく書く処理の1つです。split、filter、map を . でつなげると短く書けますが、慣れないうちは途中でデータがどうなっているのかが見えにくいところです。

私はこの書き方をTypeScriptの入門書(紹介記事)で知りました。組み込みのC言語なら、strtok で区切って atoi で数値にし、構造体の配列に詰めていくところです。その流れと比べられるように、この記事では短いコードを一度 for文の書き方に戻し、途中のデータの変化を追います。

コードは、ブラウザでTypeScriptを動かせるTypeScript Playgroundに貼れば、そのまま試せます。

今回のお題:商品の一覧を配列にする

「商品名,価格,在庫(1ならあり、0ならなし)」の形で、1行に1商品ずつ書いた文字列があるとします。これを Product 型の配列に変えます。

TypeScript
type Product = {
  name: string;
  price: number;
  inStock: boolean;
};

const data = `
りんご,120,1
みかん,80,0
ぶどう,300,1
`;

バッククォートで囲んだ文字列(テンプレートリテラル)は、改行をそのまま含められます。最初のバッククォートの直後と、最後の行の後ろにも改行があるのが、後で効いてきます。テンプレートリテラルについては「文字列のクォートの違いとテンプレートリテラル」で紹介しています。

短く書いたコード

まず、今回読み解く短いコードです。

TypeScript
const products: Product[] = data
  .split("\n")
  .filter(line => line !== "")
  .map(line => {
    const [name, priceText, stockText] = line.split(",");
    return {
      name,
      price: Number(priceText),
      inStock: stockText === "1",
    };
  });

これで products には、商品3つ分のオブジェクトが入ります。このコードが何をしているかを、for文に戻して確かめます。

for文に戻したコード

同じ処理を、for...of で1行ずつ処理する形に書き直します。手順に番号を付けました。

TypeScript
// 【準備】結果を入れる空の配列を用意する
const products: Product[] = [];

// 【手順1】改行で区切って、行の配列にする
const lines = data.split("\n");

// 【手順2】行を1つずつ取り出す
for (const line of lines) {
  // 【手順3】空の行は飛ばす
  if (line === "") {
    continue;
  }

  // 【手順4】カンマで区切って、項目ごとに取り出す
  const [name, priceText, stockText] = line.split(",");

  // 【手順5】型を変換してオブジェクトを作り、配列に追加する
  products.push({
    name,
    price: Number(priceText),
    inStock: stockText === "1",
  });
}

C言語に近い、「空の配列を用意して、1行ずつ調べて、条件に合うものを追加していく」書き方です。const [name, priceText, stockText] = ... は分割代入という書き方で、配列の1番目・2番目・3番目をそれぞれの変数に入れています。

短いコードとの対応と、データの変化

for文の各手順が、短いコードのどこに当たるかを、途中のデータと一緒に並べます。

TypeScript
const products: Product[] = data
  // 【手順1】改行で区切る(for文の lines = data.split("\n") と同じ)
  .split("\n")
  // → ["", "りんご,120,1", "みかん,80,0", "ぶどう,300,1", ""]
  //    最初と最後の "" は、` の直後と最後の改行からできた空の行

  // 【手順3】空の行を取り除く(for文の if (line === "") continue と同じ)
  .filter(line => line !== "")
  // → ["りんご,120,1", "みかん,80,0", "ぶどう,300,1"]

  // 【手順2・5】行を1つずつオブジェクトに変えて、新しい配列を作る
  //   (for文のループと push に当たる)
  .map(line => {
    // 【手順4】カンマで区切る
    const [name, priceText, stockText] = line.split(",");
    // 例:"りんご,120,1" → ["りんご", "120", "1"]

    // 【手順5】型を変換してオブジェクトを作る
    return {
      name,
      price: Number(priceText),    // "120" → 120(文字列から数値へ)
      inStock: stockText === "1",  // "1" → true、"0" → false
    };
  });
// → [
//     { name: "りんご", price: 120, inStock: true },
//     { name: "みかん", price: 80, inStock: false },
//     { name: "ぶどう", price: 300, inStock: true }
//   ]

対応をまとめると、次のようになります。

for文の書き方短いコードやっていること
const lines = data.split("\n").split("\n")改行で区切って、行の配列にする
if (line === "") continue;.filter(line => line !== "")空の行を取り除く
for (const line of lines) と products.push(...).map(line => { ... })1行ずつ変換して、新しい配列を作る
const products: Product[] = [](なし)map が新しい配列を作って返すので、空の配列はいらない

filter は「条件に合う要素だけ残した配列」、map は「要素をそれぞれ変換した配列」を作ります。filter の詳しい書き方は「filterメソッドの書き方」、line => ... の書き方は「アロー関数(=>)とは?」で紹介しています。

配列にしたデータを使う

配列にしておくと、あとの処理が書きやすくなります。商品ごとに在庫の有無を表示してみます。

TypeScript
for (const product of products) {
  const stock = product.inStock ? "在庫あり" : "在庫なし";
  console.log(`${product.name}(${product.price}円):${stock}`);
}

// りんご(120円):在庫あり
// みかん(80円):在庫なし
// ぶどう(300円):在庫あり

型を付けておくと、書き間違いに気づける

const products: Product[] の : Product[] は「この変数には Product の配列が入る」という宣言です。実行したときの動きは変わりませんが、map の中で作るオブジェクトの形が Product と合わないと、コンパイルのときにエラーで教えてくれます。

TypeScript
const products: Product[] = data
  .split("\n")
  .filter(line => line !== "")
  .map(line => {
    const [name, priceText, stockText] = line.split(",");
    return {
      name,
      price: Number(priceText),
      instock: stockText === "1", // inStock の S を小文字にした書き間違い → コンパイルエラー
    };
  });

プロパティ名の小さな書き間違いも、実行する前に見つかります。C言語で構造体のメンバー名を間違えるとコンパイルエラーになるのと似た安心感があります。

実際のデータで気をつけたいこと

Windowsで作ったデータは改行が \r\n

Windowsで作ったテキストファイルやCSVは、改行が \r\n になっていることがあります。split("\n") だけだと各行の最後に \r が残り、最後の項目が "1\r" になるので、stockText === "1" が false になってしまいます。エラーにはならず、結果だけが間違うので気づきにくい不具合です。

TypeScript
// \r\n と \n のどちらでも区切れるように、正規表現で指定する
const lines = data.split(/\r?\n/);

数値にできないときは NaN になる

Number("abc") のように数値にできない文字列を渡すと、エラーにはならず NaN(数値ではないことを表す値)になります。また、Number("") は 0 になるので、価格が空欄だと 0 円として扱われます。外から読み込んだデータを使うときは、Number.isNaN() などで確かめておくと安心です。

TypeScript
const price = Number(priceText);
if (Number.isNaN(price)) {
  console.log(`価格が数値にできません:${priceText}`);
}

なお、本格的なCSVファイルは、項目の中にカンマや改行を含められる("で囲む)など、split(",") だけでは正しく分けられないことがあります。そのようなデータを扱うときは、CSV用のライブラリを使うのが確実です。

まとめ

  • split → filter → map の短いコードは、「改行で区切る → 空の行を飛ばす → 1行ずつ変換して配列に入れる」というfor文の処理と同じ
  • map が新しい配列を作って返すので、空の配列を用意して push する必要がない
  • 分かりにくいときは、途中のデータをコメントに書き出すか、for文に戻してみると流れが追いやすい
  • 型(Product[])を付けると、プロパティ名の書き間違いをコンパイルのときに見つけられる
  • 実際のデータでは、\r\n の改行と NaN に気をつける
ほかの開発・プログラミングの記事は、開発・プログラミングメモの目次から、テーマ別に探せます。

このブログは、にほんブログ村と人気ブログランキングに参加しています。記事が役に立ったら、下のランキングのバナーのどれかを押して応援してもらえると励みになります。

ブログランキング・にほんブログ村へにほんブログ村

サーバー構築・運用ランキング プログラムランキング

TypeScript
watasoftdevをフォローする

コメント