TypeScriptのissueでたまたまDiff型に対する議論を発見したので現段階での表現方法を紹介します。
Add support for literal type subtraction · Issue #12215 · Microsoft/TypeScript
以下ではissueで紹介されている型の名前と定義を若干変えて紹介します。
Diff型とは何か
Diff型とはA
とB
の型の差分を推論する型です。FlowではUtility Typesとして提供されています。
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>
では、
- Tに含まれるstring literal typesからKeyMirror型をつくる
- Uに含まれるstring literal typesからNeverMap型をつくる
- 1と2のintersection typesからvalueの型を参照する
- 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
しています。これは言葉で説明するより型定義を見たほうがわかりやすいですね。
(K
はK 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
を表現することができます。
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の$Diff
はDiff & 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>
は文字通りT
をU
で完全に上書きすることができます。
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
型を紹介させていただきました。
気になる点がございましたらご指摘よろしくお願い致します。