Panda Noir

JavaScript の限界を究めるブログでした。最近はいろんな分野を幅広めに書いてます。

TypeScriptのExcludeはなぜT extends K ? never : Tで実装できるのか?

直感に反しているExclude型についてconditional typeの話をしつつ解説します。

Exclude型とは?

Union型から特定の要素を取り除く型です。ある型から特定のプロパティを取り除きたいときに使えます。

interface Person {
  name: string;
  age: number;
  country: string;
}

type PersonWithoutAge = Pick<Person, Exclude<keyof Person, "age">>;

Exclude型の実装と解説

Exclude型は簡潔に実装できます。

type Exclude<T, K> = T extends K ? never : T;

わけがわからないと思うので1つずつ解説します。

extendsキーワードと三項演算子

extendsは「右辺は左辺のサブクラスであるか」を判定します。特にここではプロパティ名同士の比較になるので、単に等しいかどうかを判定しています。

そして、T extends Kが真であったならneverを、偽であったならばTを返します。

動作をみて分かるとおり、これは三項演算子、もっといえばif文のような役割を果たしています。そのため、名前もconditional typeとなっています。

さて、ここまで読んでいて「おや?」と混乱してきませんか?僕もとても困惑しました。なぜならExclude<keyof Person, "age">は書き下すとkeyof Person extends "age" ? never : keyof Personとなるわけで、意味がわからないからです。ご安心ください。これであっています(この書き下しは間違えています)。このあときちんと説明します。

conditional typeは分配法則が適用される

conditional typeはT extends KのTがUnion型の場合、以下のように分配が起きます。

T extends K ? never : T
-> T = (U1 | U2 | U3)とする
-> (U1 extends K ? never : U1) | (U2 extends K ? never : U2) | (U3 extends K ? never : U3)

これを見ればExclude<keyof Person, "age">でなぜうまく行くかはほとんど明らかですね。

参考

TypeScript 2.8 の Conditional Types について - Qiita