🤔

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

2022/07/08に公開

どうもこんにちは。
今回は、だいぶTypeScriptの知識が深まってきたので、2週間ほど前にアナウンスされたTypeScript 4.8betaを一通り試してみようかなと思います。

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

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

TypeScript 4.8betaのインストール

npm install --save-dev typescript@beta

npmでインストールできます。

TypeScript 4.8beta変更点

unknown型とobject型と型引数の絞り込みが改善

#49119 Improve intersection reduction and CFA for truthy, equality, and typeof checks

制約のない型引数は{}型にアサインできないように

TypeScript 4.7では --strictNullChecks modeの動作をより厳しくする実装がなされましたが、例えば以下のような関数ではバグを検知できず、#48923でrevertされました。

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

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

この問題をTypeScript4.8では制約のない型引数は{}型にアサインできないようにすることで解決しています。
例えば上記コードをコンパイルしようとすると以下のようなエラーが出ます。

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型nullundefinedを含め、すべての値をとりうる型です。そして、{}型はnull、undefined以外のすべての値をとりうるため、unknown型はUnion Type({} | null | undefined)と同じ性質を持つことになります。(わかりやすい図

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

const f = (x: unknown, y: {} | null | undefined) => {
  x = y;
  y = x; // error
}

unknown型は何の値でも入るが、{} | null | undefinedのUnion型にはunknown型は入れられない)
これが、#49119がマージされることで、コンパイルが通るようになります。

これの何が嬉しいのかというと、unknown型からnullundefinedを取り除いて処理することができるようになります。例えば以下のようなnullundefined以外の時値を返す関数です。

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など)では、型の補完を出すために以下のような実装になっているそうです。(知らんかった)

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

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

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

型引数に対する絞り込みの改善

TypeScript4.8では型引数に対する絞り込みの改善も行われています。

const f = <T>(x: T) => {
    if (x !== undefined && x !== null) {
        x;  // {} TypeScript4.8以前ではTだった
    } else {
        x;  // T
    }
}

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

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

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

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を見てください。

NonNullableの定義の改善

さきほど解説したとおり、型引数に対する絞り込みの改善が行われたことで以下のような関数が型明確になりました。

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と{}型が交差して、しっかりとnullundefinedの可能性が排除されていることがわかります。
これは、lib.d.tsで提供されているNonNullable <T>とは対照的です。

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

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>が型明確になり、以下のような関数がコンパイルエラーにならなくなります。

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」というプルリクをまとめてみました。

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

参考

Discussion