LoginSignup
55
28

More than 1 year has passed since last update.

【TypeScript】型引数を難しく捉えていたけど、関数の引数と変わらないじゃん

Last updated at Posted at 2022-09-04

1.はじめに

型引数というものがあるということは知っていましたが、
イマイチよくわからないまま使っていました。

調べてみると、
「関数の引数と同じように使えるんだ。なんか難しく捉えていた
と感じました。

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

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

2.目次

1. はじめに
2. 目次
3. この記事でわかること
4. 環境
5. 型引数
 5.1. 型引数とは?
 5.2. 複数の型引数
 5.3. 型引数を持つ型を使う
 5.4. 型引数に型を入れて使う
 5.5. 型引数の制約
 5.6. デフォルトの型引数
 5.7. 制約とデフォルトを同時に使う
6. おわりに
7. 参考

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

型引数の基礎について理解できます。

  • 型引数の使いかた
  • 型引数に制約を与える使いかた
  • 型引数のデフォルト型の使いかた

最終的にこのコードを理解することができます。

type Engineer = {
    name: string;
    year: number;
    frontSkillList?: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
    backSkillList?: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
};

type FrontendEngineer = {
    name: string;
    year: number;
    frontSkillList: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
};

type ProjectManager = {
    name: string;
    year: number;
    frontSkillList: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
    backSkillList: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
    isPM: boolean;
};

type EngineerTeam<Programer extends Engineer, PM extends Engineer = ProjectManager> = {
    teamName: string;
    employee: Programer;
    director: PM;
};

type FrontTeam = EngineerTeam<FrontendEngineer>;

4.環境

  • TypeScript: 4.7.4
  • Node.js: 16.15.1

5.型引数

5.1.型引数とは?

型引数とは、型を定義する時に引数(パラメータ)を持てすことができる型です。

引数は宣言の中でだけしか使えません。

型引数を宣言するには、型名のあとに型引数を < > で囲うことで型引数をもった方を宣言できます。

言葉だけなれべてもイメージがわかないかと思いますので、
実際にどのように使うかを簡単な例で見ていきたいと思います。

type Engineer<T> = {
    name: string;
    age: number;
    skillList: T;
};
  • Engineer<T> が型引数を持った型(ジェネリック型)
  • < > の中の T が型引数
  • skillList: T; の部分が、skillList プロパティは T

という風に使用します。

つまり Engineer<T> 型は、

  • name プロパティは、string
  • age プロパティは、number
  • skillList プロパティは、T

のオブジェクトの型ということになります。

今回は、型引数を T としましたが、これは任意でつけることができるため、SkillList 型という風にもできます。

type Engineer<SkillList> = {
    name: string;
    age: number;
    skillList: SkillList;
};

5.2.複数の型引数

型引数は、複数設定することができます。

type Engineer<FrontSkillList, BackSkillList> = {
    name: string;
    age: number;
    frontSkillList: FrontSkillList;
    backSkillList: BackSkillList;
};

なんか説明がくどくなっちゃいますが、(そんなん説明せんでもわかってるわ!って方は飛ばしてください)

Engineer 型は、

  • name プロパティは、string
  • age プロパティは、number
  • frontSkillList プロパティは、FrontSkillList
  • backSkillList プロパティは、BackSkillList

のオブジェクトの型ということになります。

5.3.型引数を持つ型を使う

型引数の宣言を見てきましたが、
「じゃあこれをどうやって使うんだろう🤔」
となるかと思います。

ここから、型引数を持つ型の使いかたを見て聞きたいと思います。

type Engineer<FrontSkill, BackSkill> = {
    name: string;
    year: number;
    frontSkill: FrontSkill;
    backSkill: BackSkill;
};

// Engineer<FrontSkill, BackSkill> 型を使ったオブジェクト
const kakedashiEngineer:Engineer<string, string> = {
    name: 'kakedashi',
    year: 0,
    frontSkill: 'JavaScript',
    backSkill: 'PHP'
};

