IDを型安全にするとは?
各種IDを整数型や文字列型のまま扱うと、関数の引数に複数種類のIDを指定する時などに、順番を間違えて逆にしてしまってもそのままコンパイルが通ってしまうため、テスト時や実行時に不思議な動作をして、問題に気づくということがよくあります。
そのため、JavaやKotlinだと、データクラスやLombok等を使って、IDに型をつけてコンパイル時にこのようなミスに気づけるようにします。
data class EntityId(val id: Int)
class Entity {
val id: EntityId;
/* ... */
}
TypeScriptでの問題
TypeScriptでclassを使って同じようなことをしようとすると、比較の時やMapのキーとして使用した時に、同じIDを表しているオブジェクトでもオブジェクトインスタンスが違うため、比較が偽になってしまい使い物になりません。仕様でequals()やhashCode()が決められている訳でもないので、クラスでラップする方法だとやりようがありません。
ではTypeScriptでどうするか?
色々調べて試した結果、Intersection Typesを使ってnumberやstringに型チェックを追加するのが大体のケースでうまくいき、オーバーヘッドも少なそうです。
declare class Id<T extends string> {
private IDENTITY: T;
}
export type EntityId = Id<'Entity'> & number;
export function EntityId(id: number): EntityId { return id as EntityId; }
export type AttributeId = Id<'Attribute'> & number;
export function AttributeId(id: number): AttributeId { return id as AttributeId; }
export type CategoryId = Id<'Category'> & number;
export function CategoryId(id: number): CategoryId { return id as CategoryId; }
typeとfunctionを同じ名前で定義していますが、コンパイルは通りますし、exportもちゃんと動作します。
トランスパイル後
"use strict";
exports.__esModule = true;
function EntityId(id) { return id; }
exports.EntityId = EntityId;
function AttributeId(id) { return id; }
exports.AttributeId = AttributeId;
function CategoryId(id) { return id; }
exports.CategoryId = CategoryId;
Id<T>
の型情報が完全に消えていて、普通のNumber型としてJS上では扱われるようになります。
これを使って実際コードを書いてみると
import { EntityId, AttributeId } from './types';
const entityId1: EntityId = EntityId(1);
const entityId1_1: EntityId = EntityId(1);
const entityId2: EntityId = EntityId(2);
const attributeId1: AttributeId = AttributeId(1);
console.log(entityId1 === entityId1_1); // output: true
console.log(entityId1 === entityId2); // output: false
console.log(entityId1 === attributeId1); // Compile error!!!
let entityIdMutable: EntityId;
entityIdMutable = entityId1; // OK
entityIdMutable = entityId2; // OK
entityIdMutable = attributeId1; // Compile error!!!
entityIdMutable = 1; // Compile error!!!
function getEntity(entityId: EntityId) { /* ... */ }
getEntity(entityId1); // OK
getEntity(EntityId(1)); // OK
getEntity(attributeId1); // Compile error!!!
getEntity(1); // Compile error!!!
追記 (2019/11/05)
Array<number>
のようにリテラル型を型引数に指定したオブジェクトには、要素の追加ができてしまいます。Array<EntityId>
のようにちゃんと型引数にも型を指定して使ってください。
const numbers = new Array<number>();
numbers.push(EntityId(1)); // 通っちゃう
numbers.push(AttributeId(1)); // 通っちゃう
const entityIds = new Array<EntityId>();
entityIds.push(EntityId(1)); // OK
entityIds.push(AttributeId(1)); // ERROR
entityIds.push(1); // ERROR
最後に
通常のプログラムを書く時だと、これくらいで十分かなと思っています。
IDに型をつけると、IDEやVS Codeで補完や型の表示も効くので色々と捗ります。