21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 3 years have passed since last update.

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

Last updated at Posted at 2019-03-21

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

21
16
1

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
21
16

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?