Ch.4 関数

www.oreilly.co.jp

エイリアス

ある型を指し示す別名を宣言する方法。 何気なく出てくるが、Javaではクラスでしか定義しようの無かったことをTypeScriptでは簡単に実現している。

type User = {
  firstName: string,
  lastName: string,
  age: number
}

呼び出しシグネチャ

JavaでいうStreamAPIやPythonのlambdaの様に、関数そのものを引数として渡したり関数そのものを返却する際には、関数にも型の情報が必要になる。

この情報を呼び出しシグネチャ(型シグネチャ)と呼んでいる。

Functionという型も存在するが、この型は包括的過ぎて特定の関数に関する情報を何も持たない。関数の引数と戻り値の型を示す下記の様な型情報を指す。

(a: number, b: number) => boolean

ここでいうaとかbとかの仮変数の名前は、特に意味を持たない。 実際の関数では別の名前で仮引数を定義しても良い。

また、仮引数のデフォルト値は呼び出しシグネチャでは再現出来ない。

呼び出しシグネチャを型エイリアスとして宣言しておくことで、実際の関数の実装に際しては逐一パラメータや戻り値のの型注釈をせずに済む。

type FilterNum = {
  (array: number[], f: (intem: number) => boolean): number[]
}

const filterNum: FilterNum = (array, func) => {
  let result = []
  for(let i = 0; i < array.length; i++){
    let item = array[i]
    if(func(item)){
      result.push(item)
    }
  }
  return result
}

ポリモーフィズム

オーバーロード

呼出シグネチャを複数持つ関数をオーバーロードされた関数と呼ぶ。

type OverloadFilter = {
  (array: number[], f: (item: number) => boolean): number[]
  (array: string[], f: (item: string) => boolean): string[]
  (array: object[], f: (item: object) => boolean): object[]
}

let filter: OverloadFilter = (array, func) => {
  let result = []
  for(let i = 0; i < array.length; i++){
    let item = array[i]
    if(func(item)){
      result.push(item)
    }
  }
  return result
}

ただし、このような書き方はエラーとなる。 そこで、ジェネリック型パラメータを利用する。これはJavaでいうジェネリクスに相当する。

ジェネリクス

type FilterGenerics = {
  <T>(array: T[], f: (item: T) => boolean): T[]
}

const filterGen: FilterGenerics = (array, func) => {
  let result = []
  for(let i = 0; i < array.length; i++){
    let item = array[i]
    if(func(item)){
      result.push(item)
    }
  }
  return result
}

console.log(filterGen([100,101,102,103,104,105], n => n % 2 == 0))
console.log(filterGen(['Apple','Orange','Peach','Australia','America','India'], x => x.startsWith('A')))

上記の様な使用法では、型パラメータTに具体的な型をバインドするタイミングを実行時であった。 エイリアスを利用するときに具体的な型をバインドするような使い方も出来る。

type FilterGenerics<T> = {
  (array: T[], f: (item: T) => boolean): T[]
}

const filterGen: FilterGenerics<number> = (array, func) => {
  let result = []
  for(let i = 0; i < array.length; i++){
    let item = array[i]
    if(func(item)){
      result.push(item)
    }
  }
  return result
}

console.log(filterGen([100,101,102,103,104,105], n => n % 2 == 0))
//console.log(filterGen(['Apple','Orange','Peach','Australia','America','India'], x => x.startsWith('A')))

エイリアスを利用して関数filterGen定義する際に型パラメータを指定している。

実行時に指定する場合異なり、型エイリアスにおける型パラメータの指定が左辺に来ていることに注目する。 更に型アノテーションを使って関数を定義する際には左辺で、型の指定を行っている。

型パラメータ付きの型エイリアスの指定の仕方には下記の様な省略記法もある。

type FilterOmit     = <T>(array: T[], f: (item: T) => boolean) => T[] //Tのスコープが狭い
type FilterOmit2<T> = (array: T[], f: (item: T) => boolean) => T[] //Tのスコープが広い

ジェネリクス(2つ以上の型パラメータ)

型パラメータを二つ以上とるジェネリクスの指定も出来る。

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
}

map関数は配列arrayの各要素に対して、f関数の処理を行った別の配列resultを返却する。 map関数の様に2つ以上の型パラメータを使う際の型推論All or Notthing方式で、全て指定するか全く指定しないかの二者択一である。

map(
  [1,2,3,4,5],
  _ => _ **2
)

map<String, number>(
  ['apple', 'orange', 'grape'],
  _ => _.length
)

