👬

TypeScriptにはanyが4種類、undefinedが3種類、……

2021/01/02に公開
2

このツイートの解説をします。

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[] 型の anyautoType になる。
    • let 宣言または var 文の一部である。 (const は含まない)
    • export 修飾子も declare 修飾子もついていない。
    • 構造化束縛の一部ではない。
    • --noImplicitAny が有効であるか、または当該宣言が *.js / *.jsx ファイル内にある。
    • 初期化子が [] である。

autoType が通常の any 区別して扱われる場面もまた色々ありますが、主要なのは --noImplicitAny のエラー判定です。 autoType やその配列型に推論された変数はエラーになります。

main.js
// 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かどうかを判定するには、条件をできるだけ緩和して評価すればいいので、 TU に出現する型引数を「判定できない」ことを表す any に置き換えて評価すればいいことになります。しかし、通常の any は気を利かせて余計な推論を行ってしまうことがあり、それを抑制した専用の wildcardType を使うことになります。たとえば以下のような振る舞いがあります。

  • wildcardType を含む合併/交叉は wildcardType
  • wildcardTypekeyofwildcardType
  • T または KwildcardType のとき T[K]wildcardType
  • T または UwildcardType のとき T extends U ? V : WwildcardType

errorType

errorType は名前の通り型エラーが発生したときに処理を続行するために割り当てられる型です。microsoft/TypeScriptのmasterから辿れる最初のバージョンから unknownType という名前で存在し、その後TypeScript 3.0で errorType にリネームされました。

内部的な名前 (intrinsic name) は error ですが、フラグ上の扱いは any に準じ、ユーザーに提示される名前も any になります。

errorTypeany よりも強く伝搬されることがあります。

// この行自体はもちろんエラーになる
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の標準型定義で以下のように定義されています。

es5.d.ts
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 のあらわす undefinedoptional と表記)

  • Optional chainの開始位置では TNonNullable<T> | optional に変換します。
  • Optional chainの各ステップでは | optional を取り除いてから操作をし、その結果に | optional を再度付与します。
  • Optional chainの終了位置では | optional| undefined に置き換えます。

先の例の test3 では y?.foo の型は { bar: number } | undefined | optional となり、 | optional を取り除いても { bar: number } | undefined となるため、次のプロパティチェインで型エラーになります。これを実現するために undefinedoptional が区別されています。

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 のときは宣言型にフォールバックするようになっていましたが、これにはコーナーケースがありました。そのため、より根本的な対応として、不完全型の neversilentNeverType として出力するようにしました。

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 の配列型が代入されるとエラーになります。

index.js
// 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

Tsuyoshi CHOTsuyoshi CHO

ありがたいです。
ただ 「nullには3種類の亜種があります。」とあるのに2種しか列挙してないように見えます。

Masaki HaraMasaki Hara

ただ 「nullには3種類の亜種があります。」とあるのに2種しか列挙してないように見えます。

指摘ありがとうございます。最初に引用したツイートの通り、2種類と書くのが正しいのでそのように修正しました。