オプショナル型とundefinedとのユニオン型は、オブジェクトとクラスで挙動が違った

# 疑問

オプショナル型の`hoge?: string`と、ユニオン型`hoge: string | undefined`があったとき、両者の挙動に違いがあるのかないのかを検証したくなりました。

なお`strictNullCheck`と`strictPropertyInitialization`が有効な状況を前提とします。

# 検証

ブラウザ(Chrome)上で、次のコードを使って検証しました：[TypeScirpt 公式 Playground](https://www.typescriptlang.org/play/#code/PTAEhKGRnhkfoZFWGRyhkD8MhOhkHYMgWDUBAqrCYxoVaUAoAgSwDsAXAUwCcAzAQwGNLQBZATwEkKaHmBGUAG8CoUAAsA9gHNKAfgBcoAM7lqZaQG4CAXyJkqdJiw7dDfSgCZhoiTMpLV60tNAAfUAFdSAE0q0ySh9tPQJGSVJVUEkAIwArfiVTHiMBUABeGzEQUEAqhkB1hkB2hkBzhkAphkBrhkArBlRAcwZAQHdAU1cawFkGQGiGXTCIqNi4yySuFItrTJFssAKSiprKwBEGfMBbhkBFhkAxhkBihkB7BkaW2dspWSVvPwDSII6CHMBlfUBZJS9ff0CfUEAkhkBjuUBTRUB1BkB9BgWV1cBrBkAJgwzQBmDOsJPRlAB5ADupAACtRJAAHGjkdigQAyDKA1J4WP9AIAM53GRTKVV+ayBoPBDAANsp8ZV2uFIpJaZQAHS0mQACh6-A54khsIRSNR1HRPIA5HtKFKAJTyzSgHJ0hmdVnsrm8nqWQXCuGIlFo9jS2UKpUqsC4yhEHKAZQZADEM60A98oa7rxADM-TMvGMgkyuu0DudgGaGSCLcDlQAVDIBLhkQxNALI9cQALD7BsZhtF4vxtInAPUMsbQWFwhDCtMhyjY7AAwpXlMpBKNQMjPDFacRGHZZIoVGoNPmxMncYxyJJqDz5VkxFa8qSKtV6k02rY9KFGA3qxx61XrC22x2uz2HP3nK4PId7idgkRh11R+PJ9OW2NQO8PpVAPIMIMAQQzhyNykxQAsBPaMR1yIEdQAAWzrLcA1AE4YRrXdG34KdtCg2DUOUbMkJQrdLAwu0wApAFAAA5QAyvV-d5AFUGR0QRmWZaiJHIJjJaY5iWSlgT-CFoUNMUTVAGl6HpRlmS6NlOW5aQeWw+D9QE0VjQlU0ZXsC1lVVMT1WTaTtTkhS9yUkUjXFSUNNkLS5zVW1EydV13XIGC4Krb0CKrBD8J3QjiJDdYAKjOMExyLC3MbdNPMbPDKGQ3yvOIxNABgVQANBkAaPVACztQBK-yXAl7VQQBo+RqVAoDgJBkEACwYizQQBk1JqdZACS5QBt40AcgNMUAIQZ2nZFzYKhXMM3Mf0MlcnC8wIHrXP63pBr9Zhs2MmLtEmvqvX6MaRv5ZbKF69hpqihKYs2+JLE0IA)
(TypeScriptのバージョンや細かいオプションなどはリンク先の設定を参照)

#### 先に検証結果

- オブジェクトで実装した場合
    - オプショナル型`hoge?: string`では`hoge`プロパティを省略できる
    - ユニオン型`hoge: string | undefined`のときは`hoge`はプロパティを省略できない
- クラスで実装した場合
    - **どちらの型定義でも`hoge`プロパティへの代入を省略できる**
- `hoge?: string`と`hoge: string | undefined`は違う型として評価される

## オブジェクトの場合の挙動

まず2つのインタフェースを宣言します。

```ts
interface MyInterface1 {
  hoge?: string;
}

interface MyInterface2 {
  hoge: string | undefined;
}
```

MyInterface1の実装ではオプショナルなプロパティを省略できますが、
MyInterface2の実装ではプロパティは省略できません。

```ts
const obj1: MyInterface1 = {
  // オプショナルなので省略できる
}
const obj2: MyInterface2 = {
  // オプショナルでないプロパティは省略できない。省略するとコンパイルエラー
  hoge: undefined
}
```

結果どちらの実装でもhogeプロパティは`undefined`にできますが、
`hasOwnProperty`をしたときには違いが出ます。

```ts
console.log(obj1.hasOwnProperty('hoge')); // false
console.log(obj2.hasOwnProperty('hoge')); // true
```

MyInterface2はMyInterface1を包含する型として評価されます。
そのため、MyInterface1の型にMyInterface2の型を代入することは可能です。

```ts
// これは可
const obj3: MyInterface1 = obj2;
```

その反対はできません。

```ts
// これはコンパイルエラー
const obj4: MyInterface2 = obj1;
```

## クラスの場合の挙動

インタフェースの例と同じ型定義で2種類のクラスを宣言します。
このとき、**コンストラクタ内でプロパティへの代入がなくてもエラーは出ません。**

```ts
class MyClass1 {
  public hoge?: string;

  constructor() {
    // オプショナルなので省略できる
  }
}

class MyClass2 {
  public hoge: string | undefined;

  constructor() {
    // 代入しなくてもコンパイルが通る!
  }
}
```
どちらのクラスのインスタンスでもhogeについての`hasOwnProperty`の結果は`false`になります。

```ts
const myClass1 = new MyClass1();
const myClass2 = new MyClass2();

console.log(myClass1.hasOwnProperty('hoge')); // false
console.log(myClass2.hasOwnProperty('hoge')); // false
```

MyClass2はMyClass1の型を包含するのでMyClass1の型にMyClass2の型を代入することは可能です。
その逆の代入はできません。

```ts
// これは可
const myClass3: MyClass1 = new MyClass2();
// これはコンパイルエラー
const myClass4: MyClass2 = new MyClass1();
```

## インタフェースの型とクラスの型の互換性

同じ型定義なので、当然相互の代入が可能です。

```ts
const myObj1: MyInterface1 = myClass1;
const myObj2: MyInterface2 = myClass2;
const myObj3: MyClass1 = obj1;
const myObj4: MyClass2 = obj2;
```

# まとめ

確認した挙動をまとめるとこのようになります。

- **オプショナル型の`hoge?: string`と、ユニオン型`hoge: string | undefined`は同じではない**
- **オブジェクトでの実装時には、プロパティを省略できるかできないかが違う**
- **クラスでの実装時には、どちらの型定義でもプロパティへの代入を省略できてしまう**