TypeScriptでDiff型を表現する

  • 15
    Like
  • 0
    Comment

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

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

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

Diff型とは何か

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

$Diff - Utility Types | Flow

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

TypeScriptでの表現方法

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

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の差分を推論する型です。推論結果は以下のようになります。

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

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

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

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しています。

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の型を参照します。

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

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を使った応用型(Omit / Diff / WeakDiff / Overwrite)を紹介します。

Omit

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と逆の働きをします。

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しています。これは言葉で説明するより型定義を見たほうがわかりやすいですね。

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

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

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

Diff / WeakDiff

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

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

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

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

type WeakDiff<T, U> = Diff<T, U> & {[K in (keyof U & keyof T)]?: T[K]};
type T4 = WeakDiff<T1, T2>; // { b: number; c: boolean; } & { a?: number }

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

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

Overwrite

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

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

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

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

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

おわりに

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

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