TypeScriptにはanyが4種類、undefinedが3種類、……
このツイートの解説をします。
TypeScriptにはanyは4種類、undefinedは3種類、nullは2種類、trueは2種類、falseは2種類、neverは5種類あるのか。普通に使ってる分にはわからないが……
TypeScriptでは表面上は同じ名前でも内部的に異なる型が割り振られている場合がいくつかあります。そのようなもののうち、プリミティブな型についてまとめました。
対象TypeScriptバージョンは4.1.3です。
2021-01-09 update: 数え方を見直しました。 any
が4種類から6種類に増えました。
注意
ここに書かれていることを知らなくても、TypeScriptプログラミングにおいて全く困りません。あくまでコンパイラの機微を楽しむつもりでお読みください。
前提知識
-
any
,undefined
,null
,true
,false
,never
型について …… 自信がない人はuhyo氏のTypeScriptの型入門を参照。 - Widening …… TypeScriptの型入門#リテラル型と型推論を参照。
- Conditional types ……
T extends U ? V : W
の形の型のこと。 - Optional chaining …… uhyo氏のoptional chainingに関する記事を参照。
- コンテキスト型 …… メインの型推論の順序とは逆向きに型を伝搬するために、メインの型推論の前に行われる補助的な型推論のこと。
- フロー型 …… 制御フローとの関係から型を絞り込む仕組み。たとえば
x: number | null
のとき、if(x) { ... }
内ではx: number
に絞り込める。対義語は「宣言された型」。
any
anyには6種類の亜種があります。標準的なany は anyType
です。
autoType
プログラマの指示なく付与された any
型につけられることがあります。TypeScript 2.1.1で導入されました。
細かい条件を書くときりがないですが、 autoType
がつく主要な条件は以下の2つです。
- 変数宣言で、以下を満たすときに推論される
any
型はautoType
になる。-
let
宣言またはvar
文の一部である。 (const
は含まない) -
export
修飾子もdeclare
修飾子もついていない。 - 構造化束縛の一部ではない。
-
--noImplicitAny
が有効であるか、または当該宣言が*.js
/*.jsx
ファイル内にある。 - 初期化子を持たないか、初期化子が
null
/undefined
である。
-
- 変数宣言で、以下を満たすときに推論される
any[]
型のany
はautoType
になる。-
let
宣言またはvar
文の一部である。 (const
は含まない) -
export
修飾子もdeclare
修飾子もついていない。 - 構造化束縛の一部ではない。
-
--noImplicitAny
が有効であるか、または当該宣言が*.js
/*.jsx
ファイル内にある。 - 初期化子が
[]
である。
-
autoType
が通常の any
区別して扱われる場面もまた色々ありますが、主要なのは --noImplicitAny
のエラー判定です。 autoType
やその配列型に推論された変数はエラーになります。
// any型 (autoType)
let x1 = null;
let x1a = () => x1; // error
// any型 (anyType)
let x2: any = null;
let x2a = () => x2;
// any[]型 (autoType)
let x3 = [];
let x3a = () => x3; // error
// any[]型 (anyType)
let x4: any[] = [];
let x4a = () => x4;
wildcardType
wildcardType
はconditional typeの判定のために使われるダミーの型です。TypeScript 2.8で導入されました。ユーザーに露出されることはないかもしれませんが、内部的にはanyの亜種ということになっているためここで挙げます。
Conditional typeの条件節 (T extends U
) は必ずしもtrueかfalseになるわけではありません。文字通り T extends U
のようにジェネリクス引数が残っているときは、その時点では判定できないこともあります。つまり、conditional typeの条件の結果は true/false/indeterminateの3値 (正確には any
を含む場合は確定で両方に分岐するため、4値) になります。実際の型検査では「確定でfalseかどうか」と「確定でtrueかどうか」をそれぞれ検査することになります。
さて、確定でfalseかどうかを判定するには、条件をできるだけ緩和して評価すればいいので、 T
と U
に出現する型引数を「判定できない」ことを表す any
に置き換えて評価すればいいことになります。しかし、通常の any
は気を利かせて余計な推論を行ってしまうことがあり、それを抑制した専用の wildcardType
を使うことになります。たとえば以下のような振る舞いがあります。
-
wildcardType
を含む合併/交叉はwildcardType
。 -
wildcardType
のkeyof
はwildcardType
。 -
T
またはK
がwildcardType
のときT[K]
はwildcardType
。 -
T
またはU
がwildcardType
のときT extends U ? V : W
はwildcardType
。
errorType
errorType
は名前の通り型エラーが発生したときに処理を続行するために割り当てられる型です。microsoft/TypeScriptのmasterから辿れる最初のバージョンから unknownType
という名前で存在し、その後TypeScript 3.0で errorType
にリネームされました。
内部的な名前 (intrinsic name) は error
ですが、フラグ上の扱いは any
に準じ、ユーザーに提示される名前も any
になります。
errorType
は any
よりも強く伝搬されることがあります。
// この行自体はもちろんエラーになる
type Errored = number["foo"];
// { [x: string]: any }
type Test1 = Partial<any>;
// any
type Test2 = Partial<Errored>;
また、1つのエラーに起因して大量のエラーメッセージが出るのを防ぐため、後続の型検査でのエラーを抑制する効果もあります。
declare const func1: any;
// この行自体はエラーになる
declare const func2: number["foo"];
// エラー
func1<number>();
// エラーにはならない
func2<number>();
nonInferrableAnyType
nonInferrableAnyType
はコンテキスト型による推論をどこまで行うべきかを示すマーカーの役割があります。TypeScript 3.8で導入されました。
TypeScriptの型推論は原則として式の構造に対してボトムアップに、そして上から下という方向に進みます。この原則ではカバーできないユースケースがあるため、逆向きに型を伝搬する仕組みがコンテキスト型です。
// 初期化子から変数の型が推論される
const x = 42;
// 変数の型から初期化子の型が決定される (コンテキスト型)
const f: (x: number) => number = (x) => x;
コンテキスト型はあくまでメインの型推論のヒントとして使われるものなので、最終的な型とは一致しないこともあります。
// アロー関数式の型は (x: number | string) => number であり、コンテキスト型とは異なる
const f: (x: number) => number = (_x: number | string) => 42;
コンテキスト型の用途のひとつに、関数呼び出しのジェネリクス引数推論があります。 (inferTypeArguments
内)
declare function generate<T>(len: number, callback: (prev?: T) => T): T[];
// 右辺だけでは型推論が通らない例
const x: { counter: number }[] = generate(42, (prev) => ({ counter: prev ? prev.counter + 1 : 0 }));
この場合、 generate(...)
のコンテキスト型として { counter: number }[]
が与えられます。これが generate<T>
の戻り値型である T[]
と単一化され、 T = { counter: number }
が推論されるため、これがそのまま採用されます。単一化に失敗したときは単に結果が破棄されます。
分割代入の構造やデフォルト値の型も、コンテキスト型に使われます。
// 右辺式のコンテキスト型は { x: any, y: number }
// (このanyがnonInferrableAnyTypeになる)
const { x, y = 42 } = ...;
ところで、上記のルールをそのまま適用すると、予期しないコンテキスト型が推論される可能性があります。
declare function foo<T = number>(): [T];
// [number]
const x = foo();
// any
const [y] = foo();
const [y] = ...
のコンテキスト型は [any]
ですが、これと [T]
を単一化すると T = any
になってしまいます。 any
と [T]
の場合は単一化しても T
に関する制約は発生しないため、他の情報 (デフォルト型引数) が優先されます。
このような問題を抑制するためにTypeScript 3.8で導入されたのが nonInferrableAnyType
です。 nonInferrableAnyType
は分割代入内の BindingIdentifier (デフォルト値を持たないもの) のコンテキスト型に付与され、単一化時に T = any
のような制約を生成しないという違いがあります。これにより上に挙げた例は期待通りに推論されるようになります。
intrinsicMarkerType
intrinsicMarkerType
はintrinsic type (コンパイラ側で実装される特別な型エイリアス) の構文を扱うための内部的な型です。TypeScript 4.1で導入されました。
TypeScript 4.1時点でintrinsic typeは4つあり、TypeScriptの標準型定義で以下のように定義されています。
type Uppercase<S extends string> = intrinsic;
type Lowercase<S extends string> = intrinsic;
type Capitalize<S extends string> = intrinsic;
type Uncapitalize<S extends string> = intrinsic;
この定義を正しくパースできるようにするために、 type ... =
の直後に intrinsic
が来たとき (かつ、 .
が後続しないとき) は intrinsicMarkerType
としてパースするという特殊ルールが実装されています。それ以外のときは従前通り intrinsic
という型名としてパースされます。
この形の intrinsic
はコンパイラに特別扱いされ、コンパイラが知っている特定の名前の型エイリアスでなければエラーになります。そのため、 intrinsicMarkerType
の振る舞いがユーザーに露出されることは厳密にはありえるものの、そのような場合は必ず型エラーが存在することになるので、事実上露出されていないと考えて問題ないでしょう。
undefined
undefinedには3種類の亜種があります。標準的なundefinedは undefinedType
です。
undefinedWideningType
undefinedWideningType
は名前の通りwideningの対象となる undefined
型です。TypeScript 2.0で導入されました。
この区別は --strictNullChecks
が有効ではないときにのみ発生します。 --strictNullChecks
が有効なときは2つの undefined
型は内部的にも同じものになります。
--strictNullChecks
が有効ではないとき、 undefined
はwideningで any
に変化します。しかし、明示的に undefined
型を指定したときはwideningから除外したいため、 undefined
リテラルに由来するときだけ undefinedWideningType
として区別しています。
// Without --strictNullChecks
declare function id<T>(x: T): T;
let x = id(undefined); // any
let y = id(undefined as undefined); // undefined
optionalType
optionalType
はoptional chainingの型検査のために存在するダミーの型です。TypeScript 3.7で導入されました。ユーザーに露出されることはないかもしれませんが、内部的にはundefinedの亜種ということになっているためここで挙げます。
たとえば以下のようなoptional chainを考えます。
declare const x: { foo: { bar: number }} | undefined;
declare const y: { foo: { bar: number } | undefined} | undefined;
const test1 = x?.foo.bar;
const test2 = (x?.foo).bar; // error
const test3 = y?.foo.bar; // error
test2では x
がundefinedであるとき、 .foo
までしかスキップされないため型エラーになります。 test3では x.foo
がundefinedである可能性があるため型エラーになります。
この区別を実現するために、TypeScriptでは以下のように型を変形します。 (以下 optionalType
のあらわす undefined
を optional
と表記)
- Optional chainの開始位置では
T
をNonNullable<T> | optional
に変換します。 - Optional chainの各ステップでは
| optional
を取り除いてから操作をし、その結果に| optional
を再度付与します。 - Optional chainの終了位置では
| optional
を| undefined
に置き換えます。
先の例の test3
では y?.foo
の型は { bar: number } | undefined | optional
となり、 | optional
を取り除いても { bar: number } | undefined
となるため、次のプロパティチェインで型エラーになります。これを実現するために undefined
と optional
が区別されています。
null
nullには2種類の亜種があります。標準的なnullは nullType
です。
nullWideningType
undefinedWideningType
と同じです。
// Without --strictNullChecks
declare function id<T>(x: T): T;
let x = id(null); // any
let y = id(null as null); // null
false / true
falseには2種類の亜種があります。標準的なfalseは falseType
です。新鮮なfalse型 (fresh false type) とも呼ばれます。
true についても同様です。
regularFalseType
wideningの振る舞いを決めるための型です。TypeScript 3.1で導入されました。
false
リテラル (式) に由来する false
は新鮮なfalse, false
型リテラルに由来する false
は正規なfalseになります。新鮮なfalseはwideningの対象になります。
function id<T>(x: T): T { return x; }
let x = id(false); // boolean
let y = id(false as false); // false
なお、この "fresh" / "regular" という関係は、 true/false以外の型にも存在します。新鮮な型の場合、左辺に存在しないプロパティーが右辺にあるときはエラーとして扱われます。
function f(x: { foo?: number, bar?: string }) {}
f({ foo: 42, barr: "bar" }); // error
f({ foo: 42, barr: "bar" } as { foo: number, barr: string }); // OK
なお、新鮮/正規の区別にかかわらず、共通部分のない代入はエラーとして扱われます。
function f(x: { foo?: number, bar?: string }) {}
f({ barr: "bar" }); // error
f({ barr: "bar" } as { barr: string }); // error
never
neverには5種類の亜種があります。標準的なneverは neverType
です。
silentNeverType
silentNeverType
はフロー型において暫定的に置かれた never
型をあらわします。TypeScript 2.1で導入されました。
TypeScriptの never
は (どうせそこに到達しないので) 本来であればどのような操作も許容されるはずですが、問題に気付きやすくするためにいくつかの操作は型エラーとして扱われます。
declare const x: never;
console.log(x.toString()); // error
フロー型では型の絞り込みによって never
型が発生することがあります。この場合も通常はエラーになります。
let x: number | string = "foo";
if (typeof x === "string") {
console.log(x.toString());
} else {
// x: never なのでエラー
console.log(x.toString());
}
しかし、エラーになると困る場合もあります。それはフロー型の反復処理が必要な場合です。たとえば以下の例を考えます。
let x: number | string = "foo";
for (let i = 0; i < 3; i++) {
if (typeof x === "string") {
x = 42;
} else {
console.log(x);
}
}
このようにループがある場合、TypeScriptはフロー型の解決のために反復処理を行います。最初の反復では次のように型がつきます。
"1反復目" {
// この時点でのxのフロー型は string
if (typeof x === "string") {
// ここでは xのフロー型は string
x = 42;
// ここでは xのフロー型は number
} else {
// ここでは xのフロー型は never
console.log(x);
}
// ここでは xのフロー型は number
}
すると、ループの入口のフロー型を string
としていたのは不十分で、 string | number
に拡大して処理する必要があることがわかります。そこで、2反復目を行います。
"2反復目" {
// この時点でのxのフロー型は string | number
if (typeof x === "string") {
// ここでは xのフロー型は string
x = 42;
// ここでは xのフロー型は number
} else {
// ここでは xのフロー型は number
console.log(x);
}
// ここでは xのフロー型は number
}
これで収束したのでxのフロー型が確定します。
さて、このフロー型の計算の途中 (1反復目) では x
のフロー型が never
になるところがありました。ここでソースコードを以下のように変えるとどうでしょうか。
let x: number | string = "foo";
for (let i = 0; i < 3; i++) {
if (typeof x === "string") {
x = 42;
} else {
console.log(x.toString());
}
}
この場合最初の反復で never
型へのプロパティアクセスが発生してしまいます。
これを防ぐため、反復途中のフロー型 (不完全型) が never
のときは宣言型にフォールバックするようになっていましたが、これにはコーナーケースがありました。そのため、より根本的な対応として、不完全型の never
を silentNeverType
として出力するようにしました。
silentNeverType
は基本的には never
と同じですが、以下のような式で型エラーにならずそのまま伝搬されます。
- 関数呼び出し、new呼び出し
- プロパティアクセス、要素アクセス
- 単項演算、二項演算、
instanceof
,in
- 複合代入演算
silentNeverType
のもう1つの用法
TypeScript 2.4で導入されました。関数呼び出し式のコンテキスト型から関数のジェネリクス引数を推論するとき、適切な候補がなかったときのプレースホルダーとして使われています。今なら次に述べる nonInferrableType
で置き換えられるかもしれません。
nonInferrableType
TypeScript 3.5で導入されました。
関数呼び出し式のコンテキスト型から関数のジェネリクス引数を推論するとき、その推論自体が別の関数呼び出し式の引数の一部として行われていた場合で特定の場合は、推論を延期する仕様がありました。外側の関数の推論を完了してからあらためて推論したほうが正確な推論結果を得られるからです。
このとき、内側の関数呼び出し式の暫定的な型として silentNeverType
が使われていたのですが、関数を返す場合などに正しく推論されないバグがありました。この問題を解消するために、 silentNeverType
のようなプロパゲーションの挙動を持たず、単に単一化の対象とならないだけの nonInferrableType
という亜種を用意することになったようです。
implicitNeverType
implicitNeverType
は空タプルに使われる never
型です。TypeScript 2.7で導入されました。 (関連コミット: 2010c4c, 1ae0b46)
--strictNullChecks
が有効のとき、空のタプル型 []
がwideningの対象になると never[]
に拡大されます。このときの never
に付与されるのが implicitNeverType
です。
主な効果は次の通りです。JavaScriptモード (*.js
または *.jsx
) で implicitNeverType
の配列型が代入されるとエラーになります。
// With --strictNullChecks
// @ts-check
/**
* @template T
* @param {T} x
* @return {T}
*/
function id(x) { return x; }
let x = id([]); // error
let y = []; // OK (この形の右辺式だけ特別扱い)
/** @type {never[]} */
const z = [];
let w = id(z); // OK (通常のnever型の配列になる)
unreachableNeverType
unreachableNeverType
は制御フロー解析で到達不能ノードに付与される型です。TypeScript 3.7で導入されました。
到達不能コード内で変数が参照されたとき、フロー型の通常の規則に基づくと never
型が割り当てられます。しかし、到達不能性のせいでそうなっている場合はかわりに宣言型にフォールバックするようになっています。おそらく never
を割り当ててもあまり有益ではないからでしょう。
const x: number | string = "foo";
if (true) {
const y = x; // string
} else {
// Errors unless --allowUnreachableCode is specified
const y = x; // string | number
}
まとめ
TypeScriptでは同じように表示される型でも内部的に区別のある場合があります。そのうちプリミティブ型である any
, undefined
, null
, true
/false
, never
の区別について解説しました。完全に内部的な型を除くと、Wideningの挙動を制御したり、オプショナルな型エラーを抑制したりなど細かな調整のために使われているものが多くを占めていました。そのため、TypeScriptプログラミングの役にはあまり立たないと思われる一方、TypeScriptコンパイラの奥深さを理解するのにはよい題材だったと思います。
Discussion
ありがたいです。
ただ 「nullには3種類の亜種があります。」とあるのに2種しか列挙してないように見えます。
指摘ありがとうございます。最初に引用したツイートの通り、2種類と書くのが正しいのでそのように修正しました。