LoginSignup
6
2

More than 1 year has passed since last update.

【TypeScript】関数型の引数の型による部分型関係ややこしすぎません?なので、初心者にも理解できるように噛み砕いてみた

Last updated at Posted at 2022-10-02

1.はじめに

前回、関数型の返り値の型による部分型関係の記事を投稿しました。

その続編として、今回は 関数型の引数の型による部分型関係 の記事を書きました。

ややこしすぎて、ちゃんと言語化できているか不安です…

間違って解釈している所ありましたら、ご指摘いただけますと幸いです。

今回も、@uhyo さんの「プロを目指す人のための TypeScript 入門」を元にキャッチアップしました。

いやー、本当にややこしかった。

2.目次

1.はじめに
2.目次
3.この記事でわかること
4.環境
5.関数型の引数の型による部分型関係
 5.1.引数の型による部分型関係とは
 5.2.引数の型による部分型関係を使ってみる
 5.3.複数の引数の型による部分型関係
 5.4.複数の引数が同時に部分型関係のときの関数型
 5.5.引数による部分型関係と返り値の型による部分型関係の組み合わせ
6.おわりに
7.参考

3.この記事でわかること

下記の 関数型の引数の型による部分型関係 について理解できます。

「SuperType の型を引数に受け取る関数」が「SubType の型を引数に受け取る関数」の部分型になる

// SuperType のオブジェクトの定義
type Engineer = {
    name: string;
    year: number;
};

// SubType のオブジェクトの定義
type FrontendEngineer = {
    name: string;
    year: number;
    frontendSkill: Array<string>;
};

// SuperType の型を引数にもった  SubType の型の関数
const frontendEngineerInfo = (engineer: Engineer): FrontendEngineer => {
    return {
        frontendSkill: [''],
        ...engineer,
    };
};

// engineerInfo 関数を SubType の型を引数に持った SuperType の型に代入
const engineerInfo: (frontendEngineer: FrontendEngineer) => Engineer = frontendEngineerInfo;

// 関数 engineerInfo を使用する
const info = engineerInfo({
    name: 'daishi',
    year: 1,
    frontendSkill: ['JavaScript'],
});

console.log(info); // { frontendSkill: [ 'JavaScript' ], name: 'daishi', year: 1 }

4.環境

  • TypeScript: 4.7.4
  • Node.js: 16.15.1

5.関数型の引数の型による部分型関係

本記事を読んで頂く前に、以前書いた 【TypeScript】関数型の部分型関係ってややこしくて SuperType、SubType って書きまくったら理解できた(返り値の型による部分型関係) を読んでいただいてからの方が理解がしやすいかと思います。

それでは、引数の型が違う関数型同士の部分型を見てきましょう。

5.1.引数の型による部分型関係とは

S 型(SubType)が T 型(SuperType)の部分型である時、
T 型(SuperType) を引数に受け取る関数」(SubType)の型は「S 型(SubType) を引数に受け取る関数」(SuperType)の型の部分型となります。

参照: プロを目指す人のための TypeScript 入門(SuperType、SubType は追記しました)

部分型関係の向きが逆になっていますね…
(ややこしい…)
(所見でこれ理解できるになりたい)

私の感想が入ってしまいましたが、続けます…

部分型の「S 型(SubType)の値は、T 型(SuperType)の値の代わりになる」という原則に基づいて考えると理解しやすいかと思います。
T 型(SuperType)の引数がほしいところに S 型(SubType)の値を受け取っても、T 型(SuperType)の値として扱うことができるからです。
つまり、
T型(SuperType)を引数に受け取る関数」は「S 型(SubType) を引数に受け取る関数」として扱える。
これは、
T 型(SuperType)を引数に受け取る関数」(SubType)が「S 型(SubType)を引数に受け取る関数」(SuperType)の部分型であることとなります。

参照: プロを目指す人のための TypeScript 入門(SuperType、SubType は追記しました)

ややこしいですが、こういうことですw

