TypeScript 4.8beta変更点➀ unknown型とobject型と型引数の絞り込みが改善された件
どうもこんにちは。
今回は、だいぶ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型
はnull
やundefined
を含め、すべての値をとりうる型です。そして、{}型は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型
からnull
やundefined
を取り除いて処理することができるようになります。例えば以下のようなnull
、undefined
以外の時値を返す関数です。
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 & {}
はTと解釈されるようになります
Tがnullやundefinedではない場合、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と{}型
が交差して、しっかりとnull
とundefined
の可能性が排除されていることがわかります。
これは、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 & {}
のエイリアスとなったところでしょうか。
null
とundefined
の可能性が明確に排除されたことで、ジェネリクス関数でnull
、undefined
を排除したい時に型がすっきりするようになったと思います。
Discussion