この記事は、 @rithmety さんのコメントで速攻解決します。
よって、下記の記事は「こいつ、なんだかぁ〜」レベルになってます。(笑)
ことの発端
ちょっと現実逃避をしたくて、ウェブサーフィン(死語)していたところScala.js
があることを知りました。
このページには「今までこう書いてたけど、Scala.js
ならこう書ける」っていう比較がありまして。。。
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}`);
}
}
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() がダメなのか
Object
と Map
の違いは下記のとおり。
つまり、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
のプロパティはstring
かsymbol
のみなので、しょうがないです。
で、更に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
の取り扱いを調べないとダメだな。。。(続くかも)