オブジェクト指向でいう継承の関係ですね。

では、具体的に見ていきましょう。

5.2.引数の型による部分型関係を使ってみる

下記のソースコードが引数の型による部分型関係となります。

// SuperType のオブジェクトの定義
type Engineer = {
    name: string;
    year: number;
};

// SubType のオブジェクトの定義
type FrontendEngineer = {
    name: string;
    year: number;
    frontendSkill: Array<string>;
};

// SuperType の型を引数にもった void 型の関数
const engineerInfo = (engineer: Engineer) => {
    console.log(engineer.name);
    console.log(engineer.year);
};

// engineerInfo 関数を引数に SubType の型をもった関数に代入
const frontendEngineerInfo: (frontendEngineer: FrontendEngineer) => void = engineerInfo;

// 関数を使用する
frontendEngineerInfo({
    name: 'daishi',
    year: 1,
    frontendSkill: ['JavaScript'],
});

// 実行結果
// daishi
// 1

先程の引数の型による部分型関係とはで記述した内容を、今回の内容に落とし込んでみると、

Engineer 型(SuperType)を引数に受け取る関数」(SubType)が「FrontendEngineer 型(SubType)を引数に受け取る関数」(SuperType)の部分型ということになります。

これを関数型の記述に書き換えてみます。

(engineer: Engineer) => void 型(SubType)は、(frontendEngineer: FrontendEngineer) => void 型(SuperType)の部分型

となります。

では、ここから、なぜこうなるのかを見ていきます。

まずは、前提条件ですが、
FrontendEngineer 型(SubType)は、Engineer 型 (SuperType)の部分型です。

// SuperType のオブジェクトの定義
type Engineer = {
    name: string;
    year: number;
};

// SubType のオブジェクトの定義
type FrontendEngineer = {
    name: string;
    year: number;
    frontendSkill: Array<string>;
};

部分型関係の原則の
S 型(SubType)がT 型(SuperType)の部分型のとき、S 型(SubType)の値は、T 型(SuperType)の値の代わりになる」 なので、
FrontendEngineer 型(SubType)の値は、Engineer 型(SuperType)の値の代わりに使うことができます。

Engineer 型(SuperType)の引数がほしいところに FrontendEngineer 型(SubType)の引数を受け取っても Engineer 型(SuperType)として使うことができます。

つまり、
Engineer 型(SuperType)を引数に受け取る関数」は「FrontendEngineer 型(SubType)を引数に受け取る関数」の代わりに使えることになります。

といことは、
Engineer 型(SuperType)を引数に受け取る関数」(SubType)は「FrontendEngineer 型(SubType)を引数に受け取る関数」(SuperType)の部分型となります。

これで、引数と返り値の型が逆になっていることが見えてきたと思います。

なので、
(engineer: Engineer) => void 型の関数(SubType)は (frontendEngineer: FrontendEngineer) => void 型の関数(SuperType)として扱うことができます。

少しややこしいですが、一つづつ見ていくと理解できるのではないかと思います。
(私は、まだ一つづつ見ていっても混乱しちゃいますね…)

では、ソースコードに戻ります。

// SuperType の型を引数にもった void 型の関数
const engineerInfo = (engineer: Engineer) => {
    console.log(engineer.name);
    console.log(engineer.year);
};

// engineerInfo 関数を引数に SubType の型をもった関数に代入
const frontendEngineerInfo: (frontendEngineer: FrontendEngineer) => void = engineerInfo;

関数 frontendEngineerInfo が引数に受け取るのは、FrontendEngineer 型(SubType)の引数です。
しかし、FrontendEngineer 型(SubType)はEngineer 型(SuperType)の部分型なので、
FrontendEngineer 型(SubType)を Engineer 型(SuperType)としても扱えるため関数は動作します。

したがって、(engineer: Engineer) => void 型(SubType)をもつ関数 engineerInfo
(frontendEngineer: FrontendEngineer) => void 型(SuperType) の関数 frontendEngineerInfo として扱うことができます。
関数 engineerInfofrontendEngineerInfo に代入することができます。

