1.はじめに
本記事ですが、@kazatsuyu 様よりご指摘とアドバイス修正いたしました。
サンプルコードの例(命名)が良くなかったり、
部分型を上位互換と混乱を招く表現をしているとご丁寧にコメントを頂きました。
今回その修正の記事となります。
@kazatsuyu 様、ありがとうございました。
以前の記事を読んだ下さった方、混乱を招く記事を登校してしまい、申し訳ありませんでした。
こちらの記事も読んでいただけますと幸いです。
まだ、間違って解釈している所ありましたら、ご指摘いただけますと幸いです。
部分型関係は、TypeScript を理解する上で重要な概念とされています。
ただ、
「どっちがどっちの部分?」
「なぜ 〇〇 型が ◇◇ 型の部分型なら、 〇〇 型の値が ◇◇ 型の値を使える?」
となったので、部分型の概要を抑えるために「 書籍:プロを目指す人のためのTypeScript入門 」を参考に記事を書きました。
頭が柔らかくない僕は、理解するのに苦しみました。
というか、間違って解釈していました…
同じ境遇をお持ちの方は見ていただけますと幸いです。
2.目次
1. はじめに
2. 目次
3. この記事でわかること
4. 環境
5. 部分型
5.1. 部分型とは?
5.2. SuperType にオプショナルなプロパティを追加する
5.3. SuperType を SubType に代入してみると
5.4. Engineer 型にプロパティを追加
5.5. 少し複雑な部分型
6. おわりに
7. 参考
3.この記事でわかること
- 部分型の特徴
- 部分型とは、継承と同じ考え方
4.環境
- TypeScript: 4.7.4
- Node.js: 16.15.1
5.部分型
では、本題の 「部分型」は「継承」と同じ考え方 について触れていきます。
5.1.部分型とは?
2つの型の互換性を表す概念です。型 S が 型 T の部分型であるとは、S 型の値が T 型の値でもあることを指します。
書籍:プロを目指す人のためのTypeScript入門 鈴木僚太[著]
著者も書籍に書いていますが、
この言葉を見ただけでは、理解ができませんでした。
この 部分型 について、
下記の部分型サンプルコードのエンジニア(Engineer)とフロントエンドエンジニア(FrontendEngineer)を例にして見ていきたいと思います。
type Engineer = {
name: string;
year: number;
}
type FrontendEngineer = {
name: string;
year: number;
frontendSkill: Array<string>;
}
const kakedashiFront: FrontendEngineer = {
name: 'daishi',
year: 1,
frontendSkill: ['JavaScript']
};
const kakedashiEngineer: Engineer = kakedashiFront;
/**
* 実行結果
* { name: 'daishi', year: 1, frontendSkill: [ 'JavaScript' ] }
*/
console.log(kakedashiEngineer);
ここでは概要を掴むため、Engineer
型に frontendSkill
プロパティなどの スキルを記述していません。
違和感を感じるかもしれませんが、後ほど追加した内容でも説明しますのでご了承ください。
いい例が思い浮かばず申し訳ありません。
前置きはこれぐらいにして、本題に入っていきます。
このコードはコンパイルエラーになりません。
これは、FrontendEngineer
型 の値である kakedashiFront
が Engineer
型の特徴を持っているからです。
では、どういうことかそれぞれの型を見ていきます。
type Engineer = {
name: string;
year: number;
}
type FrontendEngineer = {
name: string;
year: number;
frontendSkill: Array<string>;
}
Engineer
型は、
-
name
プロパティは、string
型 -
year
プロパティは、number
型
のオブジェクトの型です。
Engineer
型では、name
と year
以外を定義していません。
なので、name
と year
以外が あってもなくても 問題ありません。
だけど、ないかも しれへんプロパティにアクセスするとコンパイルエラーになります。
次に、FrontendEngineer
型は、
-
name
プロパティは、string
型 -
year
プロパティは、number
型 -
frontendSkill
プロパティは、string
型の配列
のオブジェクトの型です。
FrontEngineer
型は Engineer
型の条件を満たしています。
この型の 条件を満たしている関係 が、部分型関係となります。
Engineer
型は 上位の型(SuperType)、FrontendEngineer
型は 下位の型(SubType) という位置づけになります。
(このように考えるといいよと、@kazatsuyu 様よりアドバイス頂きました。ありがとうございます m(_ _)m )
これは、バックエンドをかじっている私には、継承 の SuperClass、SubClass と同じ考え方なんだなと思い、しっくりきました。
継承とは、SuperClass に全体の性質を、SubClass には固有の性質を定義します。
SuperClass の性質を SubClass に引き継いで、新たに性質を付け足したり上書きしたりができる概念のことです。
なので、
-
上位の型(SuperType) の
Enigineer
型の性質(型とプロパティ)を引き継いだ 下位の型(SubType) のFrontendEngineer
型は持つ -
下位の型(SubType) の
FrontendEngineer
型に新たにプロパティと型を追加する
という特徴になります。
FrontendEngineer
型は Engineer
型を引き継いでいるから、
kakedashiFront
オブジェクトの FrontendEngineer
型を Engineer
型としても問題ありません。
これが部分型関係となります。
5.2.SuperType にオプショナルなプロパティを追加する
部分型とは?では、
Engineer
型なのにエンジニアのスキルがなく違和感を感じたかと思います。
ここでは、Engineer
型にオプショナルなプロパティの frontendSkill
を追加してみます。
type Engineer = {
name: string;
year: number;
frontendSkill?: Array<string>; // ← 追加
}
type FrontendEngineer = {
name: string;
year: number;
frontendSkill: Array<string>;
}
const kakedashiFront: FrontendEngineer = {
name: 'daishi',
year: 1,
frontendSkill: ['JavaScript']
};
const kakedashiEngineer: Engineer = kakedashiFront;
/**
* 実行結果
* { name: 'daishi', year: 1, frontendSkill: [ 'JavaScript' ] }
*/
console.log(kakedashiEngineer);
ただ、ここでも、
「エンジニアなのにフロントしか定義しないの?」っていうツッコミ👋 はとりあえず置いておいてください。
これは
実行してもコンパイルエラーになりません。
name
とyear
以外が あってもなくても 問題ありません。
だけど、ないかも しれへんプロパティにアクセスするとコンパイルエラーになります。
と記述しましたが、
ここで ないかもしれない プロパティを定義しただけになります。
このように、下位の型(SubType) のFrontendEngineer
型のプロパティを 上位の型(SuperType) の Engineer
型が違う型としてもっていても問題ありません。
5.3.SuperType を SubType に代入してみると
先程は、FrontendEngineer
(SubType)型の値を Engineer
(SuperType)型に代入しても問題なかったですが、
逆に Engineer
(SuperType)型の値を FrontendEngineer
(SubType)型に代入するとどうなるか見てみます。
type Engineer = {
name: string;
year: number;
frontendSkill?: Array<string>;
}
type FrontendEngineer = {
name: string;
year: number;
frontendSkill: Array<string>;
}
const kakedashiEngineer: Engineer = {
name: 'daishi',
year: 1,
frontendSkill: ['JavaScript']
};
// SuperType の値を SubType 型に代入
const kakedashiFrontendEngineer: FrontendEngineer = kakedashiEngineer;
これをコンパイルしてみると
Type 'Engineer' is not assignable to type 'FrontendEngineer'.
Property 'frontendSkill' is optional in type 'Engineer' but required in type 'FrontendEngineer'.
翻訳すると
'Engineer' 型は 'FrontendEngineer' 型に割り当てることができません。
'frontendSkill' プロパティ は、'Engineer' 型ではオプションですが、'FrontendEngineer' 型では必須です。
つまり、
frontendSkill
プロパティは、
SuperType の値を SubType には代入できないことが解るかと思います。
5.4.Engineer 型にプロパティを追加
Engineer
型に backendSkil
プロパティ、 infraSkill
プロパティを追加してみます。
ここまで Engineer
型に frontendSkill
プロパティしかなく違和感を感じた方もいらっしゃるかと思いますが、
ここで Engineer
型に他のスキルのプロパティも追加してみます。
type Engineer = {
name: string;
year: number;
frontendSkill?: Array<string>;
backendSkill?: Array<string>; // ← 追加
infraSkill?: Array<string>; // ← 追加
}
type FrontendEngineer = {
name: string;
year: number;
frontendSkill: Array<string>;
}
const kakedashiFront: FrontendEngineer = {
name: 'daishi',
year: 1,
frontendSkill: ['JavaScript']
};
const kakedashiEngineer: Engineer = kakedashiFront;
/**
* 実行結果
* { name: 'daishi', year: 1, frontendSkill: [ 'JavaScript' ] }
*/
console.log(kakedashiEngineer);
Engineer
型の backendSkill
プロパティ、 infraSkill
プロパティ共に、frontendSkill
プロパティ同様にオプショナルなプロパティとなっています。
つまり、backendSkill
プロパティ、 infraSkill
プロパティは、あってもなくてもいい型です。
なので、
SuperType の Engineer
型を引き継いだ SubType のFrontendEngineer
型に、backendSkill
プロパティ、 infraSkill
プロパティが存在しなくてもエラーとなりません。
5.5.少し複雑な部分型
少し応用編として、
今まで見てきた Engineer
型と FrontendEngineer
型の部分型の関係を使って、
EngineerTeam
型と FrontendEngineerTeam
型というチームの型を見てみます。
EngineerTeam
型は、FrontendEngineerTeam
型の部分型の関係になります。
type Engineer = {
name: string;
year: number;
frontendSkill?: Array<string>;
}
type FrontendEngineer = {
name: string;
year: number;
frontendSkill: Array<string>;
}
type EngineerTeam = {
teamName: string;
director: Engineer;
chief: Engineer;
employee: Engineer;
}
type FrontendEngineerTeam = {
teamName: string;
director: FrontendEngineer;
chief: FrontendEngineer;
employee: FrontendEngineer;
}
const bucho: FrontendEngineer = {
name: 'ぶちょう',
year: 20,
frontendSkill: ['TypeScript', 'React', 'Vue'],
};
const katyo: FrontendEngineer = {
name: 'かちょう',
year: 10,
frontendSkill: ['TypeScript', 'React'],
};
const kakedashi: FrontendEngineer = {
name: 'かけだし',
year: 1,
frontendSkill: ['JavaScript'],
};
const frontendTeam: FrontendEngineerTeam = {
teamName: 'frontendTeam',
director: bucho,
chief: katyo,
employee: kakedashi,
};
const engineerTeam: EngineerTeam = frontendTeam;
/**
* 実行結果
* {
* teamName: 'frontendTeam',
* director: {
* name: 'ぶちょう',
* year: 20,
* frontendSkill: [ 'TypeScript', 'React', 'Vue' ]
* },
* chief: {
* name: 'かちょう',
* year: 10,
* frontendSkill: [ 'TypeScript', 'React' ]
* },
* employee: {
* name: 'かけだし',
* year: 1,
* frontendSkill: [ 'JavaScript' ]
* }
* }
*/
console.log(engineerTeam);
今まで同様に
const engineerTeam: EngineerTeam = frontendTeam;
はコンパイルエラーとなりません。
FrontendEngineerTeam
型が EngineerTeam
型を引き継ついでいるかを見てみましょう。
type EngineerTeam = {
teamName: string;
director: Engineer;
chief: Engineer;
employee: Engineer;
}
type FrontendEngineerTeam = {
teamName: string;
director: FrontendEngineer;
chief: FrontendEngineer;
employee: FrontendEngineer;
}
director
、chief
、employee
の各プロパティ以外は同じなので、これらが引き継いでいる関係を示せれば、
FrontendEngineerTeam
型は、EngineerTeam
型を引き継いでいることが言えそうです。
director
、chief
、employee
の各プロパティは、
-
EngineerTeam
型は、それぞれEngineer
型 -
FrontendEngineerTeam
型は、それぞれFrontendEngineer
型
です。
FrontendEngineer
型は、Engineer
型を引き継いでいる(部分型)の関係でした。
ということは、
FrontendEngineerTeam
型の director
、chief
、employee
の各プロパティは、
EngineerTeam
型の director
、chief
、employee
の各プロパティ型を引き継いでいることが言えますね。
したがって、
FrontendEngineerTeam
型は、EngineerTeam
型を引き継いでいる(部分型)となります。
6.おわりに
部分型の理解に苦しみましたが、継承と同じような特徴なんだなと認識すると部分型掴むことができました。
ご指摘頂いた @kazatsuyu 様ありがとうございました。
部分型の概念を理解できたので、TypeScript の世界がもっと広がればいいなと思います。
併せて他の記事も読んでいただけると嬉しいです🙇♂️
最後まで読んでいただきありがとうございました。