TypeScript 4.8beta変更点➀ unknown型とobject型と型引数の絞り込みが改善された件

どうもこんにちは。
今回は、だいぶTypeScriptの知識が深まってきたので、2週間ほど前にアナウンスされた[TypeScript 4.8beta](https://devblogs.microsoft.com/typescript/announcing-typescript-4-8-beta/)を一通り試してみようかなと思います。

一通り試すことで知識を深め、忘れないために個人的なメモとしてアウトプットしていますので、英語の解釈が間違っていたり、おかしいことを言ってるかもしれないです。

もし間違ってることを言っていたらコメントで教えていただけると非常に助かります。

## TypeScript 4.8betaのインストール
```bash
npm install --save-dev typescript@beta
```
npmでインストールできます。

## TypeScript 4.8beta変更点
### unknown型とobject型と型引数の絞り込みが改善
[#49119 Improve intersection reduction and CFA for truthy, equality, and typeof checks](https://github.com/microsoft/TypeScript/pull/49119)

#### 制約のない型引数は{}型にアサインできないように
TypeScript 4.7では --strictNullChecks modeの動作をより厳しくする実装がなされましたが、例えば以下のような関数ではバグを検知できず、#48923でrevertされました。

```typescript
const f = <T>(x: T) => {
  const y: {} = x;
  return y;
};
```

上記の関数の任意の型引数Tはnullやundefinedをとる可能性ありますが、`const y: {} = x;`でエラーとならないです。(例えば`f(null)`と呼び出した時に、`{}型`にnullが入ってしまう。[＃48366](https://github.com/microsoft/TypeScript/issues/49005) 参照）

この問題をTypeScript4.8では制約のない型引数は{}型にアサインできないようにすることで解決しています。
例えば上記コードをコンパイルしようとすると以下のようなエラーが出ます。
```bash
index.ts:18:9 - error TS2322: Type 'T' is not assignable to type '{}'.
18   const y: {} = x;
           ~

  index.ts:17:12
    17 const f = <T>(x: T) => {
                  ~
    This type parameter might need an `extends {}` constraint.
```

このようにTypeScript4.8以降では制約のない型引数Tを`{}型`にアサインしようとする場合は`const f = <T>(x: T) => {`を`const f = <T extends {}>(x: T) => {`のようにして型引数Tを制約してあげなければならなくなります。

#### unknownはCFAでUnion Type（{} | null | undefined）と同じ動作をするようになります
`unknown型`は`null`や`undefined`を含め、すべての値をとりうる型です。そして、{}型はnull、undefined以外のすべての値をとりうるため、`unknown型`はUnion Type（{} | null | undefined）と同じ性質を持つことになります。（[わかりやすい図](https://typescriptbook.jp/reference/values-types-variables/object/difference-among-object-and-object#object%E5%9E%8Bobject%E5%9E%8B%E5%9E%8B%E3%81%AE%E4%BB%A3%E5%85%A5%E7%AF%84%E5%9B%B2)）

しかし、TypeScript4.8以前ではunknownはUnion Type（{} | null | undefined）と同じ動作をするわけではなかったため、例えば以下のようなコードはコンパイルエラーになっていました。

```typescript
const f = (x: unknown, y: {} | null | undefined) => {
  x = y;
  y = x; // error
}
```
（`unknown型`は何の値でも入るが、`{} | null | undefined`のUnion型には`unknown型`は入れられない）
これが、[#49119](https://github.com/microsoft/TypeScript/pull/49119)がマージされることで、コンパイルが通るようになります。

これの何が嬉しいのかというと、`unknown型`から`null`や`undefined`を取り除いて処理することができるようになります。例えば以下のような`null`、`undefined`以外の時値を返す関数です。
```typescript
const f = (value: unknown) => {
  if (value !== undefined && value !== null) {
    return value; // TS4.8以前ではunknown、TS4.8では{}
  }

  throw Error("Nullable");
};
```

TypeScript4.8以前では、関数fは`unknown`を返す関数と解釈されていましたが、TypeScript4.8では`{}型`を返す関数と解釈されます。つまり`unknown型`を絞り込むことができるようになります！

#### Tがnullやundefinedではない場合、`交差型T & {}`はTと解釈されるようになります
TypeScript4.8以前でも`null & {}`、`undefined & {}`は`never`と解釈されています。それに加えTypeScript4.8以降はTがnullやundefinedではない場合、`交差型T & {}`はTと解釈されるようになります。 

ただし、後方互換性のために、`string & {}`、`number & {}`、`bigint & {}`については例外があります。

いくつかのフレームワーク（Reactなど）では、型の補完を出すために以下のような実装になっているそうです。（知らんかった）

```typescript
type Alignment = string & {} | "left" | "center" | "right";
```

この型の意味は、ただstringを受け入れることができる型ですが、このようにユニオン型にすることで、サブタイプの削減が起きないようになり、入力補完に`"left" | "center" | "right"`が現れます。

TypeScript4.8で`string & {}`を単に`string`と解釈するようになると、このような実装で入力補完の実装を行うことができなくなってしまうため、`string & {}`、`number & {}`、`bigint & {}`については明示的に書いた場合に限りTypescript4.8以前と同じように解釈されるそうです。

#### 型引数に対する絞り込みの改善
TypeScript4.8では型引数に対する絞り込みの改善も行われています。
```typescript
const f = <T>(x: T) => {
    if (x !== undefined && x !== null) {
        x;  // {} TypeScript4.8以前ではTだった
    } else {
        x;  // T
    }
}
```

また、ジェネリクスの関数で型引数Tは真のフローで`{}型`と交差するようになります。

```typescript
const f = <T>(x: T) => {
    if (x) {
        x;  // T & {}
    }
    else {
        x;  // unknown
    }
}
```

そして、`null`または`undefined`の等式比較内のフローでは、型引数Tは`({} | null)`または`({} | undefined)`と交差するようになります。

```typescript
const f = <T>(x: T) => {
    if (x !== undefined) {
        x;  // T & ({} | null)
    } else {
        x;  // T
    }
    if (x !== null) {
        x;  // T & ({} | undefined)
    } else {
        x;  // T
    }
    if (x !== undefined && x !== null) {
        x;  // {}
    } else {
        x;  // T
    }
}
```

詳しくは[#49119](https://github.com/microsoft/TypeScript/pull/49119)を見てください。
#### NonNullableの定義の改善
さきほど解説したとおり、型引数に対する絞り込みの改善が行われたことで以下のような関数が型明確になりました。

```typescript
const notNull = <T>(x: T) => {
    if (x === null) throw Error();
    return x;  // T & ({} | undefined)
}

const notUndefined = <T>(x: T) => {
    if (x === undefined) throw Error();
    return x;  // T & ({} | null)
}

const notNullAndUndefined = <T>(x: T) => {
    return notUndefined(notNull(x));  // T & {}
}
```

notNullAndUndefined関数では型引数Tと`{}型`が交差して、しっかりと`null`と`undefined`の可能性が排除されていることがわかります。
これは、lib.d.tsで提供されている`NonNullable <T>`とは対照的です。

TypeScript4.8以前の`NonNullable <T>`の定義は以下のようなものであり、`NonNullable <NonNullable <T>>`は本質的に`NonNullable <T>`とはならず、不必要に複雑な型になってしまいます。

```typescript
type NonNullable<T> = T extends null | undefined ? never : T;

const f = <T>(x: T) => {
  type T1 = NonNullable<T>; // T extends null | undefined ? never : T
}
```

**そこでTypeScript4.8では`NonNullable <T>`の定義を単に`T & {}`のエイリアスとすることで、この問題を解決しています。**

これにより`NonNullable <T>`が型明確になり、以下のような関数がコンパイルエラーにならなくなります。
```typescript
const f = <T>(x: NonNullable<T>, y: NonNullable<NonNullable<T>>) => {
    x = y;
    y = x; // TypeScript4.8以前ではエラーになる
}
```

## まとめと感想
この記事では、TypeScript4.8に含まれる「[#49119 Improve intersection reduction and CFA for truthy, equality, and typeof checks](https://github.com/microsoft/TypeScript/pull/49119)」というプルリクをまとめてみました。

やっぱり一番インパクトがあった変更は`NonNullable <T>`が単に`T & {}`のエイリアスとなったところでしょうか。
`null`と`undefined`の可能性が明確に排除されたことで、ジェネリクス関数で`null`、`undefined`を排除したい時に型がすっきりするようになったと思います。



## 参考
- [Announcing TypeScript 4.8 Beta](https://devblogs.microsoft.com/typescript/announcing-typescript-4-8-beta/)
- [#49119 Improve intersection reduction and CFA for truthy, equality, and typeof checks
](https://github.com/microsoft/TypeScript/pull/49119)