概要
TypeScriptを利用している中で、必ずと言っていいほど関数は使います。
というか言語問わず、開発者なら一度は使っているでしょう。
この記事では、TypeScriptにおける関数についてさらっと振り返りたいと思います。
関数とは
JavaScriptでは、関数は第一級のオブジェクトであり、他のオブジェクトと同様に扱うことができます。
つまり、変数に割り当てる、関数に関数を渡したり返してもらう、new
をすればコンストラクタになる、など色々できます。
色々できるということは、TypeScriptでは色々な型システムを使って実現することになります。
TypeScriptで関数を宣言するには
では、TypeScriptを用いた関数の宣言例を下記で見てます。
まずは、TypeScriptの環境でJavaScriptの関数を宣言してみましょう。
※TypeScriptの環境は以前、TypeScriptの基本を振り返るの記事で簡易的にまとめましたのでご参考ください。
function func (arg) { // argは暗黙的にanyになる
return arg
}
上記の例では、いきなりパラメータarg
の下に赤いニョロニョロが出てくると思います。
通常、TypeScriptは特別なケースを除いてパラメータについては型を推論しません。
そのため、パラメータには明示的に型を指定する必要があります。
function func (arg: string) {
return arg
}
上記のように型を明示することで赤いニョロニョロが消えたかと思います。
これが、TypeScriptにおける関数の宣言方法になります。
戻り値について型推論が働くため、明示的に型を指定する必要はありませんが指定することも可能です。
戻り値を明示する場合の宣言方法は以下のようになります。
function func (arg: string): string {
return arg
}
他の関数の宣言方法について
JavaScriptには、先ほどの例のような関数の宣言方法以外にも以下のような方法があります。
// 名前付き関数
function func1 (arg: string) {
return arg
}
// 関数式
const func2 = function(arg: string) {
return arg
}
// アロー関数式
const func3 = (arg: string) => arg
// 関数コンストラクタ
const func4 = new Function('arg', 'return arg')
上記の関数コンストラクタ以外ならTypeScriptは型のサポートを行えます。
関数を呼び出す方法について
JavaScriptには、関数を呼び出す際に()
以外にもcall
,apply
,bind
という方法があります。
これらは、this
を束縛するときとかに使用したりします。
JavaScriptの関数におけるthis
についてはここでは詳しく触れませんが、とても重要なので個人的に分かりやすかった記事をご紹介します。
※ JavaScript の this を理解する多分一番分かりやすい説明
TypeScriptでもcall
,apply
,bind
での呼出しを行うことができます。
前項で紹介した関数funcを例に、()
,call
,apply
,bind
で呼び出してみましょう。
function func (arg: string): string {
return arg
}
func('hoge')
func.call(null, 'hoge')
func.apply(null, ['hoge'])
func.bind(null, 'hoge')()
無事に呼び出すことができています。
ここで注意が必要なのが、TypeScriptでcall
,apply
,bind
を安全に使用するためには、tsconfig.jsonのstrictBindCallApplyをtrueにする必要があります。これをfalseにしていると型チェックが行われません。
// strictBindCallApplyがtrueの場合😍
func.call(null, ['hoge']) // 型 'string[]' の引数を型 'string' のパラメーターに割り当てることはできません😍
func.apply(null, 'hoge') // 型 '"hoge"' の引数を型 '[string]' のパラメーターに割り当てることはできません😍
func.bind(null, ['hoge']) // 型 'string[]' の引数を型 'string' のパラメーターに割り当てることはできません😍
// 赤文字のニョロニョロがそれぞれ現れてくれたからコンパイルする前に気づけた!!!
// strictBindCallApplyがfalseの場合🤮
func.call(null, ['hoge']) // OK (型チェックが走らない...🤮)
func.apply(null, 'hoge') // OK (型チェックが走らない...🤮)
func.bind(null, ['hoge'])() // OK (型チェックが走らない...🤮)
// 気づかずにコンパイルしてしまい、コンパイル後に気づいた...
// $ ./node_modules/.bin/ts-node index.ts
// index.ts:5
// func.apply(null, 'hoge') // 型 '"hoge"' の引数を型 '[string]' のパラメーターに割り当てることはできません
パラメータについて
パラメータとは、関数に渡される名前付きの変数のことを指します。
対して、関数を呼び出すときに関数を渡すデータのことを引数と呼びます。
パラメータの省略方法について
「?」を使用することで、パラメータを省略可能と指定できます。
function func(arg?: string) {
return arg || 'not arg'
}
省略可能なパラメータは、必須のパラメータより先に定義する必要があります。
function func(arg?: string, arg2: string) { // Error: 必須パラメーターを省略可能なパラメーターの後に指定することはできません
return arg || arg2
}
ちなみに、デフォルトパラメータに関しては、省略可能なパラメータより後に指定することが可能です。
デフォルトパラメータは下記のようにパラメータに対してデフォルト値を付与してあげます。
function func(arg?: string, arg2: string = 'not arg2!!') {
return arg || arg2
}
// ちなみに、下記の通りデフォルトパラメータに関しては型付けする必要はないです。
function func2(arg?: string, arg2 = 'not arg2!!') {
return arg || arg2
}
可変長引数を扱う関数について
argumentsとレストパラメータについて
JavaScriptにはarguments
というオブジェクトがあるのをご存じでしょうか?
arguments
とは関数へ渡された引数を含む、関数内のみアクセス可能な**配列のような(Array-like)**オブジェクトの一つです。
可変長引数関数を表現したい時に使われるJavaScriptの手法の一つとして知られています。
下記の例を御覧ください。
function addNumber() {
console.log(arguments)
}
addNumber(1,2,3) // Object { 0: 1, 1: 2, 2: 3 }
上記のように関数呼出時に渡した引数にアクセスすることができます。
例えばパラメータの合算値を出力する関数を表現する場合は、arguments
を配列に変換してから処理を施す必要があります。
function addNumber() {
console.log(Array.from(arguments).reduce((acc, cur) => acc + cur, 0))
}
addNumber(1,2,3)
おそらく気づいた方も多いかと思いますが、これには型が安全ではない問題があります。
上記の関数内で現れた変数acc
とcur
の型がanyに推論されています。
その後、addNumber(1,2,3)
の行で、 個の引数が必要ですが、3 個指定されました。
という型エラーが発生しているかと思われます。
これは、addNumber
は関数が使用されるまでTypeScript的には型チェックが走りません。
つまり、使用されるまで開発者は何だかふわふわした関数をわき目に実装を進めていく必要があります。
TypeScriptで可変長引数の関数を扱う場合は、arguments
ではなくレストパラメータを用いるようにして、任意の数の引数を安全に受け付けるようにしましょう。
function addNumber(...nums: number[]) {
console.log(nums.reduce((acc, cur) => acc + cur, 0))
}
addNumber(1,2,3)
thisの取り扱いについて
※this
の詳しい振る舞いについては、は冒頭のほうでご紹介した記事やMDNをご参照ください。
thisが指すもの
ここでは、JavaScriptにおけるthis
が指すものについて軽く触れたいと思います。
まず、グローバルな実行コンテキスト(関数の外側)でのthis
はグローバルオブジェクトを指します。
console.log(this === window) // true
では、関数内でのthis
はどうでしょうか。
function func() {
return this
}
console.log(func() === window) // true
こちらの例は、グローバルオブジェクトを指しているようですね。
これは、非厳格モードの場合、呼び出し時にthis
の値が設定されないため、規定でグローバルオブジェクトを返すようになっているようです。
では、以下の例はどうでしょうか。
let obj = {
func() { return this }
}
console.log(obj.func() === window) // false
console.log(obj.func() === obj) // true
実行結果からも分かるように、obj
オブジェクトのメソッドとして定義された関数func()
でのthis
はobj
の値を指していますね。
つまり、this
はメソッドの呼び出し時に.
の左側にあるオブジェクトを指すルールになっているように見受けられます。
なので...
let obj = {
func() { return this }
}
let objFunc = obj.func
console.log(objFunc() === window) // true
console.log(objFunc() === obj) // false
上のようにobj.func
メソッドをobjFunc
に再度割り当てた後に、それを呼び出すとthis
が指すものがグローバルオブジェクトになっています。
このように、JavaScriptにおけるthisは、呼び出し方によって変わるのです。
ただし、冒頭のほうでも少し触れましたが、関数の呼出し方にはcall
,apply
,bind
による方法が存在します。
これらはthis
が指すものを好き勝手に設定することが出来るのです。
以下の例をご覧ください。
let obj = {
func() { return this }
}
let objFunc = obj.func
console.log(objFunc.call(obj) === obj) // true
console.log(objFunc.apply(obj) === obj) // true
console.log(objFunc.bind(obj)() === obj) // true
これらは、this
をobj
に指定したことによりthisを束縛しました。
call
は、obj
を関数obj.func()
内のthis
にバインドしています。(上の例では、objにバインドしています。)
apply
もcall
と同様です。(call
との違いは引数の渡し方が異なります。)
bind
もthis
を束縛する点ではcall
やapply
と同様ですが、関数を直接呼び出さずに、新しい関数を返してくれます。
TypeScriptにおけるthisの型付け
ここまでで、なんとなくthis
の振る舞いが分かった気持ちになったかと思います。
ただ、TypeScriptの環境でthis
に型を付ける場合はどうしたら良いでしょうか?
具体的な例を見ていきたいと思います。
function formatDate(this: Date, arg: string) {
console.log(arg, `${this.getFullYear()}-${this.getMonth()+1}-${this.getDate()}`)
}
formatDate.call(new Date(), '今日は')
formatDate.apply(new Date(), ['今日は'])
formatDate.bind(new Date(), '今日は')()
上の例の関数formatDate
は、関数内のthis
を用いた日付のフォーマッターです。
this: Date,
部分に注目してください。
こちらは、関数内で使用されるthis
の期待する型を指定しています。
上例のように、this
に型を割り当てたい場合は、関数の一番最初のパラメータとして宣言することで対応することが可能です。
コールシグネチャとは
さて、TypeScriptの関数において、パラメータの型や戻り値の型、thisの型を述べてきましたが、関数そのものの型はどうでしょうか。
const hoge = function func(arg: string): string {
return arg
}
console.log(typeof hoge) // function
function
型になります。
では、このfunction
型を明示的に宣言したい場合はどうすればよいでしょうか?
上例の場合だと、(arg: string) => string
として表現することができます。
const hoge: (arg: string) => string = function func(arg: string): string {
return arg
}
// 下のような明示的な宣言も可能です。(こちらが完全なコールシグネチャのアノテートで上は省略記法)
const hoge2: { (arg: string) : string } = function func(arg: string): string {
return arg
}
これを、関数呼出可能なオブジェクト型 (コールシグネチャ) と呼ぶようです。
※ ちなみに、「シグネチャ」の意味ですが、関数の名前、引数の数や型、返り値の型等のデータ構造のインターフェースといったところですね。
上の例だとなんだがコードが読みづらいので、コールシグネチャを型エイリアスと結び付けてみます。
type Hoge = (arg: string) => string
// ↓の宣言方法も↑と意味は同じです。↑が省略記法で↓完全呼出記法です。
// type Hoge = { (arg: string): string }
const hoge: Hoge = function (arg: string): string {
return arg
}
console.log(hoge('Hello!!')) // Hello!!
型エイリアスと結びつけることで大分見やすくなりました。
コールシグネチャの使いどころ
では、コールシグネチャはどういったところで使われるのか?
これは、関数を引数として渡すときや、関数を別の関数から渡す場合に型付けが欲しい場合に使用したりします。
例えば、コールバック関数を型定義したい時などですね。
type callbackType = (arg: string) => string
function func(arg: string, callback: callbackType) {
return callback(arg)
}
console.log(func('hello Callback!!', (arg) => arg))
上のような感じですね。
ちなみに、下記のようにinterface
を使った方法でも表現をすることが可能です。
interface callbackType {
(arg: string) : string
};
function func(arg: string, callback: callbackType) {
return callback(arg)
}
console.log(func('hello Callback!!', (arg) => arg))
関数型のオーバーロード
コールシグネチャについて述べてきましたが、複数の呼出シグネチャを持つ関数についてはどのように表現すればよいでしょうか?
例えば、以下のような開発の流れがあったとしましょう。
※関数型のオーバーロードは、省略記法 (アロー関数みたいなやつ) ではなく完全な呼出し記法で表現する必要があります。
- パラメータを2つ持ち、戻り値は渡ってきたパラメータを加算した関数を作りたい。
type Add = {
(x: number, y: number): number
}
const add: Add = (x: number, y: number): number => {
return x + y
}
console.log(add(1,2)) // 3
無事に、良い感じにシグネチャも宣言しつつ、関数の実装が終わりました。しかし...
- 2つめのパラメータは省略可能にしてほしいので構造を作りなおしてください。
このような要望がきた場合どう対応しましょうか。
// シグネチャ
type Add = {
(x: number, y: number): number
(x: number): number
}
// 実装側
const add: Add = (x: number, y?: number): number => {
return x + (y || 0)
}
console.log(add(1,2)) // 3
これが、関数のオーバーロードになります。
関数のコールシグネチャを複数持つような形で宣言することが可能なのです。
実装側の2つ目のパラメータに対してを「?」をつけ忘れたその瞬間に、型エラーが発生してくれるので、**開発者はシグネチャを確認して2つ目のパラメータは省略するのか!!**と気づくことができます。
ジェネリック
最後に、ジェネリックについて紹介したいと思います。
先ほど、関数のオーバーロードでシグネチャが複数持つような形で宣言ができると述べました。
しかし、今まで見てきた例は開発者が事前に型や振る舞いを知っていることを前提で紹介してきました。
では、期待している型が事前に分からない状況や、特定の型に限定したくない場合どうでしょう。
下記のように期待される型を全てを洗い出して宣言するのでしょうか?
type Log = {
(id: string , message: string): void
(id: number , message: string): void
(id: object , message: string): void
}
const log: Log = (id: string|number|object, message:string) => {
if (typeof id === 'string') {
console.log('string', id, message)
} else if (typeof id === 'number') {
console.log('number id:', id, message)
} else if (typeof id === 'object') {
console.log('object', id, message)
}
}
log('1', 'hello') // string 1 hello
log(1, 'hello') // number id: 1 hello
log({ id: 1 }, 'hello') // object { id: 1 } hello
出来れば型を抽象化していきたいですよね。
TypeScriptではそれを表現することが出来ます。
例えば、上のLog
は下記のような表現で抽象化することが出来ます。
type Log = {
<T>(id: T, message: string): void
}
const log: Log = (id, message) => {
if (typeof id === 'string') {
console.log('string', id, message)
} else if (typeof id === 'number') {
console.log('number id:', id, message)
} else if (typeof id === 'object') {
console.log('object', id, message)
}
}
log('1', 'hello')
log(1, 'hello')
log({ id: 1 }, 'hello')
とても綺麗になりました。
ある型を受け取りある型の戻り値を返却する、プレースホルダーの型を**ジェネリック型パラメータ(多相型パラメータ)**と呼んでいます。
ジェネリクスを駆使することで、以下のような、あるコールバック関数を元に数値配列,文字列配列,オブジェクト配列をフィルタリングした配列を返す関数の定義みたいなことを綺麗に表現することができますね!
type ItemFiter = {
<T>( items: T[], func: (n: T) => boolean ): T[]
}
const itemFilter: ItemFiter = (items, func) => {
const result = []
for (let i=0; i<items.length; i++) {
if (func(items[i])) result.push(items[i])
}
return result
}
// 3より低い数値配列を返す
console.log(itemFilter([1,2,3], n => n < 3))
// Scriptという文字を含む文字列配列を返す
console.log(itemFilter(['TypeScript','JavaScript','Ruby'], n => RegExp('Script').test(n)))
// Scriptという文字を含むオブジェクトの配列を返す
console.log(itemFilter([{ lang: 'TypeScript' }, {lang:'JavaScript' }, { lang: 'Ruby' }], n => RegExp('Script').test(n.lang)))
ジェネリクス型はいつ具体的な型に変わるのか
ジェネリクスの型 (<T>
) の置く場所によって具体的な型にいつ変わるか決まります。
先の例では、コールシグネチャの一部としてジェネリクスの型宣言を行いました。( <T>(id: ...
)
そのため、関数を呼び出す時に具体的な型を<T>
に対してバインドしています。 (関数を呼び出す度に引数の型の内容が<T>
が反映されていました。)
では、型エイリアスに対してジェネリクスの型宣言をした場合はどうなるでしょうか?
// コールシグネチャの一部としてのジェネリクスの型宣言
// type Log = {
// <T>(id: T, message: string): void
// 型エイリアスに対してジェネリクスの型宣言
type Log<T> = {
(id: T, message: string): void
}
// const log: Log = <T>(id: T, message:string) => { // 型エラーが発生してしまう (ジェネリック型 'Log' には 1 個の型引数が必要です。)
const log: Log<string|number|object> = (id, message) => {
if (typeof id === 'string') {
console.log('string', id, message)
} else if (typeof id === 'number') {
console.log('number id:', id, message)
} else if (typeof id === 'object') {
console.log('object', id, message)
}
}
log('1', 'hello')
log(1, 'hello')
log({ id: 1 }, 'hello')js:
上記のように、型エイリアスLogを使用する時に具体的な型がバインドされるため、型エラーが発生してしまいました。
つまり、型エイリアスLogに対して明示的に型をバインドするように宣言する必要が発生してしまいます。 (log: Log<string|number|object>
みたいに宣言しないといけないです。)
まとめると、ジェネリクスは関数の場合ならそれを使用する時か呼び出す時にバインドされることを意味します。
/**
* <T>のスコープが個々のシグネチャに限定
* 呼び出すときに<T>に対して型が決まる
*/
type Log = { <T>(id: T, message: string): void }
const log: Log = (id, message) => { console.log(id, message) }
log('1', 'hello')
/**
* ↑の省略版
*/
type Log = <T>(id: T, message: string) => void
const log: Log = (id, message) => { console.log(id, message) }
log('1', 'hello')
/**
* <T>のスコープがシグネチャ全体
* <T>が型エイリアスの一部として宣言されるので、Log型の関数を宣言するときに型が決まる
*/
type Log<T> = { (id: T, message: string): void }
const log: Log<string> = (id, message) => { console.log(id, message) }
log('1', 'hello')
/**
* ↑の省略版
*/
type Log<T> = (id: T, message: string) => void
const log: Log<string> = (id, message) => { console.log(id, message) }
log('1', 'hello')
/**
* 型エイリアス使わない版
* 呼び出すときに<T>に対して型が決まる
*/
function log<T>(id: T, message: string): void { console.log(id, message) }
log('1', 'hello')
ちなみに、複数のジェネリクスを扱いたい場合は以下のような宣言方法になります。
type Log = { <T,U>(id: T, message: U): void }
const log: Log = (id, message) => { console.log(id, message) }
log('1', 'hello')
<T,U>
のように使いたい数の変数を定義してあげれば良いです。
ちなみにTやUの型名ですが、慣例的にT,U,V...
と続けたりA,B,C...
と続けることが多いですが、IdType
など分かりやすい型名でも問題ないらしいです。
extends制約
extends
を使用することでジェネリクスに対して制約を設けることが可能です。
type Log = <T extends number|string , U>(id: T, message: U) => void
const log: Log = (id, message) => { console.log(id, message) }
log('1', 'hello')
log({id: 1}, 'hello') // 制約により型エラー
以上、TypeScriptの関数について振り返ってみた記事でした。