1.はじめに
以前、部分型関係の理解に苦しみましたが、関数型の部分型関係の理解も苦しみました。
というか、関数型の部分型関係は段違いに難しいかったです。
キャッチアップしたことを言語化するために記事にしました。
複数回に分けて記事を投稿いたします。
今回は、関数型の 返り値の型による部分型関係 です。
@uhyo さんの「プロを目指す人のための TypeScript 入門」を元にキャッチアップしました。
間違って解釈している所ありましたら、ご指摘いただけますと幸いです。
2.目次
1.はじめに
2.目次
3.この記事でわかること
4.環境
5.関数型の部分型関係
5.1.部分型関係とは
5.2.返り値の型による部分型関係とは
5.3.void 型の部分型関係
6.おわりに
7.参考
3.この記事でわかること
関数型の返り値の型による部分型関係についてわかります。
S(SubType) が T(SuperType) の部分型ならば、
同じ引数リストに対して
(引数リスト) => S (SubType)という関数型は、(引数リスト) => T (SuperType)という部分型となります
このソースコードが理解できるようになります。
type Engineer = {
name: string;
year: number;
};
type FrontendEngineer = {
name: string;
year: number;
frontendSkill: Array<string>;
};
const hasFrontendSkill = (frontendSkill: string[]) => ({
name: 'daishi',
year: 35,
frontendSkill,
});
const hasEngineerSkill: (frontendSkill: string[]) => Engineer = hasFrontendSkill;
const daishiSkill: Engineer = hasEngineerSkill(['JavaScript', 'TypeScript']);
console.log(daishiSkill); // { name: 'daishi', year: 35, frontendSkill: [ 'JavaScript', 'TypeScript' ] }
4.環境
- TypeScript: 4.7.4
- Node.js: 16.15.1
5.関数型の部分型関係
5.1部分型関係とは
関数型の部分型関係に入る前に、部分型関係について振れておこうと思います。
部分型関係とは、
ある型 A の値が必要な時に、別の型 B をもつ値で代用できることを B は A の部分型であるといいます。
A は SuperType、 B は SubType という関係になります。
SuperType の一部分が SubType ということですね。
(オブジェクト指向の継承と同じ関係です。)
先程の文章にこれを当てはめてみると少し具体的に見えてくるかと思います。
ある型 A(SuperType) の値が必要な時に、別の型 B(SubType) をもつ値で代用できることを B(SubType) は A(SuperType) の部分型であるといいます。
どうですか、多少は部分型関係がイメージしやすくなりましたか?(私だけかな?)
本記事では、私自身が、型の関係性を SuperType、 SubType 表現することで理解できたので、
これでもかっていうほど SuperType、 SubType と記述しています。
以前、部分型関係についての記事を投稿していますので、ご参考までにどうぞです。
5.2.返り値の型による部分型関係とは
では、本題の関数型の返り値の型による部分型関係について見ていきます。
下記の条件を持っている時、
- S(SubType) が T(SuperType) の部分型
- 関数に同じ引数リストを持っている
関数型の返り値が部分型関係となります。
つまり、
(引数リスト) => S (SubType)という関数型は、(引数リスト) => T(SuperType) という部分型関係となります。
部分型関係の特性で
S(SubType) 型の値を T(SuperType) 型の値の代わりに使うことができます。
関数の返り型の S(SubType)型の値を T(SuperType)型とみなすことで、
「S(SubType)型の値を返す関数」は「T(SuperType)型の値を返す関数」の代わりに使えることになります。
ややこしいですが、ソースコードで見てみましょう
// SuperType の定義
type Engineer = {
name: string;
year: number;
};
// SubType の定義
type FrontendEngineer = {
name: string;
year: number;
frontendSkill: Array<string>;
};
// SubType の関数
const hasFrontendSkill = (frontendSkill: string[]) => ({
name: 'daishi',
year: 35,
frontendSkill, // ← frontendSkill: frontendSkill を省略
});
// SuperType の関数に SubType の関数を代入
const hasEngineerSkill: (frontendSkill: string[]) => Engineer = hasFrontendSkill;
// SuperType の関数を呼び出し 変数 daishiSkill に代入
const daishiSkill: Engineer = hasEngineerSkill(['JavaScript', 'TypeScript']);
console.log(daishiSkill); // { name: 'daishi', year: 35, frontendSkill: [ 'JavaScript', 'TypeScript' ] }
FrontendEngineer (SubType)型が Engineer(SuperType)型の部分型という関係になります。
「どういうこと、意味わからん」って方は、以前投稿した部分型について記事をご参照ください。
同じ引数リスト(frontendSkill: string[]) を使った関数の
hasFrontendSkill (SubType)が hasEngineerSkill (SuperType)の部分型となります。
つまり、
(frontendSkill: string[]) => FrontendEngineer (SubType)が、
(frontendSkill: string[]) => Engineer (SuperType)の部分型ということになります。
では、細かく見ていきますね。
関数 hasFrontendSkill は (frontendSkill: string[]) => FrontendEngineer 型となります。
const hasFrontendSkill = (frontendSkill: string[]) => ({
name: 'daishi',
year: 35,
frontendSkill, // ← frontendSkill: frontendSkill を省略
});
これを、(frontendSkill: string[]) => Engineer (SuperType)型の関数 hasEngineerSkill に代入しています。
const hasEngineerSkill: (frontendSkill: string[]) => Engineer = hasFrontendSkill;
引数 ['JavaScript', 'TypeScript'] で関数 hasEngineerSkill を呼び出し、変数 daishiSkill に代入しています。
関数 hasEngineerSkill は Engineer 型なので、変数 daishiSkill も Engineer 型となります。
const daishiSkill: Engineer = hasEngineerSkill(['JavaScript', 'TypeScript']);
変数 daishiSkillの型が Engineer (SuperType)型なのに frontendSkill: [ 'JavaScript', 'TypeScript' ] が返ってきていますね。
console.log(daishiSkill); // { name: 'daishi', year: 35, frontendSkill: [ 'JavaScript', 'TypeScript' ] }
「Engineer の型定義には frontendSkill プロパティ定義していないのにあるの🤔?」
と疑問に思われたかもしれませんが、
これは、
hasEngineerSkill は Engineer 型(SuperType)、 hasFrontendSkill は FrontendEngineer 型(SubType)と異なる型の関数ですが、
hasFrontendSkill(SuperType) を hasEngineerSkill (SubType)に代入しているので中身は同じ関数オブジェクトとなります。
const hasEngineerSkill: (frontendSkill: string[]) => Engineer = hasFrontendSkill;
よってどちらを呼び出しても同じ結果が出ます。
なので、 hasEngineerSkill(['JavaScript', 'TypeScript']) で関数を呼び出しても、
変数 daishiSkill には Engineer 型と FrontendEngineer 型の両方のプロパティをもつオブジェクトが入ります。
この部分型関係のように変数に定義した型情報 Engineer よりも多いオブジェクトを得ることができます。
この辺が部分型関係が混乱しやすい概念ですよね…
私は混乱しまくりました。
どの関数が SuperType で SubType かをつかめると理解しやすいのかなと感じました(個人の感想です)。
5.3.void 型の部分型関係
返り値の型による部分型関係についてご理解できたと思いますが、 void 型が特殊な振る舞いをします
どんな型を返す関数型(SubType)も同じ引数を受け取って void 型を返す関数型(SuperType)の部分型として扱われます。
ではどういうことかを見てきます。
void 型は返り値がない型なので、
どんな値を返す関数(SubType)であっても「なにも返さない関数(SuperType)」の代わりに使うことができます。
// SubType
const f = (name: string) => ({ name });
// SuperType
const g: (name: string) => void = f;
関数 f は (name: string) => { name: string; }型の関数(SubType)ですが、
(name: string) => void 型の関数 g (SuperType)に代入することができます。
特殊と書きましたが、先程の返り値の型による部分型関係と同じ理屈ですね。
6.おわりに
だいぶややこしかったですよね…
私は、最初全く理解できませんでした。
理解するのに、何時間もかかってしまいました…
関数の返り値の型に、SuperType と SubType か当て込んでみると部分型関係が見えてくると思います。
次は、さらにややこしくなる 引数の型による部分型関係 を見ていきます。
併せて他の記事も読んでいただけると嬉しいです🙇♂️