[TypeScript]型の基本とイミュータブルな追加・更新・削除 ~ 際限なき型地獄 ~
今回はTypeScriptの型の基本を扱っていきたいと思います。練習用の題材として、データ操作の基本中の基本、追加・更新・削除を行う関数をイミュータブルの形で実装していきます。
前提条件
- イミュータブルの形を崩さないように、狂ったようにreadonly
一度生成したオブジェクトの書き換えは許さない - interfaceにはプライマリーキー用とデータの用のインターフェースを用意
データの構造は後から変えられるようにする - addItemは自動的にidを採番するため、入力データはidがあっても無くても通るようにする
もちろん他に関係ないプロパティが来たらエラー - updateItemはidが必須、それ以外のデータは任意
送られてきたプロパティだけ置き換え - deleteItemはidが必須
それ以外のデータは任意で関連プロパティはあってもエラーにはしない
この条件を満たすための型設定を各所で行います
ソースコード
//主キーを含むinterface
interface Primary {
readonly id: number;
}
const addItem = <T extends Primary>(
items: readonly T[],
item: Omit<T, keyof Primary> & Partial<Primary>
) => {
const id = items.reduce((max, item) => Math.max(max, item.id), 0) + 1;
return [...items, { id, ...item }];
};
const replaceItem = <T extends Primary>(
items: readonly T[],
item: Partial<T> & Primary
) => {
return items.map(v => {
return v.id === item.id ? { ...v, ...item } : v;
});
};
const delItem = <T extends Primary>(items: readonly T[], item: Partial<T> & Primary) => {
return items.filter(v => {
return v.id !== item.id;
});
};
//データ用interface
interface Weapon extends Primary {
readonly name: string;
readonly cost: number;
}
//初期データ
let weapons: readonly Weapon[] = [
{ id: 1, name: "竹槍", cost: 10 },
{ id: 2, name: "棍棒", cost: 40 },
{ id: 3, name: "銅の剣", cost: 120 }
];
console.log("------------ 元データ ------------------");
console.log(JSON.stringify(weapons, null, " "));
console.log("----------- 追加(鋼の剣) ---------------");
weapons = addItem(weapons, { name: "鋼の剣", cost: 1000 });
console.log(JSON.stringify(weapons, null, " "));
console.log("---------- 更新(竹槍値上げ)-------------");
weapons = replaceItem(weapons, { id: 1, cost: 10000 });
console.log(JSON.stringify(weapons, null, " "));
console.log("----------- 削除(棍棒) -----------------");
weapons = delItem(weapons, { id: 2 });
console.log(JSON.stringify(weapons, null, " "));
実行結果
------------ 元データ ------------------
[
{
"id": 1,
"name": "竹槍",
"cost": 10
},
{
"id": 2,
"name": "棍棒",
"cost": 40
},
{
"id": 3,
"name": "銅の剣",
"cost": 120
}
]
----------- 追加(鋼の剣) ---------------
[
{
"id": 1,
"name": "竹槍",
"cost": 10
},
{
"id": 2,
"name": "棍棒",
"cost": 40
},
{
"id": 3,
"name": "銅の剣",
"cost": 120
},
{
"id": 4,
"name": "鋼の剣",
"cost": 1000
}
]
---------- 更新(竹槍値上げ)-------------
[
{
"id": 1,
"name": "竹槍",
"cost": 10000
},
{
"id": 2,
"name": "棍棒",
"cost": 40
},
{
"id": 3,
"name": "銅の剣",
"cost": 120
},
{
"id": 4,
"name": "鋼の剣",
"cost": 1000
}
]
----------- 削除(棍棒) -----------------
[
{
"id": 1,
"name": "竹槍",
"cost": 10000
},
{
"id": 3,
"name": "銅の剣",
"cost": 120
},
{
"id": 4,
"name": "鋼の剣",
"cost": 1000
}
]
解説
- AddItemでやっている型定義
item: Omit<T, keyof Primary> & Partial<Primary>
//Omit<T, keyof Primary> //TからPrimary(id)を除去
//& Partial<Primary> //idを任意にして結合
- replaceItem、deleteItemでやっている型定義
item: Partial<T> & Primary
//Partial<T> //Tのプロパティを任意にする
//& Primary //Primary(id)を必須にして結合
地獄
たかが三種類の基本データ操作を行うだけで、どれだけ型の構造を書いたら良いのでしょうか?たぶんまだまだ抜け落ちがあることでしょう。extendsの三項演算子も使いたかったのですが、今回は出番がありませんでした。ということで、まだまだ型の深淵には程遠い状態です。
今回は普通にletを使っていますが、let禁止、any禁止、readonly必須の縛りプレイでプログラムを組んだら、地獄どころかオーバーキルで昇天し、気がついたら天国へ逝けるかもしれません。
私には天国は早すぎるので、しばらくは地獄の住人として暮らしていきたいと思います。