はじめに
今回はインターフェースについてまとめました。
オブジェクト指向の核心に迫ってきてますね...
【目次】
まずは基本的な型宣言をマスターしよう!
オブジェクト型の型宣言をマスターしよう!
関数の型定義をマスターしよう!
配列をマスターしよう!
インターフェースをマスターしよう! ←🈁
クラスを使いこなそう!
型修飾子についての理解を深めよう!
ジェネリックをマスターしよう!
独自の構文拡張をマスターしよう!
インターフェースとは?
インターフェースとは、構造的なデータの型定義に利用できる機能です。
インターフェースの宣言
interface Person {
name: string,
age: number
}
型エイリアスとの違い
同じような機能で型エイリアスがありました。
type PersonObj = {
name: string,
age: number
}
インターフェースと型エイリアスの違いは以下の通りです。
- クラスの構造的な型定義はインターフェースのみ可
- インターフェースは拡張、マージが可
- インターフェースは型エイリアスより高速(内部にキャッシュ)
上記理由から、インターフェースが型エイリアスより好んで利用されることが多いです。
代表的な機能
インターフェースの機能について、代表的なものをいくつかご紹介します。
オプションプロパティ
オブジェクト型の記事でもご紹介しましたが、プロパティ名の後に ?
をつけることで省略可能なオプションを定義することができます。
interface Person {
name: string,
age: number
}
interface PersonOptionalAge {
name: string,
age?: number
}
// NG: Property 'age' is missing in type '{ name: string; }' but required in type 'Person'.
let taro: Person = {
name: "Taro",
}
// OK
let haewon: PersonOptionalAge = {
name: "Haewon"
}
読み取り専用プロパティ
インターフェースではプロパティへの書き込みを禁止する、読み取り専用のプロパティを readonly
を使用して宣言できます。
以下の例では age
を読み取り専用として定義しているため、name
への再代入は成功していますが、 age
への再代入は型エラーが発生しています。
interface Person {
name: string,
readonly age: number
}
let taro: Person = {
name: "Taro",
age: 25
}
// OK
taro.name = "Haewon"
//NG: Cannot assign to 'age' because it is a read-only property.
taro.age = 21;
関数型を格納したインターフェース
インターフェースのメンバーに関数を指定する方法についてご紹介します。
- メソッド式
member(): void
- メンバーがオブジェクトのメンバーとして呼び出されることを意図
-
this
の参照先はオブジェクトとなる
- プロパティ式
member: () => void
- メンバーがオブジェクトから独立した関数(アロー関数)として呼び出されることを意図
-
this
の参照先はレキシカルスコープから決定される
以下のコードは上記を確認するためのサンプルコードです。
let value = "Global"
interface Buz {
value: string,
memberFn: () => void,
methodFn(): void
}
let buz: Buz = {
value: "Buz",
memberFn: () => console.log(this.value), // NG: The containing arrow function captures the global value of 'this'.
methodFn() { console.log(this.value) }
}
呼び出しシグネチャ
インターフェースは、関数として呼び出せる値に対して、呼び出し方法を型システムで定義することができます。
これを 呼び出しシグネチャ といいます。
呼び出しシグネチャは関数型と似ていますが、「=>
」の代わりに「:
」を利用します。
interface sampleFn {
(input: string ): void
}
const hoge: sampleFn = (text: string) => console.log(text);
上記の例では string
型の引数を受け取り値を返さない関数をインターフェースを利用して、定義しています。
ユーザー定義のプロパティを持つ関数の作成
呼び出しシグネチャを利用することでユーザー定義のプロパティを持つ関数を定義することができます。
JavaScriptのおさらいでもありますが、関数もオブジェクトです。
関数として独自のプロパティを持つことができます。
以下の例では count
プロパティを持つ関数のインターフェースを定義しています。
interface hasPropertyFunc {
count: number,
(text: string): void
}
const sampleFn: hasPropertyFunc = (input: string) => {
sampleFn.count += 1;
console.log(input, sampleFn.count)
}
// インタフェースは初期値を代入することができないので初期化。
sampleFn.count = 0;
// OK: "inputText", 1
sampleFn("inputText");
関数内でcount
プロパティを参照しない関数を代入すると、型エラーが発生します。
interface hasPropertyFunc {
count: number,
(text: string): void
}
// NG: Property 'count' is missing in type '(input: string) => void' but required in type 'hasPropertyFunc'.
const fooFn: hasPropertyFunc = (input: string) => {
console.log(input)
}
ネストされたインターフェース
オブジェクト型でインターフェースがネストできたことと同様にインターフェース型でもインターフェースの定義をネストさせることができます。
詳細がわからない方はオブジェクト型の記事をご確認ください。
interface Letter {
author: Author,
to: string
}
interface Author {
address: string,
name: string
}
let letter: Letter = {
author: {
name: "Taro",
address: "Tokyo"
},
to: "Hoge"
}
インターフェースの拡張
インターフェースは extends
キーワードを利用することで拡張することができます。
以下の例ではWriteAndRead
(派生インターフェース)に対して、Write
, Read
(ベースインターフェース)の機能を追加しています。
interface Write {
writierSkill: number
}
interface Read {
readerSkill: number
}
interface WriteAndRead extends Write, Read {
comprehensiveSkill: number
}
上記、WriteAndRead
型を実装した場合、以下3つのプロパティ定義が必要です。
writierSkill
readerSkill
comprehensiveSkill
let hoge: WriteAndRead = {
writierSkill: 10,
readerSkill: 8,
comprehensiveSkill: 9
}
// NG: Property 'comprehensiveSkill' is missing in type '{ writierSkill: number; readerSkill: number; }' but required in type 'WriteAndRead'.
let foo: WriteAndRead = {
writierSkill: 10,
readerSkill: 8,
}
オーバーライド
他のオブジェクト指向言語を学んだ方は馴染みがあると思いますが、
WriteAndRead
(派生インターフェース)は、Write
, Read
(ベースインターフェース)の機能を上書きすることができます(オーバーライド)
オーバーライドの制限として、オーバーライドした型(リテラル)がベースクラスに割当可能である必要があります
interface Page {
path: string;
}
interface IndexPage extends Page {
path: "/";
}
インターフェースのマージ
インターフェースの重要な特徴の1として、マージすることができることです。
マージは拡張(extends
)とは異なり、同スコープで同名のインターフェースが定義された場合に、それらが結合され、宣言された全てのフィールドを持った1つのインターフェースになることを意味します。
interface Merged {
first: string,
}
interface Merged {
second: string
}
上記の例ではMerged
インターフェースはマージされ、以下2つのプロパティを持つインターフェースと同意義なります。
interface Merged {
first: string,
second: string
}
実際に使うタイミングをイメージできない方がいると思いますが、利用例としては以下の通りです。
- 組み込みオブジェクトの拡張(
window
...) - 外部パッケージに含まれるオブジェクトの拡張
ひとまず、以下を覚えておけばよさそうですね...
同スコープで同名のインターフェースが定義された場合に、それらが結合され、宣言された全てのフィールドを持った1つのインターフェースになる
まとめ
以上です!
お疲れ様でした!
プログラム初学者の方は、クラスやインターフェースで詰まることが多いと思います...
これらの領域は、言語の本質となる話なので理解できるまで繰り返すことをお勧めします!
次の記事ではクラスについてまとめていきたいと思います。