TypeScriptハンズオンとTypeScript Deep Diveを読んでTypeScriptの型とクラスについて学び直しました。
目次
TypeScriptの型システム
型は、コードの品質と読みやすさを高めることができます。
また、型はリファクタリングをする際の開発スピードを高め、型があることによって、コードを書いている時点でエラーに気づくことができます。開発中に早い段階でエラーに気づけるのは大きなメリットです。
暗黙的な型推論
型推論とは、TypeScriptがソースコードを解析し、そのコードの流れから変数や関数などの型を推測してくれる仕組みのことです。
TypeScriptは型を推論することによって、不明確なコードに対しエラーを表示してくれます。
明示的な型指定
TypeScriptは型を推論しますが、型推論の結果が正しくない場合や正確でない場合は、開発者が、明示的にコード上で、型を指定する(型アノテーションを書く)ことができます。
型アノテーションを書くことによって、開発者にとってのドキュメントにもなります。
TypeScriptの型の深堀り
型エイリアス
指定した型に別名を設定することができます。エイリアスという名の通り、新しい型というよりかは既にある型に別の名前をつけているといった感じです。
type 新型名 = 型名
1 2 3 4 5 6 | type name = string type age = number type person = [name, age] const taro: person = ['taro', 20] |
リテラル型
リテラルというのは直接ソースコードに書かれる値のことです。以下のようにokだけしか値がない型が作れます。
type ok = ‘ok’
条件型(Conditional Types)
複数の型を許容する新しい型エイリアスが作成できます。
type 型名 = 型1 | 型2 | …
リテラル型と条件型を組み合わせることで、複数の値のいずれかを許容するenumのような働きをする型が作れます。
type msg = ‘hello’ | ‘bye’
ユーティリティ型
変数のさまざまな性質を付加する特殊な型が用意されており、これらをユーティリティ型といいます。以下のようなユーティリティ型があります。
Partial<T>
- Partial<T>はTの全てのプロパティをOptional(任意)のプロパティにしてくれる
Required<T>
- Required<T>はTの全てのプロパティを必須のプロパティにしてくれる
Readonly<T>
- Readonly<T>はTの全てのプロパティをreadonlyのプロパティにしてくれる
他のユーティリティ型などは以下を参照してください。
https://qiita.com/k-penguin-sato/items/e2791d7a57e96f6144e5
総称型(ジェネリクス)
総称型は値の型を特定せずに使用するための仕組みです。
<T>というのが、ここで使われている総称型の指定です。総称型は<>の中に抽象的な型を表す名前を指定します。このTは別にTでなくても良いが、TypeScriptではTから始まるアルファベットを使う習慣があるそうです。
複数の総称型を指定したい場合は<T, U, V…>という具合でカンマ区切りで記述します。
function 関数 <T> (引数) : 戻り値
ジェネリクスでUnitテストで使うmockを作ると以下のように書けそうです。
1 2 3 | function mock<T>(): T { return ({} as unknown) as T } |
TypeScriptのクラスの深堀り
インターフェース
インターフェースはオブジェクト構造を記述するための仕組みです。インターフェースではオブジェクトに用意するプロパティやメソッドを用意できます。
ただあくまでインターフェースは構造の定義をするもので、そのまま使ってオブジェクトを作ることはできません。
interface 名前 {
プロパティ: 型
メソッド(引数): 型
…
}
インターフェースの実装
用意されたインターフェースは、クラスに指定することで利用されます。クラス名の後にimplementsというキーワードを付けます。
このようにimplementsするとそのクラスでは組み込まれたインターフェースに用意されているプロパティやメソッドを全て用意しなければならないです。
つまりインターフェースはクラスにプロパティやメソッドの実装を保証するものです。
class 名前 implementes インターフェース
クリーンアーキテクチャで考えると、Portレイヤーにインターフェースを定義し、Gatewayレイヤーで実装する感じになりそうです。
1 2 3 4 5 6 7 8 9 10 | interface SamplePort { find(): Promise<number> } export class SampleGateway implements SamplePort { async find(): Promise<number> { … } } |
インターフェースの継承
インターフェースを定義するときに「extends インターフェース」と指定することで、既にあるインターフェースを継承した新たなインターフェースを作成できます。
継承して作られたインターフェースはimplementesする際に継承もとのインターフェースに用意されているプロパティやメソッドまで含めて全て実装する必要があります。
抽象クラス
インターフェースと似たような働きをするものに抽象クラスがあります。抽象クラスは具体的な処理を持たない抽象的な存在としてのクラスです。
クラス定義の冒頭にabstractを付けることで抽象クラスになります。またメソッドもabstractをつけることで抽象メソッドになります。
abstract class クラス名 {
abstract メソッド(): 型
…
}
抽象クラスとインターフェースの違い
抽象クラスとインターフェースはかなり似ています。ほぼimplementesするかextendsするかの違いですが、それでも以下のような違いがあります。
他にクラスを継承する必要があるか
もし他のクラスを継承するのであれば、抽象クラスは使えません。extendsで継承できるのは1つのクラスのみであるため、抽象クラスと他のクラスを同時に継承できません。
プロパティを義務付けるか
抽象クラスは基本的にメソッドを定義するものです。抽象プロパティはないため実装クラスに必ずプロパティを用意させたければインターフェースを使う必要があります。
静的メンバー
基本的にプロパティやメソッドはインスタンスを作成して利用するという共通点があります。クラスは基本的にインスタンスを作って利用するものであるため当然とも言えます。
ただ場合によってはインスタンスが必要ないクラスもあります。こうした場合クラスのプロパティやメソッドを静的メンバーとして用意することで、クラスから直接使えるようにできます。
この静的メンバーは「static」というキーワードを使って作成します。
static プロパティ: 型
static メソッド(引数): 型
パラメータプロパティ
読み取りのみのプロパティを扱う場合、パラメータプロパティと呼ばれる機能を使う方法があります。
パラメータプロパティはコンストラクタの引数をそのままプロパティとして扱えるようにする機能です。コンストラクタにreadonlyを指定したプロパティを用意するとそれは「変更不可のプロパティ」として使えるようになります。
1 2 3 4 5 | class People { constructor( readonly name: string ) {} } |