【技術書記録005-2 関数】プログラミングTypeScript スケールするJavaScriptアプリケーション開発

TypeScriptオライリー本第2弾やっていきましょう。今回は関数について。

オプションパラメーターとデフォルトパラメーター

オプションパラメーター:引数を省略可能に指定する デフォルトパラメーター:引数にデフォルト値を与える

function band(
  guitar: string,
  bass = 'bacchus',
  keyboard?: string
) {
  return `バンドの構成は${guitar}, ${bass}, ${keyboard}`;
}

?がついているのがオプションパラメーター。この?はNullableな変数・プロパティにもつけられるやつですね。

レストパラメーター

引数を可変長に指定する方法。

function sum(...nums: number[]): number {
  return nums.reduce((total, n) => total + n, 0)
}

sum(1,2,3)  // 結果: 6

呼び出しシグネチャ(型シグネチャとも)

関数の型を表現する構文。

function runningMileage(distance: number, speed: number): number {
  return distance * speed
}

の型は

(distance: number, speed: number) => number

と表現できる。
戻り値の型、レストパラメーター、オプションパラメーターは表現できるが、デフォルトパラメーターは表現できない。デフォルト値は値であり、型ではないから。

文脈的型付け

先ほどの呼び出しシグネチャを型エイリアスにして、その実装を書き出してみる。

type Mileage = (distance: number, speed: number) => number

let walkingMileage: Mileage = (distance, speed) => {
  return distance * speed
}

このように、実装で引数・戻り値の型を指定しなくても、TypeScriptが文脈からそれぞれの型を推論してくれる。

オーバーロードされた関数の型

さきほどのtype Mileageは省略記法。完全な書き方は

type Mileage = {
  (distance: number, speed: number): number
}

関数型のオーバーロードの際は完全なシグネチャの方が適している。 つけ麺の注文を例にしてみる(Order型は別に宣言されていると想定)。

type OrderOfTukemen = {
  (noodleAmount: number, topping: string[], atumori: boolean): Order
  (noodleAmount: number, atumori: boolean): Order
}

let order: OrderOfTukemen = (noodleAmount, topping, atumori) => {
  // ...
}     // エラー!割り当てできない!

type OrderOfTukemenオーバーロードされた関数の宣言。この場合、結合された呼び出しシグネチャを宣言する必要がある。

type OrderOfTukemen = {
  (noodleAmount: number, topping: string[], atumori: boolean): Order
  (noodleAmount: number, atumori: boolean): Order
}

let order: OrderOfTukemen = (
  noodleAmount: number,
  toppingOrAtumori: string[] | boolean,
  atumori?: boolean
) => {
  // ...
}

でも、この書き方分かりにくい気がする。これだったら型を分けて宣言してもよくない?と思う。

完全な型シグネチャを使って、関数のプロパティをモデル化することも可能。

type EatDonburi = {
  (donburi: string): void
  tableware: string
}

function lunch(donburi: string) {
  console.log(`ランチに${donburi}${lunch.tableware}で食べる`)
}
lunch.tableware = 'お箸'

lunch('カツ丼')     // ランチにカツ丼をお箸で食べる

ポリモーフィズムジェネリック

タイトルをポリモーフィズムにしときながら、それ自体の解説は一切なしなんて、さすがオライリー本!
ジェネリックの内容がポリモーフィズムそのものやし、分かるよね?ってことですか。
ある関数や型について、その中で使われる引数やプロパティの型を制約する方法がジェネリック
良い具体例が浮かばなかったので、本書のmapの実装を拝借。

function map<T, U>(array: T[], f: (item: T) => U): U[] {
  let result = []
  for (let i = 0; i < array.length; i++) {
    result[i] = f(array[i])
  }
  return result
}

console.log(
  map(
    ['sea', 'mountain', 'sky'],
    _ => _.length
  )
)     // [ 3, 8, 3 ]

つまりmapは「T型の配列のarrayと、T型の引数を取りU型の値を返す関数fを引数に取り、U型の配列を返す」関数となる。
こうすると、呼び出す時々で引数に与えるデータ型を変えることができ、受け取る結果も変えることができる。汎用性の高い関数を、型安全に定義できるのがジェネリックのメリットか。

ジェネリックの宣言位置
// Tのスコープが個々の完全シグネチャに限られるパターン
type Filter1 = {
  <T>(arr: T[], f: (item: T) => boolean): T[]
}

// Tのスコープがシグネチャ全体に及ぶパターン
type Filter2<T> = {
  (arr: T[], f: (item: T) => boolean): T[]
}

// Fileter1の省略記法
type Filter3 = <T>(arr: T[], f: (item: T) => boolean) => T[]

// Filter2の省略記法
type Filter4<T> = (arr: T[], f: (item: T) => boolean) => T[]

// 名前付き関数の呼び出しシグネチャ
function Filter5<T>(arr: T[], f: (item: T) => boolean): T[]
ジェネリックエイリアス&制限付きポリモーフィズム
type Ozoni<T> = {
  omoti: string
  soup: T
}

そのまんま、型エイリアスジェネリックを使用したもの。

制限付きポリモーフィズムは継承や交差型等を使って、ジェネリックの型に制限を付けたもの。 本書をまねるとこんなかんじ?

// 継承を使用したパターン
type Pet = {
  name: string
}

type Cat = Pet & {
  longHaired: boolean
}

type Dog = Pet & {
  size: 's' | 'm' | 'l'
}

function mapPet<T extends Pet>(
  pet: T,
  f: (name: string) => string
): T {
  return {
    ...pet,
    name: f(pet.name)
  }
}
// 交差型を継承した複数制約付きパターン
type BodyWeight = {kg: number}
type Height = {cm: number}

function calculateBMI<
  BMI extends BodyWeight & Height
  >(b: BMI): void {
  console.log(b.kg / (b.cm * b.cm))
}

type Bmi = BodyWeight & Height
let bmi: Bmi = {kg: 60, cm: 176}
calculateBMI(bmi);     // 0.00193698...

例を考えるのが大変。。。そう簡単にジェネリックを使いこなせる気がしないな。

制限付きポリモーフィズムを使って可変長引数をモデル化

いよいよ頭がしんどくなってきたので例をまるごと拝借。JSのcallを独自実装。

function call<T extends unknown[], R>(
  f: (...args: T) => R,
  ...args: T
): R {
  return f(...args)
}

function fill(length: number, value: string): string[] {
  return Array.from({length}, () => value)
}

call(fill, 10, 'hoge')     // 'hoge'が10個入った配列

このcallにおいて、Tはunknownの配列のサブタイプ、つまり任意の型の配列またはタプル、Rは戻り値の型を表す。callがfillを取っているので、Rはstringの配列となる。
そのため、最後のcallの呼び出しでは引数に関数fillと、fillに与えるnumber型とstring型の2つの引数を渡している。うーんむずかしい。

ジェネリック型のデフォルトの型

関数の引数同様、ジェネリックにもデフォルト値ならぬデフォルト型を与えられる。

type MyEvent < T extends HTMLElement = HTMLElement > = {
  target: T
  type: string
}

こうすることで、TはHTMLElementのサブタイプに限定しつつ、デフォルトの型はHTMLElementに指定することができる。これは使いやすそうな気がする。


関数の章終わり!今回はここまで!
ジェネリックを自分で使いこなすイメージが持てない。
でもジェネリックを理解してると、OSSソースコードを読む際等に役立つと思う。
使えなくても読めるようにはしておこう。

次は「クラスとインターフェース」の予定。