
TSというよりモダンJSは、「関数でデータを変換していく書き方」と非常に相性が良いんです。
状態を変更せず、
入力→処理→出力 を明確にする思想ですね。
この"関数型スタイル"と、「データ型を後から注入する仕組み」の"ジェネリスク(Generics)"という、型の再利用・抽象化ができるスタイルについて学んでみます。
最後に、「関数を返す関数」「関数を受け取る関数」という"高階関数(Higher-Order Functions)"についても説明するので、覚えるとTSマスターに近づきますよ。
関数型スタイル
命令型
let arr = [1,2,3]
for(let i=0; i<arr.length; i++){
arr[i] = arr[i] * 2
}
関数型
const arr = [1,2,3]
const result = arr.map(x => x * 2)
特徴
・副作用を減らす
・型推論が活きる
・関数を組み合わせやすい
関数型×TSが最強になる理由
関数型パターンは入力と出力が明確なので、TSの型システムと相性が良いんです。
const add = (a: number, b: number): number => a + b
関数の境界に型があるので、「意図が伝わる」コードになります。
また、条件分岐や例外処理が減るため、「型がロジックを導く」ことができます。
要するに、コメントいらずの効率が良いコードになるという事ですね。
ジェネリクス
型を固定した関数
この状態では、stringしか使えません。
function wrapString(value: string): string[] {
return [value]
}
ジェネリクス型
関数の”仕様”は同じだけど、扱う型は呼び出す側が決めることができます。
function wrap<T<(value: T): T[] {
return [value]
}
wrap(1) // number[]
wrap("hello") // string[]
wrap({id: 1}) // {id: number}[]
関数型 × Generics=TSらしい書き方
配列をフィルタする関数
function filter<T>(arr: T[], predicate: (value: T) => boolean): T[] {
return arr.filter(predicate)
}
const nums = filter([1,2,3,4], (n) => n > 2) // number[]
const words = filter(["a","bbb","cc"], (w) => w.length > 1) // string[]
解説
・型の制約は関数定義で行う。
・型の選択は使う側で行う。
→ 拡張性・再利用性が高いコードになる。
・multiply(2) を実行すると「新しい関数」が返り、値を受け取る。
→ 関数を返すので"関数を返す関数"
Higher-Order Functions(高階関数)
例:部分適用(Partial Application)
const multiply = (a: number) => (b: number) => a * b
const double = multiply(2)
double(5) // -> 10
ポイント
・関数は値として扱う。
・副作用(Side effect)を最小化することができる。
・型は境界(関数の入出力)に置く。
・処理は小さい関数に分解して、その後に合成する。
・ジェネリクスを使うと、再利用性と柔軟性を上げることができる。
ジェネリクスを使ってより効率化
const identity = <T>(value: T): T => value
ポイント
・ジェネリクスにより、型が呼ぶ側で決まる
identity(10) // -> number型
identity("hello") // -> string型
identity([1,2,3]) // -> number[]型
関数を受け取る関数
const applyTwice = <T>(fn: (v: T) => T, value: T): T => fn(fn(value))
applyTwice(x => x + 1, 5) // -> 7
ポイント
・fn が "関数"
それを引数で受け取るので
→ 関数を受け取る関数(Higher-Order Function)
関数の分類まとめ
普通の関数
(a: number) => a + 1
型 : 値 → 値
入力に対して値を返す単純な関数
関数を受け取る関数(高階関数)
arr.map(fn)
型 : 関数 → 値 or 関数
関数を引数として受け取る
関数を返す関数
multiply(2)(5)
型 : 関数
呼び出すと別の関数が戻る
両方(関数を受け取って関数を返す)
compose(fn1, fn2)
型 : 関数
FPライブラリ系に多い
multiply → 関数を返す関数(部分適用・カリー化)
identity → ジェネリクス付き普通の関数
applyTwice → 関数を受け取る関数(高階関数)
この3つは「見た目が似てても役割が全然違う」という事を理解しよう!
あとがき
高階階層は、モダンJSでも下記のように書けます。
var a = a => b => a*b
a(2)(3)
> 6
これに型宣言と、独特のジェネリクス記述をすると、TSっぽさが出るということですね。
でも、ジェネリクスって「型を呼び出しが決める」という事だけど、
「シンプルに書けるモダンJSと同じじゃね?」と思ったのは自分だけでしょうか?
でも、ジェネリクスは、次のように書くことでより安定したコードが表示前にエラーを見つけることができるようです。
function map<T, R>(arr: T[], fn: (v: T) => R): R[] {
return arr.map(fn);
}
map([1, 2, 3], x => x * 2) // number[] ✔
map(["a","b"], x => x.toUpperCase()) // string[] ✔
map([1,2,3], x => x.toUpperCase()) // compile error ❌ (事前に防げる)
どうやら、このジェネリクスに慣れることが、TSマスターに近づくポイントかもしれませんね。
0 件のコメント:
コメントを投稿