同値型を判定する型
November 23, 2021
ref: https://github.com/Microsoft/TypeScript/issues/27024#issuecomment-421529650
ある程度型パズルに慣れている方は、型同士が同じかどうか判定する型と聞いて次のように思いつくのではないでしょうか:
ts
typeEquals <X ,Y > = [X ] extends [Y ] ? ([Y ] extends [X ] ? true : false) : false;
しかし、これは assignability(代入可能かどうか)だけを判定しているため、any
型に対してはうまく動作しません。
ts
typeEquals <X ,Y > = [X ] extends [Y ] ? ([Y ] extends [X ] ? true : false) : false;// should be true, got truetypetest01 =Equals <string, string>;// should be false, got falsetypetest02 =Equals <{foo : string }, {bar : string }>;// should be false, but got truetypetest03 =Equals <any, {bar : string }>;
型が全くの同値であるかを判定するには、「条件付き型同士が割り当て可能になるにはextends
直後の型どうしが同値でなければならない」というチェッカーの性質を利用します。
ts
export typeEquals <A1 extends any,A2 extends any> = (<A >() =>A extendsA2 ? 'assignable': 'not assignable') extends <B >() =>B extendsA1 ? 'assignable': 'not assignable'? true: false;// should be true, got truetypetest01 =Equals <string, string>;// should be false, got falsetypetest02 =Equals <{foo : string }, {bar : string }>;// should be false, got falsetypetest03 =Equals <any, {bar : string }>;
詳細に見るために、Equals
を分解してみます。
ts
declare let x: <A>() => A extends A2 ? 'assignable' : 'not assignable';declare let y: <B>() => B extends A1 ? 'assignable' : 'not assignable';x = y;
x に y が割り当て可能(代入可能)なとき、Equals
の戻り値はtrue
であるということが言えます。
参考にした Issue のコメントでは、次のように述べられていました:
Here’s a solution that makes creative use of the assignability rule for conditional types, which requires that the types after extends be “identical” as that is defined by the checker:
したがって、上記コードにおいて、条件付き型 x に条件付き型 y が割り当て可能であるためには、extends
直後の型A1
とA2
が同値である必要があるということです。
具体的に値を入れて確認してみます。A1
がany
、A2
がstring
であるケース:
ts
declare letx : <A >() =>A extends string ? 'assignable' : 'not assignable';declare lety : <B >() =>B extends any ? 'assignable' : 'not assignable';// エラーが発生して代入できないType '<B>() => B extends any ? "assignable" : "not assignable"' is not assignable to type '<A>() => A extends string ? "assignable" : "not assignable"'. Type 'A extends any ? "assignable" : "not assignable"' is not assignable to type 'A extends string ? "assignable" : "not assignable"'. Type '"assignable" | "not assignable"' is not assignable to type 'A extends string ? "assignable" : "not assignable"'. Type '"assignable"' is not assignable to type 'A extends string ? "assignable" : "not assignable"'.2322Type '<B>() => B extends any ? "assignable" : "not assignable"' is not assignable to type '<A>() => A extends string ? "assignable" : "not assignable"'. Type 'A extends any ? "assignable" : "not assignable"' is not assignable to type 'A extends string ? "assignable" : "not assignable"'. Type '"assignable" | "not assignable"' is not assignable to type 'A extends string ? "assignable" : "not assignable"'. Type '"assignable"' is not assignable to type 'A extends string ? "assignable" : "not assignable"'.= x y ;
A1
とA2
がともにstring
であるケースでは:
ts
declare letx : <A >() =>A extends string ? 'assignable' : 'not assignable';declare lety : <B >() =>B extends string ? 'assignable' : 'not assignable';// 代入可能x =y ;
ではなぜ「条件付き型同士が割り当て可能になるにはextends
直後の型どうしが同値でなければならない」という性質があるのでしょうか。
extends
直後の型が同値でなくても、条件付き型同士が割り当て可能だと仮定してみます:
ts
declare letx : <A >() =>A extends string ? 'assignable' : 'not assignable';declare lety : <B >() =>B extends number ? 'assignable' : 'not assignable';constx_1 =x <string>();consty_1 =y <string>();// @ts-ignorex =y ;// 関数シグネチャの定義から戻り値はx_2は`assignable`のはず// しかし、xにyを代入しており、y_1の戻り値は`not assignable`なので// 戻り値は`assignable | not assignable`のユニオン型でなければ矛盾するconstx_2 =x <string>();
インターセクション型と通常の型は同値とみなされない
{ foo: true } & { bar: false }
と{ foo: true; bar: false }
は同値とみなされないことには注意が必要です。
ts
typeX1 = {foo : true } & {bar : false };typeX2 = {foo : true;bar : false };export typeEquals <A1 extends any,A2 extends any> = (<A >() =>A extendsA2 ? 'assignable': 'not assignable') extends <B >() =>B extendsA1 ? 'assignable': 'not assignable'? true: false;// should be true, but got falsetypetest01 =Equals <X1 ,X2 >;
これが意図したものであるかは不明ですが、同値かどうか比較している部分のソースコードを見ると、フラグ(flags
)を比較していることが分かります。
ts
// https://raw.githubusercontent.com/microsoft/TypeScript/main/src/compiler/checker.tsfunction isTypeRelatedTo(source: Type,target: Type,relation: ESMap<string, RelationComparisonResult>) {// ...if (relation !== identityRelation) {// ...} else {if (source.flags !== target.flags) return false;// ...}// ...}
フラグの定義(TypeFlags
)を見てみると、インターセクション型のフラグとオブジェクト型のフラグが異なるため、同値とみなされないようです。