TypeScriptで型名で型チェックする

## Strutual TypingとNominal Typing

TypeScriptは構造的型付け（Structual Typing）を行う言語です。これは構造的に同じ形の型なら互換性があるとみなすということです。例えば、次の例を考えます。

```typescript
type Book = {
  name: string
}

type Person = {
  name: string
}

let book = {name: 'Effective TypeScript'}
let person:Person = book // BookとPersonは同じ構造なのでコンパイルが通る
```

このコードはコンパイルが通ります。BookとPersonはどちらも「string型のnameをメンバーにもつ」という同じ構造をしているからです。

一方で、JavaやC++などでは、同様なコードを書いてもコンパイルは通りません。これらの言語は構造が同じでも継承関係のない型同士は互換性がないからです。このような方式を名前的型付け（Nominal Typing、公称型）といいます。

Structual TypingとNominal Typingは、どちらが良いというわけではなく、それぞれにメリット、デメリットがあります。より厳密な型チェックが行えるという点では、Nominal Typingに利点があります。Structual Typingは型チェックはやや緩くなりますが、Duck Typingが型セーフに行えるというメリットがあります。またTypeScriptはJavaScriptにあとから静的型定義を導入した言語なので、Nominal Typingにしてしまうと、JavaScriptとの相互運用などに支障がでてしまいます。

TypeScriptがStructual Typingとは言っても、状況によっては、Nominal Typingのような名前による型チェックを行えたほうが便利な場合もあるでしょう。もし先程の例が、DDDでいうところのValue Objectのようなものであった場合、BookとPersonのように同じ構造であっても意味が違うものには互換性を持たせたくはありません。

この記事では、Type ScriptでNominal Typingを実現する方法を紹介します。

## 結論

まず最終形態を示してから、解説をします。

再利用可能なユーティリティとして以下のような関数を用意します。
```typescript
export default function val<T>(value: Omit<T, '_brand_'>) {
  return value as T
}
```

これを使って最初の例を以下のように書き換えます。

```typescript
import val from './val'

type Book = {
  name: string
} & { readonly _brand_: unique symbol}

type Person = {
  name: string
} & { readonly _brand_: unique symbol}

let book = val<Book>({name: 'Effective TypeScript'})
let person:Person = book // BookとPersonは（_brand_以外は）構造が同じだがコンパイルエラー
```

これでBookとPersonは非互換になります。またBookにメンバーが追加になったときには、インスタンス生成時に追加のメンバーも初期化しないとコンパイルエラーになります。

```typescript
import val from './val'

type Book = {
  name: string
  price: number
} & { readonly _brand_: unique symbol}

let book = val<Book>({name: 'Effective TypeScript'}) // コンパイルエラー
```

## 型定義

まず最終形態のうち型定義の部分からみます。

```typescript
type Book = {
  name: string
} & { readonly _brand_: unique symbol}

type Person = {
  name: string
} & { readonly _brand_: unique symbol}
```

この時点で、BookとPersonは非互換になります。それぞれ固有の型を持つ_brand_をメンバーに持つからです。
_brand_を、nameと同列のメンバーとして並べてもよいのですが、交差型として分離しておくと、stringなど既存の型に対しても使えるので便利です。

```typescript
type BookName = string & { readonly _brand_: unique symbol}
type PersonName = string & { readonly _brand_: unique symbol}

let bookName:BookName = ...
let personName: PersonName = bookName // BookNameとPersonNameは非互換なのでコンパイルエラー
```

## 型アサーションによるインスタンス化

次はインスタンスの作成です。残念ながら元から必要だったメンバーだけ初期化してもコンパイルエラーになります。追加した_brand_をメンバーに持たないからです。

```typescript
type Book = {
  name: string
} & { readonly _brand_: unique symbol}

let book:Book = {name: 'Effective TypeScript'} // _brand_がないのでコンパイルエラー
```

自明な解決方法としては、型アサーションによる解決があります。

```typescript
let book:Book = {name: 'Effective TypeScript'} as Book
```

この方法だとBookにメンバーが追加になったとしてもコンパイルが通ってしまうという欠点があります。

```typescript
type Book = {
  name: string
  price: number
} & { readonly _brand_: unique symbol}

let book:Book = {name: 'Effective TypeScript'} as Book // priceがないのにコンパイルが通る
```

## ユーティリティー関数による型チェック

そこでインスタンス生成時の型チェックを行うために以下のような関数を用意します。

```typescript
export default function val<T>(value: Omit<T, '_brand_'>) {
  return value as T
}
```

``Omit<T, '_brand_'>`` は与えられた型Tから、指定したメンバー ``_brand_`` を取り除いた型を返します。
つまり関数``val<T>(value)``は、valueが``_brand_``以外のメンバーをすべて持っていることをチェックします。

  これで以下のようなインスタンス化はエラーになります。

```typescript
type Book = {
  name: string
  price: number
} & { readonly _brand_: unique symbol}

let book:Book = val<Book>({name: 'Effective TypeScript'}) // priceがないのでコンパイルエラー
```

これで、BookとPersonは同じ構造を持ちつつ、インスタンス生成時の型チェックもできるようになります。

## まとめ

``_brand_``や``val<Book>``のような余計なものが多少は入ってしまいましたが、一応、TypeScriptでもNominal Typingのようなことができるようになりました。

※著者はTypeScript初心者なので間違っていたらすみません。

## 参考

この記事を書くのに以下を参考にしました。

https://qiita.com/suin/items/ae9ed911ebab48c98835

https://qiita.com/suin/items/35c3953254255d24a193

https://typescript-jp.gitbook.io/deep-dive/main-1/nominaltyping

https://iwasiman.hatenablog.com/entry/20210712-programming-typescript#%E6%97%A5%E6%9C%AC%E8%AA%9E%E3%81%A7%E8%AA%AD%E3%82%81%E3%82%8BTypeScript%E3%81%AE%E6%9C%AC