map<String, number | boolean>(
  ['apple', 'orange', 'grape'],
  _ => 'a' === __dirname.charAt(0)
)

map<String, number | boolean>(
  ['apple', 'orange', 'grape'],
  _ => _.length
)

ジェネリクス型推論関数に渡す引数の型情報によってしか行われない。 引数を持たない関数でジェネリクスが行われている場合、明示的に型パラメータを指定してあげる必要がある。

let promise = new Promise(resolve => 
  resolve(45)  
)
promise.then(result =>
  result * 4 //'result''は 'unknown' 型です。ts(18046)
)

let promise_type = new Promise<number>(resolve => 
  resolve(45)  
)
promise_type.then(result =>
  result * 4  
)

制限付きポリモーフィズム

制限付きポリモーフィズムとは引数として取りうる型に、「少なくともT型でなくてはならない」という(上限を設ける)ことで具体的には下記の様に利用される。

type TreeNode = {
  value: string
}

type LeafNode = TreeNode & {
  isLeaf: true
}

type InnerNode = TreeNode & {
  children: [TreeNode] | [TreeNode, InnerNode]
}

let a: TreeNode  = {value: 'a'}
let b: LeafNode  = {value: 'b', isLeaf: true}
let c: InnerNode = {value: 'c', children: [b]} 

function mapNode<T extends TreeNode>(
  node: T,
  f: (value: string) => string
):T {
  return node
  return {
    ...node,
    value: f(node.value)
  }
}

console.log(mapNode(c, _ => _.toUpperCase()))
console.log(mapNode(b, _ => _.toUpperCase()))

実行結果は下記のようになる

{ value: 'C', children: [ { value: 'b', isLeaf: true } ] }
{ value: 'B', isLeaf: true }

mapNode関数の戻り値がreturn内で展開演算子(スプレッド構文)を使って展開されていることに注目する。 戻り値は「少なくともTreeNodes型である」必要があるため、変数nodeをそのまま返却することも出来る。

今回はvalueの値のみ変換した値を返却するので、それ以外の値は(TreeNodeの型に合致する様に)展開して返却する必要がある。 この様に展開することで、オブジェクトの一部のみを上書して返却することが出来る。

TypeScriptのスプレッド演算子で上書き

「制限付きポリモーフィズム」は2つ以上の型の合併型を指定することも出来る。

type HasSides = {numberOfSides: number}
type SidesHaveLength = {sideLength: number}

function logPerimeter<
  Shape extends HasSides & SidesHaveLength
>(s: Shape): Shape{
  console.log(s.numberOfSides * s.sideLength)
  return s
}

type Square = HasSides & SidesHaveLength
let square : Square = {numberOfSides: 4, sideLength: 3}
logPerimeter(square)
logPerimeter({numberOfSides: 5, sideLength: 8})
//logPerimeter({numberOfSides: 1}) //型 '{ numberOfSides: number; }' の引数を型 'HasSides & SidesHaveLength' のパラメーターに割り当てることはできません。プロパティ 'sideLength' は型 '{ numberOfSides: number; }' にありませんが、型 'SidesHaveLength' では必須です。ts(2345)

実引数(呼び出し側)では、合併型は事前に定義(ここではSquare型)してから利用してもよいし、直接指定しても良い。

「制限付きポリモーフィズム」では、引数の型の型推論を厳密に行う。

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

let myFunc = (squ: number) => squ ** 2
call(myFunc, 4)
//call(myFunc, 'a')
//call(myFunc, 1,2,3)

let myFunc2 = (squ1: number, squ2: number) => squ1 * squ2
call(myFunc2, 4, 5)
//call(myFunc2, 4)
//call(myFunc2, 'a')

上記の例では関数fの引数argsについて、引数の長さも含めた型推論を行っている。 関数fの取る引数がnumber1つであれば、argsの型Tはnumberを一つだけ取るものとして型推論を行う。 関数fの取る引数がnumber2つであれば、argsの型Tはnumberを二つ取るものとして、3つ以上のパラメータを受け付けない。

下記の様な例でも使えている。

let myFunc3 = (times: number, alphabet: string, last: string) => alphabet.repeat(times) + last
call(myFunc3, 10, 'A', '.')
//call(myFunc3, 10, 'A', 5)
//call(myFunc3, 10, 400, 5)

上記の例では、myFunc3で定義された関数の引数の型をTSコンパイラが正確に判断している。 この時argsの型Tはnumber, string, stringとなるので、それ以外のシグネチャをもつcall関数はビルドエラーとなっている。