TypeScriptのextendsってなんなん?
今日ではインターネットなしでは生活できないのと同様に、TypeScriptなしではフロントエンドの開発ができない世の中になっています。
そんなTypeScriptに囲まれた生活をしていると度々extends
を見ることになるでしょう。
今回はこのextends
の理解を深めるために記事を書きました。
Genericsのおさらい
まずはじめに本記事ではGenericsを用いたコードが出てきますので、おさらいします。
以下は最もシンプルなGenericsを用いた記法です。
type A<T> = T
const moji:A<string> = 'moji';
const suji:A<number> = 123;
別名で型引数とも言われるGenericsは受け取った型をそのまま使用する仕組みを作ります。
const f = <T>(arg: T): T => arg;
f('Hello'); // <"Hello">(arg: "Hello") => "Hello"に推論される
f<number>('Hello'); // Type Error
このように関数を作成する際によく使われる印象です。
extends
本題のextends
このextends
はいろんな使い方をしてて混乱していたので、ケース別にまとめてみます。
interfaceを用いた型の継承(拡張)
interface User {
name: string;
}
interface Admin extends User {
isMaster: boolean;
}
// Adminは以下と同じになる
interface Admin {
name: string;
isMaster: boolean;
}
よく知られているextends
はinterface
と組合わせて型を継承するときに使われます。しかし、最近では継承は開発の秩序を乱すという考えもあり、あまり使われない印象があります。
Genericsの型を限定する
const f = <T>(arg: T): string => arg.name;
上記のようなGenericsを使った関数を定義したときに、T
型にはname
プロパティが存在するかわからないためエラーになります。
そこで、T
型はname
プロパティを持っているように制限をかけたいときにextends
を使います。
type User = {
name: string;
age: number;
};
const f = <T extends User>(arg: T): string => arg.name;
f({ name: 'aaa', age: 123 });
f({ age: 123 }); // Type Error
と書くことで、T
型はUser
であることが保証され、name
プロパティを持つことになります。
これはGenericsの渡す方の型の限定というニュアンスで認識するのが良さそうです。
また、
const f = <T extends User | Admin>(arg: T): string => arg.name;
のように複数の型で制限することもできます。
しかし、これら記法は、結局は
const f = (arg: User): string => arg.name;
と書けば良いように思えて、extends
を使うメリットがいまいちわかりませんでした。
Conditional Typeとしてのextends
Conditional TypeはT extends U ? A : B
と書くことで型の条件分岐をすることができます。
これは、Genericsの受け取った側の型を分岐させたいときに使います。
type A<T> = T extends string ? string : number;
type B = A<string>; // string
type C = A<boolean>; // number
type D = A<'hello'>; // string
type E = A<123>; // numbe
これは受け取ったT
型が、stringならstring型、そうでないならnumber型という意味になります。
T extends U
を満たす条件
ここまでで、T extends U
が条件を満たす満たさないの条件式のような役割をしているように思えたので、それを検証しました。
type UserA = {
name: string;
age: number;
};
type UserB = {
name: string;
email: string;
};
type UserC = {
name: string;
age: number;
email: string;
};
type U<T> = T extends UserA ? number : string;
type A = U<UserA>; // number
type B = U<UserB>; // string
type C = U<UserC>; // number
上記の結果から
渡した型が条件の部分である場合に満たされるようです。
extendは日本語で"拡張”という意味ですが、TypeScriptにおけるこのextendsの記法が拡張され過ぎてもう意味がわからないですね。
更にとどめを刺します。
Conditional Typeとしてのextendsの中でinferを使う
inferを使うことで型を取り出すことができます。
type UserA = {
name: string;
role: 'admin' | 'user'; // ここの型を抽出してます。
};
type UserB = {
name: string;
age:number
};
type A<T> = T extends { role: infer U } ? U : null;
type B = A<UserA>; // "admin" | "user" →抽出できた
type C = A<UserB>; // null
T extends { key: infer U } ? U : V
と書くことで、Conditional Typeと同様に型を見て条件分岐するとともに、その内部の型を抽出してくれます。
T
の型にkey
プロパティが存在するかを見て、存在する場合はそのkey
の型を、存在しない場合はV
の型を返します。
Discussion