Help us understand the problem. What is going on with this article?

TypeScriptでIDに型をつけて型安全にする

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に型チェックを追加するのが大体のケースでうまくいき、オーバーヘッドも少なそうです。

types.ts
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もちゃんと動作します。

トランスパイル後

types.js
"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上では扱われるようになります。

これを使って実際コードを書いてみると

main.ts
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で補完や型の表示も効くので色々と捗ります。

chimerast
永遠のNEET スキマ妖怪 意識低い系ITエンジニア チート系異世界転生志望→成功 #プログラミングチョットデキル #駆け走るエンジニア
uzabase
企業活動の意思決定を支える情報インフラの提供
https://www.uzabase.com/
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away