はじめに
type-challengesに挑戦中にジェネリクスとextendsの組み合わせで混乱したのでまとめます
目次
extendsとは
extendsとはtype分で型引数を指定するときに型の絞り込みを行える機能のことです。
例えば、下記の例ではtype Fooに対して<T extends string>
という型引数を与えて型の絞り込みを行っています。
なので、value: T
のT
はstring型
になるということがわかります。
この場合string型
に絞っているのでFoo<number>
とすることはできません。
この構文ではジェネリクスを使用しているのでジェネリクスの理解も欠かせませんが一言で言うと、ジェネリクスは型の引数です。
以上をまとめると下記コードでは「型の引数であるTにextends
で型の制約をつけてstring型
に絞っている」と言えるのではないでしょうか。
type Foo<T extends string> = {
value: T;
};
どうやって使うか
最初に挙げた例は実用的ではありませんでした。実際にTをstring型に絞り込みたいだけなら最初からstring型でいいわけです。実際にextendsが必要な場面はいくつかありますが、主な用途は
①複数人で開発している際に他の人が書いたコードでもどんな値を返り値で受け取るのか大体わかる
実務での開発時にパッとみただけで大体何の返り値を受け取るのかがわかるので関数を追っていく時間がだいぶ短縮されて効率が上がります。
②型の絞り込みをすることによってコンパイルエラーを防げる
TypeScriptの場合、引数で指定した引数にメソッドチェーンで処理を記述しようとするとコンパイルエラーが出る場合があります。そのメソッドを持つ引数が与えられるかわからないためです。例です。
changeBackgroundColor()という関数を例に考えてみます。この関数は指定されたHTML要素の背景色を変更して、そのHTML要素を返す関数です。
ジェネリクス型Tを定義することでHTMLButtonElementやHTMLDivElementなどの任意のHTML要素を受け取れるようにしています。
このコードはコンパイルに失敗します。ジェネリクスの型Tは任意の型が指定可能なので、渡す型によってはstyleプロパティが存在しない場合があるからです。コンパイラは存在しないプロパティへの参照が発生する可能性を検知してコンパイルエラーとしているのです。
function changeBackgroundColor<T>(element: T) {
// Property 'style' does not exist on type 'T'.(2339)
element.style.backgroundColor = "red";
return element;
}
上記の例を見るとelement.style.backgroundColor
と記載されていますが、この状態だとジェネリクスTが引数で渡されたときに本当にstyle.backgroundColor
というメソッドが存在するのかわからないわけです。なのでextendsで型を指定してあげることによってstyle.backgroundColor
を持つメソッドが引数として確実に渡されるということを教えてあげるわけです。下記の通りextendsを追加することによってHTMLElementの範囲内で引数を渡されることが確定するのでコンパイルエラーは防げます。
function changeBackgroundColor<T extends HTMLElement>(element: T) {
element.style.backgroundColor = "red";
return element;
}
https://typescriptbook.jp/reference/generics/type-parameter-constraint
以上が私の考えるextendsのメリットです。(間違いあれば指摘いただけるとありがたいです!)
【蛇足】応用
今回本当に書きたかったのはこっちです。なぜなら、もともとtype-challengesで躓いてしまったのがきっかけでextendsを調べ直したからです。
私が躓いたのはeasyのExcludeです。(情けない)
この問題はTypeScriptのExcludeの機能を自身で記載するというものです。ここからはネタバレなので自身で取り組みたい人はブラウザを閉じてください。
Excludeは第一引数で指定したユニオン型に対して、第二引数で指定した値を第一引数から取り除くというものです。
type Grade = "A" | "B" | "C" | "D" | "E";
type PassGrade = Exclude<Grade, "E">;
// "A" | "B" | "C" | "D"
答え
type MyExclude<T, U> = T extends U ? never : T
答えを見ても初めは分からなかったので自分で色々と調べた結果、理解するために必要な要素が3つわかりました。
・extends(今回の投稿で説明)
・conditional types(分配機能)
・never型の挙動
- extends
こちらは先ほど説明した通りなのですが、私はT extends U
がいまだにしっくり来ません。なぜならUがTを内包しているというのがT extends U
だと思うのですが、今回の場合Uは第二引数で文字列を指定してその値をTから除外する役割となります。なのでT('a' | 'b' | 'c'
)の場合U(例えばaを第二引数で指定した場合'a' | 'b' | 'c', a
)となるのでTがUを内包しているように見えるからです。
今回のextendsは「UがTを内包する」という理解だと少し理解が遅れます。次に説明する分配機能に関わってくるのですが、詳しくは「UはTnを内包する場合はneverを返し、そうじゃない場合はTnを返す。」となります。言葉では分かりにくいと思うのでコードで書くと以下のようになります。
type abc = |a | b |c
abc extends a ? never : T
// ↓ abc extends a ? never : T の処理内容
//(a extends a ? never : a)
//(b extends a ? never : b)
//(c extends a ? never : c)
- conditional types(分配機能)
私は勘違いをしていました。conditional typesは三項演算子のようなものだと思っていたのですが、Union型に対しては挙動が変わります。これは一言でいうと分配を行います。
// Union型abcに対してUnion型を展開してa、b、cそれぞれに対して処理を行う
type abc = |a | b |c
// 以下のようにUnion型を展開する
//(a extends a ? never : a)
//(b extends a ? never : b)
//(c extends a ? never : c)
abc extends a ? never : T
- never型の挙動
never型には値を返さないという特徴があります。他にも色々とありますが割愛します。
これをUnion型に応用することができます。
存在し得ない型を Union Types に追加しても何の変化も生まれないため、このような結果になる。
type Foo = 1 | 2; // type Foo = 1 | 2
type Bar = 1 | 2 | 3; // type Bar = 1 | 2 | 3
type Baz = 1 | 2 | 3 | never; // type Baz = 1 | 2 | 3
つまり、Union型に追加しても何も返しません。
まとめ
・extends(今回の投稿で説明)
・conditional types(分配機能)
・never型の挙動
上記3点を理解することでこの問題を解けることがわかりました。
参考文献