🧅
ユニオン型をkeyにもvalueにも持ったオブジェクト型(?)を作りたい
謎タイトルだけど伝わってほしい。
TypeScriptと型バトルしたときに得た知見を共有します。
やりたいこと
以下のようにUnionKey
をkeyにもvalueにも持つユニオンなオブジェクト型を作りたくなりました。
type UnionKey = "aaa" | "bbb";
type UnionItem = {
id: "aaa";
aaa: Record<string, unknown>;
} | {
id: "bbb";
bbb: Record<string, unknown>;
}
手を動かしてみる
最初はとりあえずMapped Typesを使ってみましたがうまくいかず…
type UnionItem = {
id: UnionKey;
} & {
[K in UnionKey]: Record<string, unknown>;
}
const item: UnionItem = {
id: "aaa",
aaa: { hoge: "fuga" },
bbb: { piyo: "payo" }, // id:"aaa"なのにbbbがないといけない
}
Mapped Typesの箇所をOptionalにすればまあまあうまくいきますが、使う側でいろいろ気を使わないといけないので微妙でした。
type UnionItem = {
id: UnionKey;
} & {
[K in UnionKey]?: Record<string, unknown>;
}
const item: UnionItem = {
id: "aaa",
aaa: { hoge: "fuga" }, // OK
}
// 何かしらの処理
const consoleUnionItem = (item: UnionItem[UnionKey]) =>
console.log(item);
if (item.id === "aaa") {
// コンパイルエラー
consoleUnionItem(item.aaa);
// 通りはするがid:"aaa"ならitem.aaaは保証されていたい
consoleUnionItem(item.aaa ?? { hoge: "fuga default" });
}
できたもの
いろいろ書き直したりChatGPTに聞いたりしていたところ、中間にUnionItemMapを作成することで要件を満たすことができました。
type UnionItemMap<T extends UnionKey> = {
id: T
} & {
[K in T]: Record<string, unknown>;
};
type UnionItem = {
[K in UnionKey]: UnionItemMap<K>;
}[UnionKey];
UnionItemMapは、渡されたジェネリクスに合ったオブジェクト型を返す型です。
展開すると以下のようになります。
type UnionItemMap<"aaa"> = {
id: "aaa";
aaa: Record<string, unknown>;
}
type UnionItemMap<"bbb"> = {
id: "bbb";
bbb: Record<string, unknown>;
}
最後にUnionKeyをkeyに持つオブジェクトに対してユニオン型が分配され、求めていた型を作ることができました!
type UnionItem = {
aaa: UnionItemMap<"aaa">;
bbb: UnionItemMap<"bbb">;
}["aaa" | "bbb"];
// ↓
type UnionItem = UnionItemMap<"aaa"> | UnionItemMap<"bbb">;
// ↓
type UnionItem = {
id: "aaa";
aaa: Record<string, unknown>;
} | {
id: "bbb";
bbb: Record<string, unknown>;
};
さいごに
今回はなんとかなりましたが、まだまだTypeScript知らないことだらけなのでどこかでちゃんと勉強したいです。TypeScriptとの型バトルは続く…
参考
Discussion
この定義を達成したいと考えたユースケースをつかめていないのですが、やりたいことみて、ぼくもちょっとやってみました。
template-literal-types使えば途中は省けるんじゃないかと思いました。
demo code.
ありがとうございます!
確かにこちらの方が記述がシンプルになって嬉しいですね!
参考にさせていただきます🙌