TypeScript でモジュール外では実装できないインターフェースを定義する

## 問題意識

ライブラリを書いていると、このインターフェースは、使い方を公開してるだけで、実装してほしいわけじゃないんだよなあ、というケースが結構ある。

例えば次のような

~~~ts:ライブラリ側
export function createMyNode(): MyNode {
    return new MyNodeImpl();
}

export interface MyNode {
    readonly parent: MyNode | null;
    readonly children: readonly MyNode[];
    append(node: MyNode): void;
}

class MyNodeImpl implements MyNode {
    private _parent: MyNodeImpl | null = null;
    private _childern: MyNodeImpl[] = [];
    get parent() { return this._parent; }
    get children() { return this._childern; }

    append(node: MyNode) {
        if (!(node instanceof MyNodeImpl)) {
            throw new Error('oops!');
        }
        if (node._parent === null) {
            node._parent = this;
            this._childern.push(node);
        }
        // ... 略
    }
}
~~~

このコードは、次のような誤った使い方を静的に防げない。

~~~ts:利用者側
const node = createMyNode();
node.append({
  parent: null,
  children: [],
  append(node) { this.children.push(node); }
});
~~~

`append` に適当に `MyNode` を実装したオブジェクトを突っ込むことができてしまうけれど、それを受け取ったところで正常に処理はできない。

コードにあるように、チェックして実行時にエラーを出すことはできるけれど、実行時にコストがかかるし、あんまりうれしくない。

インターフェースじゃなくてクラスにする手は完全には解決にならないし、他の問題を色々と呼び込むので、それはそれで避けたいところ。

せっかくの**Type**Scriptなのだし、型で防ぎたいよね。

と、いうわけで、

## 実装できないインターフェースを書く

~~~ts:ライブラリ側・改
export function createMyNode(): MyNode {
    return new MyNodeImpl();
}

declare const seal: unique symbol; // (1)
export interface MyNode {
    [seal]: undefined; // (2)
    readonly parent: MyNode | null;
    readonly children: readonly MyNode[];
    append(node: MyNode): void;
}

class MyNodeImpl implements MyNode {
    [seal]: undefined; // (3)
    private _parent: MyNodeImpl | null = null;
    private _childern: MyNodeImpl[] = [];
    get parent() { return this._parent; }
    get children() { return this._childern; }
    
    append(node: MyNodeImpl) { // (4)
        if (node._parent === null) {
            node._parent = this;
            this._childern.push(node);
        }
        // ... 略
    }
}
~~~

(1)
モジュールの中で `symbol` を宣言する。`export` はしない。
型システム上で防ぐだけなので、`symbol` の実体は要らない。アンビエント宣言で済ませる。

(2)(3)
それをインターフェースと実装クラスに、それぞれプロパティとして追加。
値も要らないので型は `undefined` にしておく(`never`でもいいかも)。
初期化は要らない。javascript に変換されれば、追加部分は跡形もなくなる。

(4)
最後に、引数はインターフェースではなく実装クラスで受ける。
`instanceof` でのチェックは無くて良い。
一般的には危険だけれど、今回のケースでは `MyNode` の実装は `MyNodeImpl` 以外に存在しないのでセーフ。
TypeScript の引数は bivariant なので、この書き方自体には問題がない。

さて、これでモジュール外では、(普通の方法では) `MyNode` を実装できない。
実装するには `seal` を得る必要があるが、

~~~ts:利用者側・改
const node = createMyNode();
declare const seal: unique symbol;
node.append({
    [seal]: undefined, // ここでエラー
    parent: null,
    children: [],
    append(node) { this.children.push(node); }
});
~~~

同じ名前を付けたところで、それは別のシンボルだ。
tsc はこれをちゃんと弾いてくれる。

めでたしめでたし。

## 抜け道とその対処

「(普通の方法では)」と書いたように、この方法は抜け道がある。

~~~ts:利用者側・改改
const node = createMyNode();

type GetSymbol<T> = {
    [S in keyof T]: S extends symbol ? S : never
}[keyof T];

declare const seal: GetSymbol<MyNode>;
class HackNode implements MyNode {
    [seal]: undefined;
    parent: MyNode | null = null;
    children: MyNode[] = [];
    append(node) { this.children.push(node); }
}
node.append(new HackNode());
~~~

それがインターフェース内で唯一の `symbol` キーならば、抽出できてしまう。
どうしても対抗したければ、こう。

~~~ts:ライブラリ側・改改
declare const seal1: unique symbol;
declare const seal2: unique symbol;
export interface MyNode {
    [seal1]: undefined;
    [seal2]: undefined;
    // 略
}
class MyNodeImpl implements MyNode {
    [seal1]: undefined;
    [seal2]: undefined;
    // 略
}
~~~

安直に二つにする。

この場合、`GetSymbol<MyNode>` は `unique symbol | unique symbol` になるので、一つに絞り込めなくなり上記の方法でも実装できなくなる(何か面白い抜け道あったら教えてください)。

まあ、普通は実装のためにシンボルを引っこ抜こうとか考えないので、利用者の間違いを防ぐ意図なら、二つに増やす対処をする必要は無いように思う。
強引に異なるオブジェクトを突っ込みたいなら、そもそも `as any` で行けてしまうのだし。