構造が同じでも違う型に判定してよ！：TypeScript で Nominal Typing


#はじめに

前回の [TypeScriptやばいところ濃縮ジュース](https://qiita.com/yukikaze/items/0da56f591971df947d3f)に引き続き、今回も勉強ノートです。

今回のお題はこちら

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

https://twitter.com/tomokon_0314/status/1434501331771744260?s=20

これをみてざっと検証してみました

##実行環境

`TypeScript 4.5.0-dev.20210903`

##いざ

早速いってみましょう。

まずは公式のサンプルを抜粋します。実装内容は表題の通り、構造が一緒でも名前が違えば「違う型に判定される」パターンを作っていきます。


```typescript
// FOO
interface FooId extends String {
	_fooIdBrand: string; // To prevent type errors
}

// BAR
interface BarId extends String {
	_barIdBrand: string; // To prevent type errors
}

/**
* Usage Demo
*/
let fooId: FooId;
let barId: BarId;

// Safety!
fooId = barId; // error
barId = fooId; // error
fooId = <FooId>barId; // error
barId = <BarId>fooId; // error

// Newing up
fooId = 'foo' as any;
barId = 'bar' as any;

// If you need the base string
let str: string;
str = fooId as any;
str = barId as any;

```

さて、最後の二行。なぜanyにしないといけないのでしょうか。
ついstringにしたくなりますが...?


```typescript

// If you need the base string
let str: string;
str = fooId as any;
str = barId as any;

// Can't do this
str = fooId as string; // error

```

やってみたらエラーになりました。もちろん、stringをstringに代入できないのではなく、「`fooId`をstringにキャストできないよ」と怒られています。サブタイプには型アサーションできないですね。

#型アサーションに必要な条件とは？

なんとなく、`_fooIdBrand`プロパティの存在が重要そうですが、他にいくつかの質問が浮かびます。

1. typeだとどうなる？
1. extendsは？
1. プロパティ名同じで違う型は？

と言うわけで全部やってみました！

# Interfaceで継承・拡張

```typescript


/**
 * More Examples
 */

// extending
interface FooFooId extends FooId {
	_fooFooIdBrand: string; // Not _fooIdBrand
}

// more properties but not extending
interface FooBarId {
	_fooBarIdBrand: string;
	_fooIdBrand: string;
	_barIdBrand: string;
}

fooId as AnotherFooId; // OK
fooId as FooFooId; // ok because FooId is a subtype of FooFooId
fooId as FooBarId; // error, FooId is not a subtype of FooBarId

```

明確に継承したインターフェイスは許容するみたいですね。ここは名前主義的です。しかし同じ構造の違う名前のインターフェイスは受け入れてしまっているので、まだ構造主義的です。

#同じ構造のtypeでfooIdをキャストできるか？

```typescript
/**
 * Same Structures but type
 */ 
type FooIdType = {
	_fooIdBrand: string; // To prevent type errors
}

type AnotherFooIdType = {
	_fooIdBrand: string; // To prevent type errors
}

// Intersection
type FooFooIdType = FooIdType & {
	_fooFooIdBrand: string; // Not _fooIdBrand
}

// more properties but not intersection
type FooBarIdType = {
	_fooBarIdBrand: string;
	_fooIdBrand: string;
	_barIdBrand: string;
}

fooId as FooIdType; // ok. If same structure, type is also accepted
fooId as FooFooIdType; // error, Not extended but intersection
fooId as FooBarIdType; // error
```

同じ構造のタイプは、名前が違っても許容するみたいです。

また、typeには継承の概念がないのでインターセクションだけ試しましたが、そっちはやっぱりダメですね。そこからプロパティを増やしたタイプがダメなのもわかります。
ちなみにユニオンでもダメでした。

# FooIdTypeの型アサーション

```typescript

const fooIdType: FooIdType = "fooId" as any;

str = fooIdType as any; // ok
str = fooIdType as string; // error

fooIdType as AnotherFooIdType; // ok
fooIdType as FooFooIdType; // ok
fooIdType as FooBarIdType; // ok

```

文字列へのアサーションは同じ振る舞いですが、他の類似の型へのアサーションは通ってしまいました。

# typeをinterfaceでアサーション

```typescript
fooIdType as FooId; // ok
fooIdType as FooFooId; // ok
fooIdType as FooBarId; // ok

```

これも全部通ります

# 型アサーション対応表

FooId, FooIdTypeそれぞれに対する型アサーションの結果

`o`アサート受け入れ
`x`型アサーションエラー
`-`実験なし

| 条件 | Type | Interface |Type as Interface| Interface as Type|
|:-:|:-:|:-:|:-:|:-:|
|基準|-|-|o|o|
| サブタイプ|  x | x |-|-|
|別名| o | o |-|-|
|継承| - | o |o|-|
| インターセクション | o | - |-|x|
| ユニオン | o | - |-|-|
|拡張| o | x |o|x|

#まとめ

名前主義パターンを目指したものでしたが、受け入れてしまっているパターンもちらほらですね。
それでもインターフェイスはまだ拒否しているものが多く、名前主義っぽさがあります。

## 型アサーションの条件

anyなどを省くと、今回の実験ではこんな結果になりました。

- type
    - 構造的なスーパータイプを全て受け入れます。
    - interfaceも構造だけをみて受け入れます
- interface
    - 構造的に全く同一なものを、type/interface問わず受け入れます。
    - 明確に継承しているものも受け入れます。

こうしてみるとinterfaceは比較的堅牢な気もしてきますが、numberを受け入れてしまうEnumや綴りにspecificなリテラルタイプと適宜使い分ける必要がありそうです。