TypeScript で number 型を unique symbol で区別させる + ほのかな注意点

他の言語には新しい型にするだけで区別してくれるものもありますが、TypeScript ではなんらかの一手間必要です。

TypeScript は似た型を同じように扱ってしまう

関数にまちがった数値 ID を渡したくない局面があるとします。そこで違うクラスとして定義してみても、コンパイラはエラーにしません。

class ProductId extends Number {}
class CategoryId extends Number {}

const productId = new ProductId(1)
const categoryId = new CategoryId(1)

function findProduct(id: ProductId): any {}

findProduct(productId)
findProduct(categoryId) // 通る
findProduct(1)          // 通る

TypeScript Playground

unique symbol を与えるテクニック

そこでユニークシンボルを名として取る void フィールドを定義するとコンパイラーが区別するようになるテクニックがあります。

declare const ProductIdtype: unique symbol;
declare const CategoryIdType: unique symbol;

class ProductId extends Number {
    [ProductIdtype]: void
}

class CategoryId extends Number {
    [CategoryIdType]: void
}

const productId = new ProductId(1)
const categoryId = new CategoryId(1)

function findProduct(id: ProductId): any {}

findProduct(productId)
findProduct(categoryId) // コンパイルエラー
findProduct(1)          // コンパイルエラー

class 部のコンパイル結果は以下のようになります。

class ProductId extends Number {
}
class CategoryId extends Number {
}

純粋にコンパイルタイムだけに活躍するので便利ですね。

TypeScript Playground

便利

もとが number なので、number のように扱えて便利な局面があります。

class A {}

console.log(`product id ${productId}`) // "product id 1"
console.log(`class ${new A()}`)        // ”object [object Object]"

これは例えばリクエスト時の URL 組み立てに使えます。

注意

もとは number ですが、異なる class なので number とは異なるふるまいもあるので注意が必要です。

const id = 1
const productId = new ProductId(id)
const anotherId = new ProductId(id)

console.log(productId === anotherId) // 同じ 1 だが false
console.log(productId + anotherId)   // コンパイルエラー

以下のようにすれば意図通りのふるまいをします。

console.log(productId.valueOf() === anotherId.valueOf()) // true
console.log(productId.valueOf() + anotherId.valueOf())   // 2

まとめ

TypeScript で明確に区別したいクラスを定義したいときはお手軽で便利なテクニックですね。

実際に使ってみて軽く小指をぶつけたので、注意点と併せて紹介しました。