kakedashiEngineer:Engineer<string, string> というように使います。

これは、型宣言した Engineer<FrontSkill, BackSkill>

  • FrontSkill の型引数に string
  • BackSkill の型引数に string

を代入するという意味になります。

kakedashiEngineer は、

  • frontSkill プロパティは string 型となり、'JavaScript' という値
  • backSkill プロパティは string 型となり、'PHP' という値

が入っているオブジェクトとなります。

つまり、

type Engineer = {
    name: string;
    year: number;
    frontSkill: string;
    backSkill: string;
};

という型と同じ意味になります。

このように、

型宣言する際には具体的な型を宣言せずに、使用する際に型を定義します。

使いたい用途に合わせて型を宣言できます。

型引数を持った型を使う上で注意があります。

型引数を持った型を使用する場合は、型引数を用いなければエラーとなります。

どういうことかといいますと、

type Engineer<FrontSkill, BackSkill> = {
    name: string;
    year: number;
    frontSkill: FrontSkill;
    backSkill: BackSkill;
};

// Engineer<FrontSkill, BackSkill> 型を使ったオブジェクト
const kakedashiEngineer:Engineer = {
    name: 'kakedashi',
    year: 0,
    frontSkill: 'JavaScript',
    backSkill: 'PHP',
};

ということです。
これをコンパイルすると

Generic type 'Engineer' requires 2 type argument(s).

ジェネリック型の 'Engineer' は2つの型引数を必要とします。

と注意されてしまいます。

(デフォルト値を設定していれば、使用時に型引数を省略することができます。 → 参照:デフォルトの型引数

5.4.型引数に型を入れて使う

使用する際に、型引数に型を入れて使うこともできます。

type Engineer<FrontSkillList, BackSkillList> = {
    name: string;
    year: number;
    frontSkillList: FrontSkillList;
    backSkillList: BackSkillList;
};

type EngineerSkillList = {
    languageList: Array<string>;
    frameworkList: Array<string>;
};

const kakedashiEngineer:Engineer<EngineerSkillList, EngineerSkillList> = {
    name: 'kakedashi',
    year: 0,
    frontSkillList: {
        languagList: ['JavaScript'],
        frameworkList: ['React'],
    },
    backSkillList: {
        languageList: ['PHP'],
        frameworkList: ['Laravel'],
    },
};

FrontSkillListBackSkillList の方引数に EngineerSkillList 型を代入しています。

frontSkillList, backSkillList プロパティはそれぞれ EngineerSkillList 型ということになります。

つまり、

type Engineer = {
    name: string;
    year: number;
    frontSkillList: EngineerSkillList;
    backSkillList: EngineerSkillList;
};

という型と同じ意味になります。

更にこの型を分解してみると、

type Engineer = {
    name: string;
    year: number;
    frontSkillList: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    },
    backSkillList: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    },
};

という型になります。

5.5.型引数の制約

次に制約を与えた型引数を見ていきます。

型引数に ある型の部分型でなければいけない という制約です。

部分型に関しては、以前記事を書きましたのでご参考にどうぞです。
(ご覧いただいた際についでに いいね してくださると嬉しいです😆 w)

では、どのように制約を与えるのかといいますと、
型引数の宣言の後ろに extends 型 と宣言します。

type Engineer<FrontSkillList extends LanguageSkillList, FrontSkillList extends LanguageSkillList> = {
    name: string;
    year: number;
    frontSkillList: FrontSkillList;
    backSkillList: BackSkillList;
};

このように使います。

では、具体的なコードを見てみましょう。

// 型引数に制約を与えた型を宣言
type Engineer<FrontSkillList extends LanguageSkillList, FrontSkillList extends LanguageSkillList> = {
    name: string;
    year: number;
    frontSkillList: FrontSkillList;
    backSkillList: BackSkillList;
};

