前回のおさらい
Typescriptの関数定義は、以下のようなパターンがあります。
//一般的な関数定義
function greet(name: string): void {
console.log(`hello, ${name}`)
}
// 関数式
let greet2 = function(name: string): void {
console.log(`hello, ${name}`)
}
// アロー関数
let greet3 = (name: string) => {
console.log(`hello, ${name}`)
}
// アロー関数の省略記法
let greet4 = (name: string) => console.log(`hello, ${name}`)
迷ったらアロー関数で定義しておくと一旦はよいです。 this
をきっちり利用する関数を使いたい場合には function
を使いましょう。
関数定義のおさらい
以下のような関数 filter
を考えます。
- 第一引数に、配列を取る
- 第二引数に、その配列に対するフィルタ関数を取る
上記の条件で関数の型を考えてみましょう。
//アロー関数定義
let filter: (
source: unknown[], //引数① もとの配列
predicate: (input: unknown) => boolean //引数② フィルタリング用の関数
) => unknown[] //引数③ リターン
ただし、これだと型の束縛がないので、例えばこのような関数も代入できてしまいます。
filter = (
src: unknown[],
pred: (input: string) => boolean
): number[] => { //return と pred の型が一致していない
return []
}
もっと型を束縛すればいいのですが、そうすると汎用性がなくなります。
let filter2: (
source: number[], //引数① もとの配列
predicate: (input: number) => boolean //引数② フィルタリング用の関数
) => number[] //引数③ リターン
前回話した関数のオーバーロードにて、複数の型の定義はできますが冗長です。
// 関数のオーバーロード
type Filter = {
(src: number[], pred: (input: number) => boolean): number[]
(src: string[], pred: (input: string) => boolean): string[]
(src: object[], pred: (input: object) => boolean): object[]
}
これらの冗長な定義を解決するのが、多くの言語で採用されているジェネリクスです。
ジェネリクス
ジェネリクスとは、一部の型を後から決められるような型のことです。
Filter関数は、ジェネリクスを用いて以下のように定義できます。
type filter3<T> = (
source: T[], //引数① もとの配列
predicate: (input: T) => boolean //引数② フィルタリング用の関数
) => T[] //引数③ リターン
上記における T
を 「型パラメータ」と呼びます。
ジェネリクスの利用
ジェネリクスを利用するには、型を以下のように束縛する必要があります。
let filter4: filter3<number> = (src, pred) => {
return src.filter(v => pred(v))
}
ただし、型が推論できる場合には自動で型束縛を行うこともできます。
function filter<T>(src: T[], func: (input: T) => boolean):T[] {
return [] //仮実装
}
filter([1,2,3,4,5], (_) => true)
ジェネリクスの定義方法
以下のように、様々な形での定義が可能です。
type Filter1<T> = {
(src: T[], pred: (input: T) => boolean) : T[]
}
type Filter2 = {
<T>(src: T[], pred: (input: T) => boolean) : T[]
}
//多分これが一番一般的
type Filter3<T> = (src: T[], pred: (input: T) => boolean) => T[]
type Filter4 = <T>(src: T[], pred: (input: T) => boolean) => T[]
function filter5<T>(src: T[], pred: (input: T) => boolean): T[]{
return []
}
デフォルト引数のように、型パラメータの初期値を設定することもできます。
type FilterWithDefault<T = number> = (src: T[], pred: (input: T) => boolean) => T[]
複数の型パラメータを持つジェネリクス
ジェネリクスには、複数の型パラメータをもたせることもできます。
例えば、以下のような関数 map
を考えてみましょう。
- ソースとなる配列を第一引数に、配列の要素を変換する関数を第二引数に取る
- 変換された結果を戻り値とする
この場合、ソースの配列の型だけでなく、戻り値の型も指定をする必要があります。
type Map<T,U> = (src: T[], func:(input:T) => U) => U[]
ジェネリック型エイリアス
ここまで関数を中心に見てきましたが、関数以外の型に対してもジェネリック型の定義はできます。
たとえば、複数のFilter
を管理する FilterType
という型を考えてみましょう。
type Filter<T> = (src: T[], pred: (input: T) => boolean) => T[]
type FilterPipe<T> = {
filters: Filter<T>[]
apply: (src: T[]) => T
}
制限付きのジェネリクス型
ジェネリクスを利用したいときでも、あらゆる値が入るのではなく、特定の型に制限したい場合があります。
例えば、これまで扱ってきた Filter
について、文字列と数値にしか適用したくない、という形になった場合、以下のような書き方で制限することができます。
type LimitedFilter<T extends string | number> = (src: T[], pred: (input: T) => boolean) => T[]
type stringFilter = LimitedFilter<string>
type objectFilter = LimitedFilter<Object> //Type 'Object' is not assignable to type 'number'.(2344)
練習問題
プログラミングTypescriptより
型安全なアサーション関数、isを実装してください。型で概略を記述することから始めます。
これは、完成したら、次のように使えるものです。
// stringとstringを比較します is('string', 'otherstring') // false // booleanとbooleanを比較します is(true, false) // false // numberとnumberを比較します is(42, 42) // true // 異なる型同士を比較すると、コンパイル時エラーになります is(10, 'foo') // エラー TS2345: 型 '"foo"' の引数を型 'number' の // パラメーターに割り当てることはできません。
---
## 回答例
```ts
function is<T>(left: T, right: T):boolean {
return left === right
}
演習問題
練習問題のコードを改良して、任意の数の引数を渡せるようにしてください。
is(1, 2, 3) //false
is("type", "type", "type", "type") //true
is("type", 123, {}) //Error