はじめに
みなさん、部分型は好きですか?
僕はあまり理解できていないため、好きではありません。
なので、この記事と一緒に部分型とお友達、いや、お知り合いになりましょう。
部分型について
type Name = { name: string };
type NameAndAge = { name: string, age: number };
上記の二つの型を見てください。部分型という言葉を用いて表すと、「NameAndAge型はName型の部分型である」といえます。オブジェクトのプロパティが多い=条件が厳しいほど該当するオブジェクトの数は減る=範囲が狭くなると言うなら、「狭い型は大きい型の部分型である」と言い換えられますね。
部分型の特徴として、狭い型を大きい型の代わりに使用できる(=コンパイルエラーにならない)という点が挙げられます。
型Sが型Lの部分型になる定義としては、以下の通りです。
1. Lが持つプロパティはすべてSにも存在する
2. 条件1のプロパティについて、Sにおけるそのプロパティの型はLにおけるプロパティの型の部分型(または同じ型)である。
const user1: NameAndAge = { name: "taro", age: 20 };
const user2: Name = user1;
// user2 = { name: "taro", age: 20 };となる。NameAndAgeはNameの部分型なので代入可能です。
補足:過剰プロパティチェック
変数経由では上の代入はOKですが、オブジェクトリテラルを直接 Name に代入すると追加プロパティ(age)でエラーになります。
これはコンパイルエラーにはなりません。user1とuser2は型が違うため本来コンパイルエラーが起きるべきですが、NameAndAge(狭い型)とName(広い型)の部分型関係が成り立っているため、コンパイルは通ります。
プロパティが多いと狭い、というのは直観的につかみづらいかもしれません。「プロパティが増える=>汎用性が落ちる=>狭い型」と自分は落とし込みました。また、条件2についても解説します。
type Animal = { age: number };
type Human = { name: string, age: number };
type AnimalFriend = { friend1: Animal, friend2: Animal };
type MyFriend = { friend1: Human, friend2: Human };
MyFriend型は、AnimalFriend型の部分型です。各プロパティについて見てみると、HumanはnameとageがあるためAnimalの部分型である(条件2を満たす)といえます。また、AnimalFriendにあるプロパティはすべてMyFriendに存在するため、全体として部分型といえます。
関数型と部分型
今まではオブジェクトの型について触れてきましたが、ここからは関数型と部分型の関係について解説したいと思います。関数型はみなさんにも馴染みのあるかと思います
type FnType = (consoleNum: number) => void;
const fn: FnType = (arg: number) => console.log(arg);
このように、関数の引数の型と返り値の型を記述したものを言います。この関数型についても、部分型の考えを適用できます。
type HasName = {name: string}; //型定義
type HasNameAndAge = {name: string, age: number }; //型定義
const fromAge = (age: number): HasNameAndAge => ({name: "John", age}); // 関数定義
const f: (age:number) => HasName = fromAge;
// const 変数名: (引数:引数の型) => 返り値の型 = 変数に代入する値 つまり f = fromAge
const obj: HasName = f(100);
// 実体は { name: "John", age: 100} だが、型は HasName
HasAgeAndName型を返す関数は、HasName型を返す関数の代わりに使える、ということです。言い換えると、部分型が成り立てば、狭い関数型は大きい関数型の代わりに使えるということです。
ちなみに、このプログラムを実行するとobjにはどのようなオブジェクトが入ると思いますか?
答えは、{name: "John", age:100}です。でも、obj.ageではアクセスできません。悲しいですね。
このように、TypeScriptでは型情報に比べてより情報の多いオブジェクトが得られることがあります。
返り値のvoidについて
返り値がvoid型の場合、今までのような狭い型、大きい型という定義に当てはめるとどうなるでしょうか。答えはめちゃくちゃ大きい型です。(void は「呼び出し側で返り値を使わない」契約です。)ですので
const f = (name: string) => ({name});
const g: (name: string) => void = f;
// 変数gの定義(型定義も含め)を行い、fをgに代入している
fは返り値がオブジェクト型であり、voidと比べて狭い型です。なので、部分型関係が成り立ち、fはgに代入できます。
const obj: HasName = g("tanaka");
ちなみに、上記のコードはどうなるでしょうか。答えはコンパイルエラーです。
g("tanaka")の返り値の型はvoidです。voidはオブジェクトではなく、undefinedですので、HasName型のオブジェクトに代入することはできません。
内部的にはf("tanaka")を呼び、{name: "tanaka"}というオブジェクトを返しますが、TypeScriptは「gはvoidを返すと約束しています!」という態度を取ります。ですので、コンパイルエラーが起こります。「実行時の真実」と「型としての契約」が分離している、ということですね。
引数の型と部分型
引数の型が異なる関数型を二つ見比べてみましょう
type HasName = {name: string}; //型定義
type HasNameAndAge = {name: string, age: number}; //型定義
const showName = (obj: HasName) => { console.log(obj.name)};
// showNameの型は(obj: HasName) => void
const g: (obj: HasNameAndAge) => void = showName;
// gの型は(obj: HasNameAndAge) => void
// ここでは、(obj: HasName) => void という型の変数を(obj: HasNameAndAge) => voidという型の変数に代入している
g({name:"tanaka", age:30});
// コンソールには"tanaka"が表示される
ここで注目したいのは、showNameの型 (obj: HasName) => void を、
より「引数が厳しい」型である (obj: HasNameAndAge) => void の変数 g に代入している、という点です。
引数の型だけを取り出して見ると、
- showName の引数の型: HasName
- g の引数の型: HasNameAndAge
となっており、HasNameAndAge は HasName の部分型
という関係にあります。
ところが、関数型としては逆 です。代入が成り立っているのは、
(obj: HasName) => void // これは
(obj: HasNameAndAge) => void // これの部分型
だからです。
つまり、関数型どうしで部分型関係を見るとき、
引数の型の部分型関係は反転するということが、この例で確認できます。
引数の数と部分型
type UnaryFunc = (arg: number) => number;
type BinaryFunc = (left: number, right: number) => number;
この二つの関数型について、UnaryFuncはBinaryFuncの部分型といえます。
あれ?オブジェクトの型の時はプロパティが多いと狭いから、この関数型を見比べるとBinaryFuncのほうがプロパティ(引数)が多くて狭いのでは?と思われるかもしれません。
ここで、関数を使う側の気持ちで見つめ直してください。「引数を一つ渡すつもりの場所」と、「引数を二つ渡すつもりの場所」。どちらがどちらの部分でしょうか。言い換えると、どちらをどちらの代わりに使えるでしょうか。
「引数を2つ渡すつもりでいる場所」に、「引数を1つしか使わない関数」を渡しても問題は起きません。引数をたくさんある関数型(BinaryFuncの想定)を引数を一つしかとらない関数型(UnaryFuncの実装)に置き換えるとなると、一つ目の引数のみが使用され、そのほかの引数を破棄することで代用できます。
ですが、その逆は引数が不足してしまうため代用できず、エラーになります。このことから、引数が少ない関数型は引数が多い関数型の部分型となることができる、といえます。
まとめ
部分型について、理解が深まりましたか?オブジェクト型と関数型で部分型の当てはめ方が異なるなど、難しい点がたくさんあったと思います。この記事が理解の一助になれば幸いです。ちなみに、僕は部分型と若干お近づきになれたと思います。皆さんも良い部分型ライフをお送りください。