LoginSignup
0
1

More than 1 year has passed since last update.

Map<K,V> から [K, V][] (タプルの配列)に変換する

Last updated at Posted at 2023-01-27

この記事は、 @rithmety さんのコメントで速攻解決します。
よって、下記の記事は「こいつ、なんだかぁ〜」レベルになってます。(笑)

ことの発端

ちょっと現実逃避をしたくて、ウェブサーフィン(死語)していたところScala.jsがあることを知りました。

このページには「今までこう書いてたけど、Scala.jsならこう書ける」っていう比較がありまして。。。

TypeScript
class Person {
  constructor(public firstName: string,
      public lastName: string) {
  }

  fullName() {
    return `${this.firstName} ${this.lastName}`;
  }
}

const personMap = new Map<number, Person>([
  [10, new Person("Roger", "Moore")],
  [20, new Person("James", "Bond")]
]);

const names = new Array<string>();
for (const [key, person] of personMap) {
  if (key > 15) {
    names.push(`${key} = ${person.firstName}`);
  }
}
Scala.jsだとこんなにシンプル!
class Person(val firstName: String, val lastName: String) {
  def fullName(): String =
    s"$firstName $lastName"
}
val personMap = Map(
  10 -> new Person("Roger", "Moore"),
  20 -> new Person("James", "Bond")
)
val names = for {
  (key, person) <- personMap
  if key > 15
} yield s"$key = ${person.firstName}"

いや〜、Scala.jpを候補としている人が、今どきTypeScriptでこんな書き方しないでしょと思い、リファクタリングをしてみました。

const name = Object.keys(personMap)// こいつが string[] になりやがるんだよね~~
  .map(key => parseInt(key)) // あ。。。 う〜〜ん。。。
  .filter(key => key > 15)
  .map(key => `${key} = ${personMap.get(key)}`); // personMap.get(key) は string | undefined なんだよなぁ。。。

あ〜、そうそう!
そうだったわ。。。
TypeScriptのイケてないところを見事に突いた絶妙なサンプルだな。
特に連想配列のキーをnumberにするところなんか悪意しか感じないw
(↑ 後段で、そういう問題じゃないということに気付きます)
まぁ、それはそうと、これって何とかならないかな?

で、どうする?

Map<number, Person>[number, Person][] に変換できれば、メソッドチェーンで色々便利そう。

書いてみる

メタプログラミングは久々なのでリハビリがてら書いてみるとする。

function mapToTuples<
  T extends Map<unknown, unknown>,
  K = T extends Map<infer K, unknown> ? K : never,
  V = T extends Map<unknown, infer V> ? V : never
>(map: T): [K, V][] {
  return Object.keys(map).map(idx => [idx as K, map.get(idx) as V]);
}

意外とアッサリ書けた。
で、こいつを使って書き直してみる。

const names = mapToTuples(personMap)
  .filter(x => x[0] > 15)
  .map(x => `${x[0]} = ${x[1].firstName}`)

Scaler.js よりスッキリしていて素敵~(小並感)
まぁ、mapToTuples() は、内部で配列作っるのでメモリのコストを考えると微妙だけど、無理矢理メソッドチェーンで何とか頑張るよりは可読性は高いんじゃないかしら?

と、これで終わりにしようと思って、いざ実行してみると、names[] 空っぽじゃないですか!!

何故 mapToTuples() がダメなのか

ObjectMap の違いは下記のとおり。

つまり、Mapオブジェクトに対して Object.keys() は使えません。
う~ん、何となく過去コードでバグ作ってないかとモヤモヤする。。。

書き直す

簡単に目的の[K, V][]にしても良かったのだけど、map() という高階関数を持つオブジェクトを返却しました。
(ジェネリクスのリハビリ中なので寄り道気味です)

function mapTransform<
  T extends Map<unknown, unknown>,
  K = T extends Map<infer K, unknown> ? K : never,
  V = T extends Map<unknown, infer V> ? V : never
