2020-07-30

TypeScript その2 関数・クラス

つづきです(めんどいのでTypeScriptはTSと書く)

関数でTypeScriptを使うときの注意点

前回も少し書いたけど、関数でTSの型を使う際に気を付ける箇所は

  • 引数
  • 戻り値

の2点のみ

JSでは色々な方法で関数を定義することができるが、それぞれの関数における書き方は以下の通り

// functionキーワードによる関数定義
function sum(arg1: number, arg2: number): number {
  return arg1 + arg2
}

// 無名関数による定義
const sum2: (arg1: number, arg2: number) => number = function(
  arg1: number,
  arg2: number,
): number {
  return arg1 + arg2
}

// アロー関数による定義
const sum3: (arg1: number, arg2: number) => number = (
  arg1: number,
  arg2: number,
): number => arg1 + arg2

オプショナルな引数としたい場合は以下のように ? を付与する

const sum4: (arg1: number, arg2: number, arg3?: boolean) => number = (
  arg1: number,
  arg2: number,
  arg3?: boolean,
): number => {
  if(arg3) console.log("arg3 is true")
  return arg1 + arg2
}

sum4(5, 6)       // => OK
sum4(5, 6, true) // => OK

デフォルト引数を指定したい場合は以下の通り

const hoge: (arg1: number, arg2?: number) => number = (
  arg1: number,
  arg2: number = 1.1
): number => {
  return arg1 * arg2
}

console.log(hoge(1000, 1.5)) // 1500
console.log(hoge(1000))      // 1100

Restパラメータ(引数の個数を定めず(予測ができず)、何個でも渡していいパラメータ)の場合は、以下のようにArray指定してやればよい

const hoge2: (...values: number[]) => number = (
  ...values: number[]
): number => {
  console.log(values)
  return 123
}

console.log(hoge2(1, 2, 3))

関数をオーバーロードする場合は以下の通りシグネチャを使用する

// シグネチャを必要なだけ定義する
// ここでは数値を二倍にするdoubleと、文字を二つ連結するdoubleを定義したいので以下のようになる
function double(value: number): number
function double(value: string): string

// double関数の実態を定義する
// 引数や返り値の型は実態側で具体的に定義せずanyにする(unionなどで指定しない)
// 型の制約を行うのはシグネチャの役割のため
function double(value: any): any {
  if(typeof value === "number") value * 2
  if(typeof value === "string") value + value
}

console.log(double(100))    // 200
console.log(double('hako')) // hakohako
console.log(double(true))   // booleanを使えるシグネチャの定義がないのでerror!

クラスでTypeScriptを使うときの注意点

クラスで型定義を行うと以下のようになる
注意点としてはconstructorに返り値の型指定はしてはならないということくらい

class Person {
  // publicやprivateのことをアクセス修飾子と呼ぶ
  public name: string // person.name でアクセスが可能(publicは書かなくても同じ動作になる)
  private age: number // person.age でアクセスできない(Personクラス内部でのみアクセス可能)
  protected nationality: string // person.nationalityでアクセスできない(Person内部とサブクラスのみアクセス可能)

  // constructorは何も返さないので、返り値の型指定は不要
  // (返り値はないが、void型も指定してはいけない)
  constructor(name: string, age: number, nationality: string) {
    this.name = name
    this.age = age
    this.nationality = nationality
  }

  profile(): string {
    return `name: ${this.name}, age: ${this.age}, nationality: ${this.nationality}`
  }
}

const taro = new Person('taro', 99, 'ja')
console.log(taro.profile()) // name: taro, age: 99, nationality: ja

アクセス修飾子については以前記事を書いている => https://hakozaru.com/posts/js-public-private

もしPersonクラスを継承したAndroidクラスを定義した場合

class Android extends Person {
  constructor(name: string, age: number, nationality: string) {
    super(name, age, nationality)
  }

  profile(): string {
    // プロパティ 'age' はプライベートで、クラス 'Person' 内でのみアクセス可能なので
    // this.ageはコンパイルエラーになる
    return `name: ${this.name}, age: ${this.age}, nationality: ${this.nationality}`
  }
}

のような感じでアクセス修飾子についてはちゃんと意識しておきましょうと言う話でした

constructorメソッドとメンバ変数

TypeScriptを使っている場合、 constructor メソッドを以下のように書くと、メンバ変数の定義を省略しつつ初期化まで行ってくれる

class Person {
  // 省略したくなるけど最後の {} は必須
  // また、通常は書かないがpublicも省略はできない
  constructor(public name: string, private age: number) {}
}

const taro = new Person('taro', 99)
console.log(taro) // Person { name: 'taro', age: 99 }

ゲッターとセッター(アクセサ)

