😵

TypeScriptのextendsってなんなん?

2022/10/21に公開約3,000字

今日ではインターネットなしでは生活できないのと同様に、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;
}

よく知られているextendsinterfaceと組合わせて型を継承するときに使われます。しかし、最近では継承は開発の秩序を乱すという考えもあり、あまり使われない印象があります。

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

ログインするとコメントできます