🧠

type-challengeの型同士の厳密な等価評価に使われているEqual<X, Y>

2022/03/21に公開

type-challengeというTypeScriptの型について学習するためのプロジェクトがある。これは型に関する問題とそのテストケースが提示され、それを満たす実装を書いてコンパイルエラーを無くしていくという流れで進む。

例えばこんな感じの問題。与えられた型がタプルの場合はそのサイズを返すという型を書く問題。type Length<T extends readonly any[]> = T['length']とでも書けばこれはOK。

/* _____________ Your Code Here _____________ */

type Length<T extends any> = any


/* _____________ Test Cases _____________ */
import { Equal, Expect } from '@type-challenges/utils'

const tesla = ['tesla', 'model 3', 'model X', 'model Y'] as const
const spaceX = ['FALCON 9', 'FALCON HEAVY', 'DRAGON', 'STARSHIP', 'HUMAN SPACEFLIGHT'] as const

type cases = [
  Expect<Equal<Length<typeof tesla>, 4>>,
  Expect<Equal<Length<typeof spaceX>, 5>>,
  // @ts-expect-error
  Length<5>,
  // @ts-expect-error
  Length<'hello world'>,
]

こういった問題がレベル別でたくさん投稿されているので、step by stepでTypeScriptの型に習熟していくのに最適である。

自分は最近このプロジェクトの問題を解き始めたところなのだが、その中でふと型のテストケースの中で利用されているEqualという型が気になった。これは標準のユーティリティ型ではなく@type-challenges/utilsで定義されている独自の型だ。この中を覗いてみると実装はこんな感じ。

type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

目的はXとYを型レベルで厳密に等価かどうか比較すること。

実装はConditional Typeを使っているだけだが、最初見た時なぜこれが下記ではないのか疑問だった。これでも十分では?と思ったのだ。

type Equal<X, Y> = X extends Y ? true : false

なぜ<T>() => T extends X ? 1 : 2 extends (<T>() => T extends Y ? 1 : 2) ? true : falseというような書き方をしているのだろうか。

自分の知識では考えても理解ができなかったので少し調べてみると、下記の2つのリンクで大体どういうことか説明されていた。自分なりにこんな感じか?と理解したことをメモしてみる。

https://github.com/microsoft/TypeScript/issues/27024
https://stackoverflow.com/questions/68961864/how-does-the-equals-work-in-typescript

まずConditional Type内の<T>() => Tが未確定なので遅延評価になる。

この場合(<T>() => T extends X ? 1 : 2)(<T>() => T extends Y ? 1 : 2)に代入可能であればtrue、そうでなければfalseを返すようになるというコードであるが、その代入可能かどうかを決めるのはisTypeIdenticalToという内部的な実装である。

isTypeIdenticalToソースコードでこう定義されている。

isTypeIdenticalToの実装。長いので省略。
// TYPE CHECKING
function isTypeIdenticalTo(source, target) {
    return isTypeRelatedTo(source, target, identityRelation);
}

function isTypeRelatedTo(source, target, relation) {
    if (isFreshLiteralType(source)) {
        source = source.regularType;
    }
    if (isFreshLiteralType(target)) {
        target = target.regularType;
    }
    if (source === target) {
        return true;
    }
    if (relation !== identityRelation) {
        if (relation === comparableRelation && !(target.flags & 131072 /* Never */) && isSimpleTypeRelatedTo(target, source, relation) || isSimpleTypeRelatedTo(source, target, relation)) {
            return true;
        }
    }
    else {
        if (source.flags !== target.flags)
            return false;
        if (source.flags & 67358815 /* Singleton */)
            return true;
    }
    if (source.flags & 524288 /* Object */ && target.flags & 524288 /* Object */) {
        var related = relation.get(getRelationKey(source, target, 0 /* None */, relation));
        if (related !== undefined) {
            return !!(related & 1 /* Succeeded */);
        }
    }
    if (source.flags & 469499904 /* StructuredOrInstantiable */ || target.flags & 469499904 /* StructuredOrInstantiable */) {
        return checkTypeRelatedTo(source, target, relation, /*errorNode*/ undefined);
    }
    return false;
}

基本的にはsourcetarget===を用いて厳密な等価評価をするのがメイン。

色々書いたが結局のところ要は下記のy = xがエラーになるか(代入可能か)どうかに帰着する。

let x: <T>() => (T extends X ? 1 : 2)
let y: <T>() => (T extends Y ? 1 : 2)
y = x

これを内部的に決めるのはisIdenticalTo(X, Y)であり、つまりX === Yをしてるのと同値。
? 1 : 2の部分はどういう意味が???と思っていたがanyneverでなければ別に何でも良いっぽかった。難しい。

ということでEqual<X, Y>はこういう実装になっているらしい。自分が最初にイメージしていた下記の実装だと一部の値型同士等ではうまくいくが、うまくいかないケースも結構ある。

type Equal<X, Y> = X extends Y ? true : false

例えば下記。

Equal<true, boolean>

これは厳密な等価評価をして欲しいのでtrue === booleanであり、falseを返して欲しい。しかし自分の実装だとtrue extends booleanになり、これはtrueを返す。こういう例がいくつかある。

よってこの実装が採用されているということらしい。

type Equal<X, Y> =
  (<T>() => T extends X ? 1 : 2) extends
  (<T>() => T extends Y ? 1 : 2) ? true : false

ただこの実装はEqual<{x:1}&{y:2}, {x:1,y:2}>のような交差型では使えなくて、というのもisIdenticalToの内部実装のsource.flags !== target.flagsの部分でfalseになってしまうからとのこと。

TypeScriptの型パズルは難しい。自分でもまだ理解が曖昧な部分も多々あるので間違っていたら指摘してもらえるとありがたい。

Discussion