TypeScriptでDiff型を表現する

TypeScriptのissueでたまたまDiff型に対する議論を発見したので現段階での表現方法を紹介します。

[Add support for literal type subtraction · Issue #12215 · Microsoft/TypeScript][issue]

以下では[issue][issue]で紹介されている型の名前と定義を若干変えて紹介します。

# Diff型とは何か

Diff型とは`A`と`B`の型の差分を推論する型です。FlowではUtility Typesとして提供されています。

[$Diff<A, B> - Utility Types | Flow](https://flow.org/en/docs/types/utilities/#toc-diff)

```ts
type A = { a: number, b: string }
type B = { a: number }
type T = $Diff<A, B> // { b: string } & { a?: number }
```

# TypeScriptでの表現方法

```ts
type DiffKey<T extends string, U extends string> = (
    & {[P in T]: P }
    & {[P in U]: never }
    & { [x: string]: never }
)[T];

type Omit<T, K extends keyof T> = Pick<T, DiffKey<keyof T, K>>;

type Diff<T, U> = Omit<T, keyof U & keyof T>;
// $Diff
type WeakDiff<T, U> = Diff<T, U> & {[K in (keyof U & keyof T)]?: T[K]};
```

`Diff`を表現するキモは`DiffKey`です。`DiffKey`の詳しい解説とその応用型を紹介します。

# DiffKey

```ts
type DiffKey<T extends string, U extends string> = (
    & {[P in T]: P } // (1)
    & {[P in U]: never } // (2)
    & { [x: string]: never } // (3)
)[T]; // (4)
```

`DiffKey`はstring literal typesで構成されるunion typesの差分を推論する型です。推論結果は以下のようになります。

```ts
type T = DiffKey<'a' | 'b' | 'c', 'a'> // 'b' | 'c'
```

`DiffKey`の推論過程を理解するためにひとつずつ分解してみます。

(1)では`T`のstring literal typesからkeyとvalueが対になるオブジェクト型にmappingしています。

```ts
type KeyMirror<T extends string> = {[P in T]: P}
type T = KeyMirror<'a'| 'b'> // { a: 'a', b: 'b' }
```

(2)では`U`のstring literal typesからvalueの型がneverになるオブジェクト型にmappingしています。

```ts
type NeverMap<U extends string> = {[P in U]: never}
type T = NeverMap<'a' | 'b'> // { a: never, b: never }
```

(3)は(1) & (2)の型から(4)でindexアクセスするために必要になります。

(4)では`T`のstring literal typesから(1) & (2) & (3)の型の**value**の型を参照します。

わかりやすいようにこの状態をベタ書きしてみます。

```ts
type KeyMirrorAndNeverMap = {
    a: 'a' & never
    b: 'b'
    c: 'c'
}

type T1 = KeyMirrorAndNeverMap['a'] // never
type T2 = KeyMirrorAndNeverMap['b'] // 'b'
```

得られる結果は`string literal types | never`になります。

まとめると、`DiffKey<T extends string, U extends string>`では、

1. Tに含まれるstring literal typesからKeyMirror型をつくる
2. Uに含まれるstring literal typesからNeverMap型をつくる
3. 1と2のintersection typesから**value**の型を参照する
4. 3の結果、`string literal types | never`が得られる

最終的に`never`が消えてstring literal typesのunion typesの差分を取得することができます。

ここまでの説明で登場した型はこちらで触って確認できます。

[Diffkey - TypeScript Playground][diffkey]

次に`DiffKey`を使った応用型(`Omit` / `Diff` / `WeakDiff` / `Overwrite`)を紹介します。

# Omit

```ts
type Omit<T, K extends keyof T> = Pick<T, DiffKey<keyof T, K>>;
```

TypeScriptで提供されている`Pick<T, K extends keyof T>`は`T`から`K`に含まれるstring literal typesを持つオブジェクトを推論する型です。

`Omit`はちょうど`Pick`と逆の働きをします。

```ts
type T1 = { a: number, b: string, c: boolean }
type T2 = Pick<T1, 'a'> // { a: number }
type T3 = Omit<T1, 'a'> // { b: string, c: boolean }
```

`DiffKey`を使い`T`に含まれるkey(`keyof T`)から除外したいkey(`K`)の差分のkeyで`T`から`Pick`しています。これは言葉で説明するより型定義を見たほうがわかりやすいですね。

(`K`は`K extends keyof T`ではなく`K extends string`でも良い気がする)


lodashなどで提供される`omit`関数の返り値の型推論に有効です。

```ts
function omit<T, K extends keyof T>(obj: T, keys: K | K[]): Omit<T, K> {
    const _keys = Array.isArray(keys) ? keys : [keys];
    const clone = Object.assign({}, obj);

    let i = -1;
    const len = _keys.length;
    while (++i < len) delete clone[_keys[i]];

    return clone as any;
}

const obj = { a: 1, b: '', c: true };
const result = omit(obj, 'a'); // { b: '', c: true }
```

`Omit`のおかげで、返り値の型を`Partial<T>`より厳密にすることができます。また`Omit`を使って次の`Diff`を表現することができます。

[Omit - TypeScript Playground][omit]


# Diff / WeakDiff

```ts
type Diff<T, U> = Omit<T, keyof T & keyof U>;
```

`Diff<T, U>`は`T`から`U`を引いた型を推論することができます。

```ts
type T1 = { a: number, b: string, c: boolean }
type T2 = { a: number }
type T3 = Diff<T1, T2> // { b: string, c: boolean }
```

Flowの`$Diff`は`Diff & T - Uで除外されたオプショナルなプロパティ`になります。

```ts
type WeakDiff<T, U> = Diff<T, U> & {[K in (keyof U & keyof T)]?: T[K]};
```

```ts
type T4 = WeakDiff<T1, T2>; // { b: number; c: boolean; } & { a?: number }
```

`WeakDiff`があるとReact.jsなどでよく登場する、propsをinjectするHigh Order Componentが返すComponentのpropsを型安全にすることができます。

```ts
interface SFC<P = {}> {
    (props: P): any;
    defaultProps?: Partial<P>;
}

// defaultPropsをつけるだけのHOC
function withDefaultProps<D extends object>(defaultProps: D) {
    return function enhance<P = {}>(component: SFC<P>): SFC<WeakDiff<P, D>> {
        const _component: SFC<any> = (props: any) => component(props)
        _component.defaultProps = defaultProps;
        return _component as any;
    };
}

// Component
const Counter = (props: { name: string, count: number }) => {/*  */ };
const CounterWithDefaultName = withDefaultProps({ name: 'MyCounter' })(Counter);

CounterWithDefaultName({ count: 1 })
CounterWithDefaultName({ count: 1, name: '' })
CounterWithDefaultName({ name: '' }) // error
```

[Diff / WeakDiff - TypeScript Playground][diff]


# Overwrite

```ts
type Overwrite<T, U> = Diff<T, U> & U;
```

`Overwrite<T, U>`は文字通り`T`を`U`で完全に上書きすることができます。

```ts
type T1 = { a: string, b: number, c: boolean };
type T2 = { a: number, b: string };
type T3 = Overwrite<T1, T2> // { c: boolean } & T2
```

`Overwrite`はinterfaceの一部分だけを書き換えたい場合に便利です。

```ts
interface Todo {
    id: number;
    content: string;
    completed: boolean;
}

type NewTodo = Overwrite<Todo, { id?: number }>;
type TodoRecord = Overwrite<Todo, { completed: 0 | 1 }>
```

`NewTodo`はidだけがoptionalに上書きされます。
`TodoRecord`はcompletedの型だけが`0 | 1`に上書きされます。

[Overwrite - TypeScript Playground][overwrite]

# おわりに

[Add support for literal type subtraction · Issue #12215 · Microsoft/TypeScript][issue]を元に私が理解できた範囲で`Diff`型を紹介させていただきました。

気になる点がございましたらご指摘よろしくお願い致します。


[issue]:https://github.com/Microsoft/TypeScript/issues/12215
[diffkey]:https://goo.gl/wUCxbu
[omit]:https://goo.gl/q1H5bu
[diff]:https://goo.gl/Z2G2Qu
[overwrite]:https://goo.gl/jPfL9T