では、関数 frontendEngineerInfo を呼び出します。

// 関数を使用する
frontendEngineerInfo({
    name: 'daishi',
    year: 1,
    frontendSkill: ['JavaScript'],
});

関数 engineerInfofrontendEngineerInfo として扱えるので、

// SuperType の型を引数にもった void 型の関数
const engineerInfo = (engineer: Engineer) => {
    console.log(engineer.name);
    console.log(engineer.year);
};

関数 engineerInfo(SubType) の引数に
{ name: 'daishi', year: 1, frontendSkill: ['JavaScript', 'TypeScript'] } が渡されて
engineer.nameengineer.year を出力するので daishi1 が出力されます。

ここで、
「なぜ Engineer 型は、nameyear のプロパティしか持たないはずなのに、
余計な frontendSkill: Array<string> を渡してる?」
と疑問に持たれるかもしれません。

// SuperType のオブジェクトの定義
type Engineer = {
    name: string;
    year: number;
};

だけど、関数 engineerInfo の引数 engineer が余計なプロパティを持っていても動作には影響がありません。

型と引数が混在しているからややこしく感じるかもしれませんが、
関数を使用する際に 使わない値 を含んだオブジェクトや配列を引数として渡しても問題ないですよね。
それと同じことです。

以上が、引数の型による部分型関係です。
少し複雑ですが、SuperType の 代わりに SubType を使うというこを念頭において見ていけば流れを掴むことはできるのではないでしょうか。
次に、引数が複数あったときの引数の型による部分型関係を見ていきます。

5.3.複数の引数の型による部分型関係

引数が複数あっても引数の型による部分型関係は同じです。

今回は、

  • 1つ目の引数は部分型関係
  • 2つ目の引数は同じ

という引数が 2 つ場合で見ていきます。
これは、部分型関係が成り立ちます。

// SuperType のオブジェクトの定義
type Engineer = {
    name: string;
    year: number;
};

// SubType のオブジェクトの定義
type FrontendEngineer = {
    name: string;
    year: number;
    frontendSkill: Array<string>;
};

// SuperType の型と boolean 型を引数にもった void 型の関数
const engineerInfo = (engineer: Engineer, isPM: boolean) => {
    console.log(engineer.name);
    console.log(engineer.year);
    console.log(isPM);
};

// engineerInfo 関数を SubType の型と boolean 型を引数にもった関数に代入
const frontendEngineerInfo: (frontendEngineer: FrontendEngineer, isPM: boolean) => void = engineerInfo;

// 関数 rontendEngineerInfo を使用する
frontendEngineerInfo(
    {
        name: 'daishi',
        year: 1,
        frontendSkill: ['JavaScript'],
    },
    false,
);
// 実行結果
// daishi
// 1
// false

(engineer: Engineer, isPM: boolean) => void 型(SubType)の関数 engineerInfo は、
(frontendEngineer: FrontendEngineer, isPM: boolean) => void 型(SuperType)の関数 frontendEngineerInfo の部分型となります。

これは、引数の型による部分型関係を使ってみると同じ理屈です。

関数 frontendEngineerInfo

  • 第1引数の frontendEngineerFrontendEngineer 型(SubType)の値を受け取っても Engineer 型(SuperType)とみなせる
  • 第2引数は同じなのでそのまま使える

なので、関数 engineerInfo を関数 frontendEngineerInfo の代わりとして扱うことができます。

5.4.複数の引数が同時に部分型関係のときの関数型

複数の引数が同時に部分型関係にある場合も関数型に部分型関係が発生します。

  • 1つ目の引数は部分型関係
  • 2つ目の引数も同じ部分型関係

という引数を持った関数ということです。

では、ソースコードを見ていきしょう。

// SuperType のオブジェクトの定義
type Engineer = {
    name: string;
    year: number;
};

