TypeScriptの型定義(d.ts)を読んでいると、
いろいろな型パズルが出てきてわけわかんなくなってきたので、
anyやnullなどの特殊な型同士をいじくったときの結果をまとめた。
TypeScript3.4.3で確認。動作を見ただけで仕様を当たったわけではない点に注意。
[追記]
strictNullChecksの存在をすっかり忘れていたので追加。直感通りの素直な結果ではあった。
Union Types
type Union<A, B> = A | B;
"strictNullChecks": false
の場合
A|B | any | unknown | string | void | null | undefined | never |
---|---|---|---|---|---|---|---|
any | any | any | any | any | any | any | any |
unknown | unknown | unknown | unknown | unknown | unknown | unknown | |
string | string | string|void | string | string | string | ||
void | void | void | void | void | |||
null | null | null | null | ||||
undefined | undefined | undefined | |||||
never | never |
"strictNullChecks": true
の場合(異なる部分は太字)
A|B | any | unknown | string | void | null | undefined | never |
---|---|---|---|---|---|---|---|
any | any | any | any | any | any | any | any |
unknown | unknown | unknown | unknown | unknown | unknown | unknown | |
string | string | string|void | string|null | string|undefined | string | ||
void | void | void|null | void|undefined | void | |||
null | null | null|undefined | null | ||||
undefined | undefined | undefined | |||||
never | never |
"strictNullChecks":true
や"strict":true
(含strictNullChecks)によって以下のケースを除外できる。
let a: string = null; // strictNullChecks: trueではエラー
voidについて。
voidは関数の返り値が無いことを示すが、実際は値を返してもエラーにならない。
ただし、厳密にvoidではない場合はきちんとチェックされる。
type Returns<T> = () => T;
// 返り値型が void なら何返してもOK
const a: Returns<void> = () => 1; // OK
// 返り値型が string|void ならそれ以外はNG
const b: Returns<string|void> = () => 1; // NG
// void値は真偽値判定できないという違いもある
if (a()) { // エラー
a() || true; // エラー
}
if (b()) { // OK
b() || true; // OK
}
さきほどの表でstring|void
がそのまま残っているのはこのチェックのためだろう。
Intersection types
type Intersection<A, B> = A & B;
"strictNullChecks": false
の場合
A & B | any | unknown | string | void | null | undefined | never |
---|---|---|---|---|---|---|---|
any | any | any | any | any | any | any | never |
unknown | unknown | void | string | null | undefined | never | |
string | string | string & void | null | undefined | never | ||
void | void | null | undefined | never | |||
null | null | undefined | never | ||||
undefined | undefined | never | |||||
never | never |
"strictNullChecks": true
の場合(異なる部分は太字)
A & B | any | unknown | string | void | null | undefined | never |
---|---|---|---|---|---|---|---|
any | any | any | any | any | any | any | never |
unknown | unknown | void | string | null | undefined | never | |
string | string | string & void | string & null | string & undefined | never | ||
void | void | void & null | void & undefined | never | |||
null | null | null & undefined | never | ||||
undefined | undefined | never | |||||
never | never |
さっきと逆になると思いきや、ここでもanyが強い。
ただ、anyとneverはneverが勝つ。
type a = any | any & never; // => any
type b = (any | any) & never; // => never
type c = any | (any & never); // => any
型演算は右結合のようだ。
Extends
特殊な型同士のExtendsの関係性を見てみる。
継承というより代入可能性と考えたほうがわかりやすいかもしれない。
type Extends<A, B> = A extends B ? "○" : "×";
"strictNullChecks": false
の場合
A extends B | (B) any | unknown | string | void | null | undefined | never |
---|---|---|---|---|---|---|---|
(A) any | ○ | ○ | ○|× | ○|× | ○|× | ○|× | ○|× |
unknown | ○ | ○ | × | × | × | × | × |
string | ○ | ○ | ○ | × | × | × | × |
void | ○ | ○ | × | ○ | × | × | × |
null | ○ | ○ | ○ | ○ | ○ | ○ | × |
undefined | ○ | ○ | ○ | ○ | ○ | ○ | × |
never | never | never | never | never | never | never | never |
"strictNullChecks": true
の場合(異なる部分は太字)
A extends B | (B) any | unknown | string | void | null | undefined | never |
---|---|---|---|---|---|---|---|
(A) any | ○ | ○ | ○|× | ○|× | ○|× | ○|× | ○|× |
unknown | ○ | ○ | × | × | × | × | × |
string | ○ | ○ | ○ | × | × | × | × |
void | ○ | ○ | × | ○ | × | × | × |
null | ○ | ○ | × | × | ○ | × | × |
undefined | ○ | ○ | × | ○ | × | ○ | × |
never | never | never | never | never | never | never | never |
へんてこなのはanyとneverで、
anyはstringやvoidを継承している状態と継承していない状態の両方を併せ持っている。
neverはそもそも判定すらされず、全体がneverと評価されてしまう。
この挙動を利用して、例えばanyを判定する型を作ることができる。
// neverを継承するかの判定
// この型は、Tがanyのときtrue|false, neverのときnever, それ以外のときfalseになる
type ExtendsNever<T> = T extends never ? true : false;
// booleanはtrue|falseなので、booleanとマッチするかどうかでanyを取り出せる
// (ちなみにこの型はReactの型定義でも使われている)
type IsExactlyAny<T> = boolean extends ExtendsNever<T> ? true : false;
// 同じ要領でneverを判定
type IsExactlyNever<T> = false extends ExtendsNever<T> ? false : true;
// unknownを判定してみよう
ちなみに、union distributionを発生させないようにextendsを変えた場合、結果はまた違ったものになる。
type Extends<A, B> = A[] extends B[] ? "○" : "×";
A[] extends B[] | (B) any | unknown | string | void | null | undefined | never |
---|---|---|---|---|---|---|---|
(A) any | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
unknown | ○ | ○ | × | × | × | × | × |
string | ○ | ○ | ○ | × | × | × | × |
void | ○ | ○ | × | ○ | × | × | × |
null | ○ | ○ | × | × | ○ | × | × |
undefined | ○ | ○ | × | ○ | × | ○ | × |
never | ○ | ○ | ○ | ○ | ○ | ○ | ○ |
never extends never ? true : false
はneverだけど、
never[] extends never[] ? true : false
はtrueというわけ。ややこしいね。
型パズルで遊ぶのもほどほどに。