type LanguageSkillList = {
    languageList: Array<string>;
}

// LanguageSkillList 型の部分型
type LanguageAndFrameworkSkillList = {
    languageList: Array<string>;
    frameworkList: Array<string>;
}

// 型引数に制約を与えた型を別名 (Kakedashi) で宣言する
type Kakedashi = Engineer<LanguageSkillList, LanguageAndFrameworkSkillList>;

const kakedashiEngineer:Kakedashi = {
    name: 'kakedashi',
    year: 0,
    frontSkillList: {
        languageList: ['JavaScript'],
    },
    backSkillList: {
        languageList: ['PHP'],
        frameworkList: ['Laravel'],
    },
};

FrontSkillList extends LanguageSkillListBackSkillList extends LanguageSkillList が制約を与えた型引数となります。

型引数 FrontSkillListBackSkillList に代入する型はLanguageSkillList の部分型でなければいけません。

つまり、

LanguageSkillList 型の性質を引き継いでいる引数を FrontSkillListBackSkillList に代入しなければいけません

ということです。

下記の内容から、LanguageAndFrameworkSkillList 型は LanguageSkillList の部分型の関係ということがわかります。
(なぜ?と思う方はこちらの記事をご参考にしてください)

type LanguageSkillList = {
    languageList: Array<string>;
}

type LanguageAndFrameworkSkillList = {
    languageList: Array<string>;
    frameworkList: Array<string>;
}

次に、下記の内容から、

  • 型引数の FrontSkillListLanguageSkillList を代入
  • 型引数の BackSkillListLanguageSkillList の部分型の LanguageAndFrameworkSkillList を代入

ということがわるかと思います。

type Kakedashi = Engineer<LanguageSkillList, LanguageAndFrameworkSkillList>;

これが制約となります。

したがって、制約にない型引数を代入するとエラーとなります。

type Engineer<FrontSkillList extends LanguageSkillList, BackSkillList extends LanguageSkillList> = {
    name: string;
    year: number;
    frontSkillList: FrontSkillList;
    backSkillList: BackSkillList;
};

type LanguageSkillList = {
    languageList: Array<string>;
}

type Manager = Engineer<string, number>;

Type 'string' does not satisfy the constraint 'LanguageSkillList'.

('string' 型は制約 'LanguageSkillList' を満たしません。)

制約を与えることで型をより安全に使えますね。

5.6.デフォルトの型引数

型引数を持つ方を使う で、

型引数を持った型を使用する場合は、型引数を用いなければエラーとなります。

と記述しましたが、使用する際に型引数を省略することができます。

それが、デフォルトの型引数です。

では、型引数のデフォルトの型について見ていきます。

これは、型引数を省略してもデフォルトの型を使ってくれるものになります。

型引数の後ろに = 型 と宣言して使います。
この の部分がデフォルト値となります。

type EngineerTeam<Programer = Engineer, PM = Engineer> = {
    teamName: string;
    director: PM;
    employee: Programer;
}

このように使います。

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

type Engineer = {
    name: string;
    year: number;
    frontSkillList?: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
    backSkillList?: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
};

// 型引数にデフォルトの型を宣言
type EngineerTeam<Programer = Engineer, PM = Engineer> = {
    teamName: string;
    employee: Programer;
    director: PM;
};

// EngineerTeam<Engineer, Enginerr>と同じ
type FrontTeam = EngineerTeam;

const frontTeam: FrontTeam = {
    teamName: 'ふろんとチーム',
    employee: {
        name: 'かけだし',
        year: 1,
        frontSkillList: {
            languageList: ['JavaScript'],
            frameworkList: ['React'],
        },
    },
    director: {
        name: 'ぶちょう',
        year: 20,
        frontSkillList: {
            languageList: ['JavaScript', 'TypeScript'],
            frameworkList: ['Next.js', 'Nuxt.js'],
        },
        backSkillList: {
            languageList: ['PHP', 'Ruby'],
            frameworkList: ['Laravel', 'Ruby on Rails'],
        }
    }
};