// SubType のオブジェクトの定義
type FrontendEngineer = {
    name: string;
    year: number;
    frontendSkill: Array<string>;
};

// 複数の SuperType の型の引数をもった void 型の関数
const engineerInfo = (kakedashiEngineer: Engineer, seniorEngineer: Engineer) => {
    console.log(kakedashiEngineer.name);
    console.log(kakedashiEngineer.year);
    console.log(seniorEngineer.name);
    console.log(seniorEngineer.year);
};

// engineerInfo 関数を 複数の SubType の型を引数にもった関数に代入
const frontendEngineerInfo: (kakedashiFrontendEngineer: FrontendEngineer, seniorFrontendEngineer: FrontendEngineer) => void = engineerInfo;

// 関数 frontendEngineerInfo を使用する
frontendEngineerInfo(
    {
        name: 'daishi',
        year: 1,
        frontendSkill: ['JavaScript'],
    },
    {
        name: 'manju',
        year: 10,
        frontendSkill: ['JavaScript', 'TypeScript'],
    },
);
// 実行結果
// daishi
// 1
// manju
// 10

(kakedashiEngineer: Engineer, seniorEngineer: Engineer) => void 型(SubType)の関数 engineerInfo は、 (kakedashiFrontendEngineer: FrontendEngineer, seniorFrontendEngineer: FrontendEngineer) => void 型(SuperType)の関数 frontendEngineerInfo の部分型となります。

理屈は全く同じなので説明は省略いたします。

複数の部分型関係が同居している場合、それらが全て同じ向きでなければ関数型の部分型関係につながりません。

(engineer: Engineer, frontendEngineer: FrontendEngineer) => void 型と (frontendEngineer: FrontendEngineer, engineer: Engineer) => void型があった場合は、
部分型関係とはならないということです。

第1引数と第2引数では、部分型関係の向きが逆であり、両方合わせるとどちらがどちらの SuperType かが言えないからです。

5.5.引数による部分型関係と返り値の型による部分型関係の組み合わせ

引数による部分型関係は、返り値の型による部分型関係と組み合わさっても大丈夫です。

// SuperType のオブジェクトの定義
type Engineer = {
    name: string;
    year: number;
};

// SubType のオブジェクトの定義
type FrontendEngineer = {
    name: string;
    year: number;
    frontendSkill: Array<string>;
};

// SuperType の型を引数にもった  SubType の型の関数
const frontendEngineerInfo = (engineer: Engineer): FrontendEngineer => {
    return {
        frontendSkill: [''],
        ...engineer,
    };
};

// engineerInfo 関数を SubType の型を引数に持った SuperType の型に代入
const engineerInfo: (frontendEngineer: FrontendEngineer) => Engineer = frontendEngineerInfo;

// 関数 engineerInfo を使用する
const info = engineerInfo({
    name: 'daishi',
    year: 1,
    frontendSkill: ['JavaScript'],
});

console.log(info); // { frontendSkill: [ 'JavaScript' ], name: 'daishi', year: 1 }

(engineer: Engineer) => FrontendEngineer 型(SubType) は(frontendEngineer: FrontendEngineer) => Engineer 型(SuperType)の部分型となります。

返り値の型が逆になっていてややこしそうに見えますが、今までと同じ理屈です。

でも、ややこしいですよね…

6.おわりに

引数の型と返り値の型が逆になるので、すごくややこしいですよね…

重要なのは、SubType の値は、SuperType の値の代わりになる です。

どの型がどの型の部分型かを一つ一つ確認すると流れが見えてくるのではないかなと思います。

なれるまでは、「SubType の値は、SuperType の値の代わり」と呪文を唱えるように方の関係性を見ながらソースを追っかけたいと思います。

シューイチ投稿していますので、併せて他の記事も読んでいただけると嬉しいです🙇‍♂️

7.参考

書籍:プロを目指す人のためのTypeScript入門 鈴木僚太[著]

6
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
6
2