TypeScript 3.7 ~ 4.2 までの主な変更をまとめて復習する

[自ブログからの転載です。](https://blog.hedrall.work/posts/typescript-travel-3.7-4.2-20210516)

# 概要

最近久しぶりにTypeScriptのリリースを追って見たのですが、意外と`TypeScript`の各バージョン・機能毎の詳細な記事は見かけますが、全体的にまとめて振り返れる様な情報がなかったので、このまとめを作成しました。

本記事では2019年後半の `v3.7` から 執筆時点で最新の `v4.2` まで、期間にして最近一年ちょっとの主なリリースを既存の記事を引用しながらまとめて行きたいと思います。
結構分量がありますが、重要そうな変更から、細かい変更の順にリストアップしていきます。

時系列としては以下の通りです。

| バージョン | リリース日 |
| :-- | :-: |
| 4.2 | 2021年02月26日 |
| 4.1 | 2020年11月20日 |
| 4.0 | 2020年08月21日 |
| 3.9 | 2020年04月29日 |
| 3.8 | 2020年01月17日 |
| 3.7 | 2019年10月02日 |

> ※ 4.0はメジャーバージョンアップではありません。

なお、一次情報としてはこちらのアナウンスメントを参考にしております。

- [Announcing TypeScript 4.2](https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/)
- [Announcing TypeScript 4.1](https://devblogs.microsoft.com/typescript/announcing-typescript-4-1/)
- [Announcing TypeScript 4.0](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0/)
- [Announcing TypeScript 3.9](https://devblogs.microsoft.com/typescript/announcing-typescript-3-9/)
- [Announcing TypeScript 3.8](https://devblogs.microsoft.com/typescript/announcing-typescript-3-8/)
- [Announcing TypeScript 3.7](https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/)

# Template Literal Types `v4.1`

これはテンプレート文字列の定義と同じ方法で、プレースホルダ付きの文字列型が定義できる様になる変更です。

```typescript
// 今まで
type VerticalAlign = 'top' | 'bottom';
// 新たにこんな型が定義できる様になりました。
type Decorated = `---${VerticalAlign}---`;
// = '---top---' | '---bottom---'
```

この様にプレイスホルダ部分にunion型が入る場合は全ての組み合わせで展開されます。

紹介されている用例で分かりやすかったのは、上下左右の配置を指定するよくある型の生成です。

```typescript
type VerticalAlign = 'top' | 'middle' | 'bottom';
type HorizontalAlignment = 'left' | 'center' | 'right';
type Alignment = `${VerticalAlign}-${HorizontalAlignment}`;
// =>  | "top-left"       | "top-center"        | "top-right"
//        | "middle-left"  | "middle-center"   | "middle-right"
//        | "bottom-left" | "bottom-center" | "bottom-right"
```

さらに、型計算の為の新たな（特殊な）4つの型関数が追加されています。

```typescript
type HelloWorld = 'HelloWorld';
type HELLOWORLD = `${Uppercase<HelloWorld>}`;
type helloworld = `${Lowercase<HelloWorld>}`;
// type HelloWorld = `${Capitalize<HelloWorld>}`;
type helloWorld = `${Uncapitalize<HelloWorld>}`;
```

詳細は下記が参考になります。
- [TypeScript 4.1 の Template Literal Types がやばい](https://qiita.com/acro5piano/items/5787b9133f7433040085)
- [Template Literal Types で「単位を持つ数値」の文字列型をきれいに扱いたい](https://zenn.dev/mizchi/articles/template-literal-types-for-unit)
- [📂TypeScript 4.1で密かに追加されたintrinsicキーワードとstring mapped types](https://zenn.dev/uhyo/articles/typescript-intrinsic)

# タプル

タプルは非常に便利な機能だと思います。TypeScriptの場合はArrayを使用して(無理矢理)タプルっぽい機能を実装しています。
タプルとは下記の様な、中身の型が指定された配列型です。

3.9以前では以下の様な機能でした。

```typescript
type TupleA = [string, number?];
const a1: TupleA = ['arg1']; // OK
const a2: TupleA = ['arg1', 1]; // OK
const a3: TupleA = ['arg1', 1, 2]; // Error
const a4: TupleA = ['arg1', 'arg2']; // Error

// 「最後」の要素は特別な「レストパラメータ」としても定義できる
type TupleB = [string, number, ...boolean[]];
const b1: TupleB = ['arg1', 1, false]; // OK
const b2: TupleB = ['arg1', 1, false, false]; // OK
const b3: TupleB = ['arg1', 1]; // OK
const b4: TupleB = ['arg1', 1, 2]; // Error
const b5: TupleB = ['arg1', 1, false, 2]; // Error
```

こんな感じで、配列の要素の型を決めておけるので、例えば複数の値を返す関数の戻り値の型などに便利な機能です。

```typescript
function numAndString (): [number, string] {
  return [100, 'test'];
}
```

一方、いくつかの強い制約がありました。

1. 可変長のレストパラメータは最後に1回だけしか使えない
2. レストパラメータは配列の形式でしか使えない( ...string[]の様に )

そこで、`v4.0` で [`Variadic Tuple Types`](https://devblogs.microsoft.com/typescript/announcing-typescript-4-0/#variadic-tuple-types) がリリースされ、2が改善されました。

参考: [TypeScript 4.0で導入されるVariadic Tuple Typesをさっそく使いこなす](https://qiita.com/uhyo/items/7e31bbd93a80ce9cec84#variadic-tuple-types%E3%81%AE%E3%81%99%E3%81%94%E3%81%84%E7%82%B9)

これは2つの変更が含まれています。

1つ目は `...T` の様にタプル型に他のタプル型をスプレッド演算子で展開できる様になりました。
(より正確にはタプル型のスプレッドがジェネリックとして定義され、実際の値に置き換えることでタプルを組み合わせ可能になりました。)

```typescript
type TupleC = [string, string];
type TupleD = [number, number];
type TupleE = [...TupleC, ...TupleD];
```

逆に型をスプレッド演算子にキャプチャすることもできます。

```typescript
function tail<T extends any[]>(arr: readonly [any, ...T]) {
  const [_ignore, ...rest] = arr;
  return rest;
}
const tuple1 = [1, 2, 3, 4] as const;
const r1 = tail(tuple1); // => r1: [2, 3, 4]
const tuple2 = ['hello', 'world'] as const;
const r2 = tail([...tuple1, ...tuple2]); // => r2: [2, 3, 4, 'hello', 'world']
```

2つ目の変更点は、このタプルのスプレッドはどこにでも配置できます。

```typescript
type Strings = [string, string];
type Numbers = [number, number];
type StrStrNumNumBoolArr = [...Strings, ...Numbers, boolean[]];

// ただし、配列形式のレストパラメータは最後に一度だけしか使用できません
type Tuple = [...Strings, ...string[], ...Numbers]; // Error
```

さらに `v4.2` では `Leading/Middle Rest Elements in Tuple Types` がリリースされ、
タプルの最初や中間でも配列のレストパラメータを使用することができる様になりました。

```typescript
type Tuple = [...string[], number];

// ただし、レストパラメータがつかえるのは1回だけです。
type A = [...string[], ...boolean[], number]; // Error
// また、レストパラメータの後にはオプショナルパラメータはこれません。
type B = [...string[], boolean?, number]; // Error
```

少し話はそれますが、`v3.9` のリリースでは `Labeled Tuple Elements` がリリースされ、タプルの要素を命名できる様になりました。

この様に、タプル型は短期間のうちに可能な限り柔軟になってきました。

```typescript
const example = (...args: [first: number, second: string]) => { /* ... */};
```

これは単純にドキュメンテーションの為の改善です。

# ECMA Scriptの機能

`ECMA Script` 標準で実装されている(実装されそう)な機能がTSに実装されたものです。

## Optional Chaining `v3.7`

[proposal-optional-chaining](https://github.com/tc39/proposal-optional-chaining)で提案された、`ES2020` の機能です。

以下の2文は同じになります。

```typescript
a && a.b && a.b();
// ↓
a?.b?.();
```

## Nullish Coalescing `v3.7`

[proposal-nullish-coalescing](https://github.com/tc39/proposal-nullish-coalescing)で提案された、`ES2020` の機能です。

以下の2文は同じになります。

```typescript
const b = a !== null && a !== void 0 ? a : 'null or undefined';
// ↓
const b = a ?? 'null or undefined';
```

## Private Fields `v3.8`

[proposal-class-fields](https://github.com/tc39/proposal-class-fields、現在 Stage3 の `ES2022` に入る予定の機能です。
(Chromeはv74から、Node.jsはv12から実装されています。)

クラスにプライベートなフィールドを定義できます。

```typescript
class A {
  // '#'で始まるフィールドがプライベートになる
  #privateField;

  constructor() { this.#privateField = 'hoge' }
}
```

TypeScriptの `private` 修飾子との違いが気になりますが、`private` はJS変換後に消去されるのに対して、'#'で始まフィールドは`hard privacy`と呼び、完全に定義されたクラス内のスコープに存在するので挙動が異なります。

```typescript
// 以下は違いの例
class A {
  #hard: number = 10;
  getHardA = () => this.#hard;
}

class B extends A {
  // privateと違いoverrideできるが
  #hard: number = 20;
  getHardB = () => this.#hard;
}
const b = new B();
b.getHardA(); // 10 <= 別のスコープの変数として保存されている
b.getHardB(); // 20
```

## Short-Circuiting Assignment Operators

[proposal-logical-assignment](https://github.com/tc39/proposal-logical-assignment)で提案された、 `ES2020` の機能です。

下記の3対の文は同じです。

```typescript
// 1. a が falsy なら代入する
a ||= b;
a || (a = b);

// 2. a が Truthy なら代入する
a &&= b;
a && (a = b);

// 3. a が null か undefined なら代入する
a ??= b;
a ?? (a = b);
// a !== null && a !== undefined ? a : (a = b);
```

# Key Remapping in Mapped Types `v4.1`

`Mapped Type` を作成する時に、キーを変換することができる様になりました。

```typescript
type Getters<T> = {
  // ここの as に注目
  [K in keyof T as `get${Capitalize<string & K>}`]: () => T[K]
};
type A = Getters<{name: string, age: number}>;
// => {
//   getName: () => string,
//   getAge: () => number,
// }
```

# Assertion Functions `v3.7`

Type Guardをご存知でしょうか？

Assertion FunctionsはType Guardと同様に、コンパイラにユーザ側が責任を持って型安全を保証する機能です。

例えば、以下の場合itemは `number | undefined` に推論されます。

```typescript
const myArray = [1, 2, 3];
for (const index in myArray) {
  const item = myArray[index];
  // const item: number | undefined;
}
```

その為、itemをnumberとして操作したい場合は `undefined` でない事ををチェックしないといけません。

```typescript
const item = myArray[index];
if (typeof item !== 'number') return 0;
return item;
// const item: number;
```

(`as`を使ってダウンキャストすることも可能ですが、なんのチェックもせずに型を変えてしまうのは基本的に危険です。)

ここで、numberかどうかの判定は良く使いそうなので、`typeof item !== 'number'` を関数に切り出してみましょう。

```typescript
function isNumber (value: any) {
  return typeof value === 'number';
}
const item = myArray[index];
if (!isNumber(item)) return 0;
return item;
// const item: number | undefined;
```

この様に、切り出した場合は型の条件が絞りこまれません。この現象に開発者側が自責で対応する方法が `type guard`です。

```typescript
// value is numberの部分でvalueの型を開発者が保証する
function isNumber (value: any): value is number {
  return typeof value === 'number';
}
const item = myArray[index];
if (!isNumber(item)) return 0;
return item;
// const item: number;
```

これと同じ操作をアサーションで使える様にしたのが、本題の`assertion functions`です。
`type guard` はif文で使用しましたが、`assertion functions`は条件を満たさない場合に `throw` する事を、開発者側が保証することができます。

```typescript
// value is numberの部分でvalueの型を開発者が保証する
function isNumber (value: any): asserts value is number {
  if (typeof value !== 'number') {
    throw new TypeError('test');
  }
}
const item = myArray[index];
isNumber(item);
return item;
// const item: number;
```

今回は `for-in` でループを回しており、itemがundefinedにならないのは、
開発者から見れば納得ができる部分なので、`assertion functions`を使用しても問題ないと思われます。

下記の参考の通り、どちらもTSの型チェックをマヒさせる危険性があるので、使い所は慎重に考える必要がありそうです。

参考
- [TypeScript の Type Guard を使ってキャストいらず](https://qiita.com/propella/items/33433278497f290ceadb)
- [TypeScript 3.7の`asserts x is T`型はどのように危険なのか](https://qiita.com/uhyo/items/b8d2ea6fbf6214fc4194)

# Smarter Type Alias `v4.2`

結構便利な変更です。以下の `doStuff` 関数の戻り値の型を考えます。

```typescript
type BasicPrimitive = number | string | boolean;

export function doStuff(value: BasicPrimitive) {
  if (Math.random() < 0.5) {
    return undefined;
  }
  return value;
}
```

`v4.1` 以前は `number | string | boolean | undefined` に推論されてましたが、

`v4.2` では `BasicPrimitive` が保持されて `BasicPrimitive | undefined` になりました。

# コメント系

## // @ts-expect-error Comments `v3.9`

まずは下記のコードをみてください。

```typescript
// ライブラリを開発している想定想定

import assert from 'assert';
function doStuff(abc: string, xyz: string) {
  // 下記の様なアサーションを使用して、JavaScriptユーザに警告を与えたい！
  assert(typeof abc === "string");
  assert(typeof xyz === "string");
}
```

この関数のテストを書く場合、あえて型を間違えた異常系も検査したいと考えると思います。

```typescript
expect(() => {
  // 123のところで、TSがエラーする
  doStuff(123, 456);
}).toThrow();
```

これではまずTSがうまく動かなくなってしまいます。そこで、`@ts-expect-error` コメントをつけることで、TSのエラーを抑制することができます。

```typescript
expect(() => {
  // @ts-expect-error
  doStuff(123, 456); // OK
}).toThrow();
```

`@ts-ignore` との違いはややこしいですが、`@ts-expect-error`の場合はTSの静的なエラーがでない場合はエラーします。

以下の通りです。

```typescript
expect(() => {
  // @ts-expect-error
  doStuff('123', 456); // NG
  // @ts-ignore
  doStuff('123', 456); // OK (になってしまう)
}).toThrow();
```

これにより、エラーが発生することが見込まれる場合は `@ts-expect-erorr` を使用した方が、より適切な情報を持ったコードになります。

## @ts-nocheck `v3.7`

```typescript
// @ts-nocheck
```

これを記述すると、ファイル全体の型検査をしなくなります。
JSとTSに移行する途中などで、利用できます。

# abstract Construct Signatures `v4.2`

```typescript
// 今まで、クラスコンストラクタの型は以下の様に宣言できましたが、
type ClassConstructor = new () => {};
// 4.2からは abstract 修飾子が加わり、abstractクラスのコンストラクタが区別される様になりました。
type AbstractClassConstructor = abstract new () => {};
```

詳細は以下をみてください。用例を考えるとちょっとややこしいです。

参考
- [abstract Construct Signatures](https://devblogs.microsoft.com/typescript/announcing-typescript-4-2/#abstract-construct-signatures)
- [TypeScript 4.2　覚書](https://zenn.dev/yutama_kotaro/articles/43ac06011d3b47)

# Recursive Conditional Types `v4.1`

型の定義を再帰的に行った時に、それが解析される様になりました。
下記はネストされた配列をフラット化する `deepFlatten` 関数へ型をつける例です。

```typescript
// ネストされた配列の要素の型を再帰的に返す
type ElementType<T> = T extends ReadonlyArray<infer U> ? ElementType<U> : T;

function deepFlatten<T extends unknown[]>(x: T): ElementType<T>[] {
  throw '実装する';
}

// 以下の3つは全て戻り値が number[] 型に推論されます
deepFlatten([1, 2, 3]);
deepFlatten([[1], [2, 3]]);
deepFlatten([[1], [[2]], [[[3]]]]);
```

`v4.0` までは全て、`ElementType<number>` 型に推論されていた為、型の定義ができてもうまく静的解析が走らない状態でした。

# Class Property Inference from Constructors `v4.0`

コンストラクタで初期化した時に代入される型にフィールドの型が推論されます。

```typescript
class Square {
  area;       // numberに推論される
  sideLength; // numberに推論される

  constructor(sideLength: number) {
    this.sideLength = sideLength;
    this.area = sideLength ** 2;
  }
}
```

以前は `area` も `sideLength` も `any` に推論されていました。

constructorの中で初期化されない可能性があるプロパティはちゃんと `undefined` とのユニオンで推論されます。

```typescript
class Person {
  name; // string | undefined に推論される

  constructor(name: string) {
    if (Math.random()) {
      this.name = name;
    }
  }
}
```

# インデックスシグネチャに関する改修

## Stricter Assignability Checks to Unions with Index Signatures `v3.8`

```typescript
const obj1: { [x: string]: number } | { a: number } = { a: 5, c: 'abc' }; // error
// cの部分がエラーする様になった。
```

以前は `c` は過剰なプロパティとみなされ型のチェックがされなかったが、`v3.8`からはインデックスシグネチャに含まれると判断し、numberで縛ってくれる。

## noUncheckedIndexedAccessフラグの追加 `v4.1`

```typescript
interface Options {
  hoge: string;
  huga: number;

  // インデックスシグネチャ
  [propName: string]: string | number;
}

function checkOptions(opts: Options) {
  opts.hoge; // string
  opts.fuga; // number

  opts.hogefuga.toString();
  // noUncheckedIndexedAccessが false(無指定) の時,
  // opts['c'] が string | number に推論されてエラーしない
  // noUncheckedIndexedAccessが true の時,
  // opts['c'] が string | number | undefined に推論され、エラーする
}
```

## noPropertyAccessFromIndexSignatureフラグの追加 `v4.2`

trueにした場合はインデックスシグネチャに当たるプロパティにアクセスする時に、`['name']`の構文を使用しなくてはならない。

```typescript
function checkOptions(opts: Options) {
  const hoge = opts.hoge; // OK
  const fuga = opts.fuga; // OK
  const hogefuga = opts.hogefuga; // noPropertyAccessFromIndexSignatureが true の時はエラーする
  // TS4111: Property 'c' comes from an index signature, so it must be accessed with ['c'].

  const d = opts['hogefuga']; // OK
}
```

これにより、インデックスシグネチャではないプロパティをタイポした時に警告が出る。

```typescript
function checkOptions(opts: Options) {
  const a = opts.hogo; // Error
}
```

## Relaxed Rules Between Optional Properties and String Index Signatures `v4.2`

```typescript
type WesAndersonWatchCount = {
  "Fantastic Mr. Fox"?: number;
  "The Royal Tenenbaums"?: number;
};

declare const wesAndersonWatchCount: WesAndersonWatchCount;
// 分割代入の時に、以前であればエラーが発生していたが、
// 定義されてないプロパティは消去される事をTSが理解してくれる。
const movieWatchCount: { [key: string]: number } = wesAndersonWatchCount;
```

# useDefineForClassFieldsの導入 `v3.7`

TSとECMAScriptでクラスフィールドの生成方法にズレが生じたが、ブレーキングチェンジになる為、フラグとして
ECMAScriptに寄せる機能をリリースしました。 `useDefineForClassFields` をONにする事が強く推奨されています。

詳細:
- [The useDefineForClassFields Flag and The declare Property Modifier](https://devblogs.microsoft.com/typescript/announcing-typescript-3-7/#the-usedefineforclassfields-flag-and-the-declare-property-modifier)
- [qiita: TypeScript v3.7.2 変更点](https://qiita.com/vvakame/items/60d8d43ded0b160a99cc#%E3%82%88%E3%82%8A%E5%8E%B3%E5%AF%86%E3%81%AAes%E4%BB%95%E6%A7%98%E3%81%B8%E3%81%AE%E8%BF%BD%E5%BE%93%E3%81%A8-usedefineforclassfields-%E3%82%AA%E3%83%97%E3%82%B7%E3%83%A7%E3%83%B3%E3%81%AE%E8%BF%BD%E5%8A%A0)

# その他細かい変更点

## unknown on catch Clause Bindings `v4.0`

キャッチでバインドする変数は、以前は `any` 型でしたが、`unknown` 型として受け取る事ができる様になりました。

```typescript
try {}
catch (x: unknown) {
  // 明示的に unknown がつけられる
  // しっかりプロパティチェックをしないとエラーする
  console.log(x.message); // error

  if (x instanceof Error) {
    console.log(x.message); // OK
  }
}
```

## Stricter Checks For The in Operator `v4.2`

`in` 演算子に primitive 型が使えなくなりました。

```typescript
const _1 = "foo" in { a: 42 }; // OK
const _2 = "foo" in [42]; // OK
const _3 = "foo" in string; // error
```

## Improvements in Inference and Promise.all `v3.9`

Promise.allの推論が改善されました。

```typescript
interface Lion { eatMeets(): void }
interface Elephant {eatVegetables(): void }
async function visitZoo(lionExhibit: Promise<Lion>, elephantExhibit: Promise<Elephant | undefined>) {
  let [lion, elephant] = await Promise.all([lionExhibit, elephantExhibit]);
  lion.eatMeets();
  //以前は, Object is possibly 'undefined'. でエラーした
}
```

## export * as ns Syntax `v3.8`

```typescript
import * as SomeModule1 from "./some-module";
export { SomeModule1 };
// これが以下の様にかける様になった
export * as SomeModule2 from './some-module';
```

## Type-Only Imports and Exports `v3.8`

型だけをimportしたい時に、明示的に型だけを使用する事を宣言できる。

```typescript
import type { SomeThing } from './some-module';
// これはコンパイル時に確実に消去されるので、importに寄る副作用が起きない
```

# 参考文献

TSブログのアナウンスを地道に読み返したのですが、各回のリリースに関しては分かりやすい記事がすでにありますので、分かりづらい部分はこちらも参考にさせていただきました。

- [Qiita - TypeScript v3.7.2 変更点](https://qiita.com/vvakame/items/60d8d43ded0b160a99cc)
- [Qiita - TypeScript 3.8 Beta 追加機能等まとめ](https://qiita.com/yoshimok/items/aff85cd72f7c62ce2369)
- [Qiita - TypeScript v3.9.2 変更点](https://qiita.com/vvakame/items/b98540566bc802d1c0fb)
- [Zenn - TypeScript v4 まとめ](https://zenn.dev/smish0000/articles/8da1f46aa4465f)
- [Zenn - TypeScript 4.2　覚書](https://zenn.dev/yutama_kotaro/articles/43ac06011d3b47)

# まとめ

細かなものを含めると意外と追うのが大変でした。しかし、タプル、テンプレートリテラル、インデックスシグネチャなど普段TSを使っていて「あれ、っちょっとゆるくない？」と思った部分がどんどん改修されてきている印象で、TSは言語として確実に成長しているのだなぁと改めて感じました。

(もしこの記事を最後まで読んだ方がましたら、その根気に拍手を送りたいと思います。ありがとうございます。)