クラスのpublicなメンバ変数は、デフォルトでインスタンス経由からアクセス可能で、変更も可能

class Person {
  name: string
  age: number

  constructor(name: string, age: number) {
    this.name = name
    this.age = age
  }
}

const taro = new Person('taro', 99)
console.log(taro.name, taro.age) // taro 99
taro.name = 'hako'
taro.age = 123
console.log(taro.name, taro.age) // hako 123

しかし、クラスによっては自由に参照されたり、変更されると困る場合も考えられる
そんなときに使えるのがゲッターやセッターなどのアクセサである

上のPersonクラスのメンバ変数で、ageの読み取りは許可するが、変更を拒否したい場合は以下のようにする

class Person {
  name: string
  private _age: number // privateなメンバ変数名をアンスコから始めるのは慣習

  constructor(name: string, age: number) {
    this.name = name
    this._age = age
  }

  get age(): number {
    return this._age
  }

  // セッターの場合は↓(当然getterと同名で問題ない)
  // set age(newAge: number) {
  //   this._age = newAge
  // }
}

const taro = new Person('taro', 99)
console.log(taro.name, taro.age) // taro 99
taro.name = 'hako'
taro.age = 123  // 読み取り専用プロパティであるため、'age' に代入することはできない
taro._age = 123 // プロパティ '_age' はプライベートで、クラス 'Person' 内でのみアクセス可能
console.log(taro.name, taro.age) // hako 99

用件に応じてメンバ変数をprivate化したり、アクセサで調整するとよい

readonly修飾子

実はTSにはreadonly修飾子があるので「読み取りは許可するが変更は拒否する」というメンバ変数を簡単に定義することができる

class Person {
  readonly name: string

  constructor(name: string) {
    this.name = name
  }
}

// 省略記法でもOK
// class Person {
//   constructor(readonly name: string) {}
// }

const taro = new Person('taro')
taro.name          // OK
taro.name = 'hoge' // NG

静的メンバ(クラス変数)

class Me {
  static hoge: string = 'is static'

  static test(): string {
    return this.hoge
  }
}

console.log(Me.hoge)   // is static
console.log(Me.test()) // is static

特に書くことがない

名前空間

TSでは名前空間を使うための機能も提供されている

namespace Hoge {
  // Hoge.XXXX の形でアクセスできるようにexportする必要がある
  export class Fuga {}

  // さらに名前空間を定義する場合は、それもexportする必要がある
  export namespace Hako {
    // ...
  }
}

class Fuga {}

Hoge.Fuga

継承

TS的に気を付ける点は特にない
親クラスの関数を呼ぶ時は super.methodName() みたいに呼べることだけ覚えておけばよさそう
(あとJSのクラスは単一継承)

class Parent {
  name: string

  constructor(name: string) {
    this.name = name
  }

  run(): string {
    return 'I can run'
  }
}

class Child extends Parent {
  speed: number

  constructor(name: string, hoge: number) {
    super(name)
    this.speed = hoge
  }

  run(): string {
    return `${super.run()} ${this.speed} km/s`
  }
}

// I can run 999 km/s
console.log(new Child('Myname!', 999).run())

抽象クラス/メソッド

必ずオーバーライドする必要があるメソッドのことで、処理の実態が存在しないメソッドを指す
〇〇というメソッドがある、ということを宣言しているだけ
(この宣言のことを↑でも書いたがシグネチャと呼ぶ)

抽象メソッドを使う場合は、クラスも抽象クラスになっていなければならない
具体的にコードで示すと

// abstractを付与すると、抽象クラス/メソッドになる
// Parentは抽象クラス
abstract class Parent {
  // hogeは抽象メソッド
  abstract hoge(): string
}

class Child extends Parent {
  hoge(): string {
    return "hoge"
  }
}

// hogeを実装していないのでerror!
class Child2 extends Parent {}

Rubyだと NotImplementedError 例外を発生させるやつですね

クラスで使うinterface

JSでは単一継承しかできないものの、複数のinterfaceを継承(のようなことを)することはできる

前回の記事で紹介したinterfaceでは「オブジェクト型に名前をつける」ために利用していたが
どちらかというと、この複数継承っぽいことを実現するために使う方が多いらしい

interface InterfaceOne {
  methodOne(): void
}

interface InterfaceTwo {
  methodTwo(): void
}

class Hoge implements InterfaceOne, InterfaceTwo {
  methodOne(): void {
    console.log('method one!')
  }

  methodTwo(): void {
    console.log('method two!')
  }
}

const hoge = new Hoge()
hoge.methodOne() // method one!
hoge.methodTwo() // method two!

interfaceとtypeはとても似ているが、なるべくinterfaceを使うのがいいらしい
参考: https://qiita.com/tkrkt/items/d01b96363e58a7df830e