>(src: T): {
    map<
        MAP_FN extends (kv: [K, V]) => unknown,
        MAP_FN_RET = MAP_FN extends (kv: [K, V]) => infer R ? R : never
    >(f: MAP_FN): MAP_FN_RET[]
}{
  return {
    map<
        MAP_FN extends (kv: [K, V]) => unknown,
        MAP_FN_RET = MAP_FN extends (kv: [K, V]) => infer R ? R : never
    >(f: MAP_FN): MAP_FN_RET[] {
        const re = [] as MAP_FN_RET[];
        src.forEach((v, k) => re.push(f([k, v] as [K, V]) as MAP_FN_RET));
        return re;
    }
  }
}

んで、これを使うと、こんな感じになります。

const names = mapTransform(personMap)
    .map(x => x)
    .filter(x => x[0] > 15)
    .map(x => `${x[0]} = ${x[1].firstName}`);

う~ん、やっぱりmapTransform()は素直に [K, V][] を返却しても良かったかな?(苦笑)
なので、再度mapToTuples()という関数名に変更して再実装。

配列を返却することにした(関数名も変更)
function mapToTuples<
  T extends Map<unknown, unknown>,
  K = T extends Map<infer K, unknown> ? K : never,
  V = T extends Map<unknown, infer V> ? V : never
>(src: T): [K, V][]
{
    const re = [] as [K, V][];
    src.forEach((v, k) => re.push([k, v] as [K, V]));
    return re;
}

これなら、こう書けるか。。。

const names = mapToTuples(personMap)
    .filter(x => x[0] > 15)
    .map(x => `${x[0]} = ${x[1].firstName}`);

ついでに Object の変換も作っておく

function objectToTuples<
  T extends {[_ in string | number | symbol]: unknown},
  K = T extends {[_ in infer K]: unknown} ? K : never,
  V = T extends {[_ in string | number | symbol]: infer V} ? V : never
>(src: T): [K, V][] {
    return Object.keys(src).map(k => [k as K, src[k] as V]);
}

んで、こんな感じで使う。

const obj = {
    1: "one",
    2: "two"
};
const x = objectToTuples(obj); // x は [1 | 2, string][]

一見、良さげに見えるんだけど、タプルの1個めの型は 1 | 2 と認識しているんだけど、実体は文字列が入っているという罠があります。
原因はObject.keys()string[]を返却しやがる。。。
というか、Objectのプロパティはstringsymbolのみなので、しょうがないです。

で、更にsymbolをキーとしている場合、Object.keys()Object.entries() では列挙されません。

const s = Symbol();
const o = {
  1: "one",
  0o10: "eight",
  0xA: "ten",
  [s]: "sym",
  s: "str"
};
for (const [key, value] of Object.entries(o)) {
  console.log(`${key}:${typeof key} -> ${value}`);
}
console.log(`symbol ${o[s]}`);
結果
1:string -> one
8:string -> eight
10:string -> ten
s:string -> str
symbol sym

ということで、下記のようにobjectToTuples書き直します。

function objectToTuples<
  T extends {[_ in string | number /*| symbol*/]: unknown},
  V = T extends {[_ in string | number /*| symbol*/]: infer V} ? V : never
>(src: T): [string, V][] {
    return Object.keys(src).map(k => [k, src[k] as V]);
}
const s = Symbol();
const o = {
  1: "one",
  0o10: "eight",
  0xA: "ten",
  [s]: "sym",
  s: "str"
};
const x = objectToTuples(obj); // x は [string, string][]
console.log(x)

あれ?
エラーにならない。。。orz

結果
[["1", "one"], ["8", "eight"], ["10", "ten"], ["s", "str"]]

symbolは暗黙の文字列変換しないが、これはJavaScriptの仕様。

ということは、TypeScriptの型情報としてのsymbolの取り扱いを調べないとダメだな。。。(続くかも)

0
1
4

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
0
1