公称型をTypeScriptで実現するための基礎

先日、[異なる2つの型システム「公称型」と「構造的部分型」](https://qiita.com/suin/items/52cf80021361168f6b0e)という投稿をして、TypeScriptの型システムは、JavaやPHPと全然違うという話をしました。

TypeScriptは「構造的部分型」と言って、オブジェクトの構造が同じなら互換したものとしてみなされるので、意味的に全く関係ないオブジェクト同士を入れ替えて扱うこともできるわけです。

例えば、たまたま同じ`name`プロパティを持っている`Book`型と`Person`型があったとき、

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

TypeScriptではこの2つの型を区別しないんです。なので、`Book`型の変数に`Person`型の変数を代入してもコンパイルエラーになりません。

```typescript
const bob: Person = { name: 'Bob' }
const book: Book = bob
```

これは、JavaやPHPのような公称型の言語になれていると、気になってしまうところです。公称型のプログラミングでは、互換性を断ち切るため、つまり、誤った代入をしてしまわないために、わざわざ名前をつけるような側面もあるわけだからです。

本稿では公称型(nominal typing)をTypeScriptで実現するための基礎について説明していきたいと思います。

## TypeScriptでも公称型を実現したい……！

TypeScriptの構造的部分型は柔軟性があってJavaScriptをあつかうには適切な型システムだと思いますが、一方で公称型のようにクラス名やインターフェイス名で厳密に区別されるような仕掛けが欲しくなることも、しばしばあるかと思います。

理想を言えば、`nominal`修飾子のようなものがTypeScriptにあって、手軽に公称型を実現できたら良かったのですが。

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

現実は無いので型レベルプログラミングで努力することになります。


### よくある公称型の実現方法

TypeScriptでの公称型のテクニックとしては、型ごとに微妙に構造を変えるというのが基本です。

`hoge`でも`fuga`でも何でもいいので、適当なプロパティを1つ追加して、全く同じ構造にならないようにするのがポイントです。

```typescript
type Book = { name: string, hoge: any }
type Person = { name: string, fuga: any }
```

ただこれだと、うっかりプロパティ名がかぶって、同じ構造になってしまう事故が無いわけではないです。より安全にするために、シンボルを使うのが定石です。シンボルは絶対にかぶらないので安心できます。

```typescript
const bookNominality = Symbol()
type Book = { name: string, [bookNominality]: any }
const personNominality = Symbol()
type Person = { name: string, [personNominality]: any }
```

上で定義した定数のシンボルは、実行時には不要なので、コンパイル後のJavaScriptコードに残らないよう、`declare const`に置き換えてしまえば、実用レベルの実装にはなります。

```typescript
declare const bookNominality: unique symbol
type Book = { name: string, [bookNominality]: any }

declare const personNominality: unique symbol
type Person = { name: string, [personNominality]: any }
```

最後に、万が一、

```typescript
book[bookNominality] = '...'
```

のような誤った代入や、

```typescript
return book[bookNominality]
```

のような誤った値の利用など、どのうっかりミスを防ぐためにも、シンボルの型を`never`にしておけばさらに安心です。

```typescript
type Book = { name: string, [bookNominality]: never }
                                              ^^^^^
```

これで、`book[bookNominality]`に代入しようとしたら即コンパイルエラーになり、`book[bookNominality]`の値を参照してどこかに渡そうとしてもコンパイルエラーを起こしやすくなります。

### どうやって`Book`型の変数を作るの？

公称型チックな`Book`型を定義することはできたのですが、その型の変数を作るにはどうしたらいいのでしょうか？

こうかな……？

```typescript
type Book = { name: string, [bookNominality]: never }
const book: Book = { name: 'オライリー' }
```

これはコンパイルエラーになります。`[bookNominality]`プロパティが足りないからです。

では、こう……？

```typescript
const book: Book = { name: 'オライリー', [bookNominality]: undefined }
```

これもだめです。`[bookNominality]`は`never`型なのでどんな値も代入できません。

やるならこうです。

```typescript
const book: Book = {
  name: 'オライリー', 
  [bookNominality]: undefined as never 
}
```

これで一応コンパイルは通るのですが、生成されたJavaScriptコードを見ると、

```javascript
const book = { name: 'オライリー', [bookNominality]: undefined };
```

のようになっています。実行時には`bookNominality`はデータとして不要ですし、`bookNominality`の変数定義は`declare`文で書いたのでコンパイル時には消えています。なので、変数未定義エラーが実行時に発生してしまいます。

```
ReferenceError: bookNominality is not defined
```

この実行時の問題を解決するためには、`bookNominality`をプロパティに与えずに、`Book`オブジェクトを作る必要があります。

なので、こうするのがテクニックとしては定番です。

```typescript
const book: Book = { name: 'オライリー' } as Book
```

これにも1つ難点があって、`Book`型の構造の変化に追従しずらいという問題があります。例えば、`Book`型に`price`プロパティが追加されたとしても、`as Book`があるところはコンパイルで構造の不一致に気づくことができません。

```typescript
type Book = {
  name: string
  price: number
　[bookNominality]: never
}

const book: Book = { name: 'オライリー' } as Book // コンパイル通ってしまう
```

もう少しできることとしては、`Book`オブジェクトを作る関数を用意して、それを使うことを強制することです。

```typescript
// book.ts
declare const bookNominality: unique symbol // これはexportしない

export type Book = {
  name: string
  price: number
　[bookNominality]: never
}

export function newBook(name: string): Book {
  return {name: name} as Book
}

// 利用者側のコード.ts
import {Book, newBook} from './book'
const book: Book = newBook('オライリー')
```

こうすれば、利用者は`newBook`を使わないと`Book`オブジェクトを作れないので、`Book`型の構造変化に追従しやすくなります。

それでもなお、`newBook`の実装では`as Book`があるため、そこの実装だけは構造の変化に注意して直していく必要があるのには変わりがありません。

できれば、`as`を使わなくてもいいコードにできればいいのですが……。

そう思いながら試行錯誤した結果、型レベルプログラミングで解決する方法を思いついたので、次回はそのことについて書きたいと思います。

次回 → [公称型をTypeScriptで《より型安全》に実現する方法 - Qiita](https://qiita.com/suin/items/35c3953254255d24a193)

---

最後までお読みくださりありがとうございました。Twitterでは、Qiitaに書かない技術ネタなどもツイートしているので、よかったらフォローお願いします:relieved:→[Twitter@suin](https://twitter.com/suin)