ではまずは、デフォルトの型を宣言している部分から見てみます。

// 型引数にデフォルトの型を宣言
type EngineerTeam<Programer = Engineer, PM = Engineer> = {
    teamName: string;
    employee: Programer;
    director: PM;
};

これは、

  • 型引数の Programer に何も代入されなかったら Engineer 型を使う
  • 型引数の PM に何も代入されなかったら Engineer 型を使う

という意味になります。

では、デフォルト型を使ってみましょう。

type FrontTeam = EngineerTeam;

FrontTeam 型は EngineerTeam 型を使いますが、引数がありません。
なので、これは、引数にデフォルト型が代入されることになります。

type FrontTeam = EngineerTeam<Engineer, Enginerr>;

と同じことになります。

片方の型引数にのみ型を代入することもできます。

type FrontTeam = EngineerTeam<string>;

これは、

type FrontTeam = engineerTeam<string, Engineert>;

と同じです。

ただ、今のところは、右にだけ指定することはできませんのでデフォルト型を設定するときは左に型引数を設ける必要があります。

5.7.制約とデフォルトを同時に使う

制約とデフォルトを同時に使うこともできます。

このように使います。
少しややこしそうに見えますが、今まで見てきたことをまとめただけになります。

type Engineer = {
    name: string;
    year: number;
    frontSkillList?: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
    backSkillList?: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
};

type FrontendEngineer = {
    name: string;
    year: number;
    frontSkillList: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
};

type ProjectManager = {
    name: string;
    year: number;
    frontSkillList: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
    backSkillList: {
        languageList: Array<string>;
        frameworkList: Array<string>;
    }
    isPM: boolean;
};

// 型引数に制約とデフォルト型を宣言
type EngineerTeam<Programer extends Engineer, PM extends Engineer = ProjectManager> = {
    teamName: string;
    employee: Programer;
    director: PM;
};

// EngineerTeam<FrontendEngineer, ProjectManager>と同じ
type FrontTeam = EngineerTeam<FrontendEngineer>;

const frontTeam: FrontTeam = {
    teamName: 'ふろんとチーム',
    employee: {
        name: 'かけだし',
        year: 1,
        frontSkillList: {
            languageList: ['JavaScript'],
            frameworkList: ['React'],
        },
    },
    director: {
        name: 'ぶちょう',
        year: 20,
        frontSkillList: {
            languageList: ['JavaScript', 'TypeScript'],
            frameworkList: ['Next.js', 'Nuxt.js'],
        },
        backSkillList: {
            languageList: ['PHP', 'Ruby'],
            frameworkList: ['Laravel', 'Ruby on Rails'],
        },
        isPM: true,
    }
};

では、型宣言しているところから見てみましょう。

// 型引数に制約とデフォルト型を宣言
type EngineerTeam<Programer extends Engineer, PM extends Engineer = ProjectManager> = {
    teamName: string;
    employee: Programer;
    director: PM;
};

この部分ですね。

  • 型引数 Programer
    • Engineer 型の部分型(制約)
  • 型引数 PM
    • Engineer 型の部分型(制約)
    • デフォルト型は ProjectManager

と意味になります。

ややこしそうに見えますが、単純ですね。

この型を使っているのが、

type FrontTeam = EngineerTeam<FrontendEngineer>;

になります。

これは、

type FrontTeam = EngineerTeam<FrontendEngineer, ProjectManager>;

と同じことです。

6.おわりに

型引数の使いかたの理解が深まりました。

今までわけも分からず使っていたフシがあったので、
これからは内容を理解して上で使えそうです。

というか、型引数を使わないと不便だなと感じるようになりましたw

併せて他の記事も読んでいただけると嬉しいです🙇‍♂️

最後まで読んでいただきありがとうございました。

7.参考

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

55
28
2

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
55
28