こんにちは
株式会社HRBrain でフロントエンドエンジニアをしているみつです。
TypeScript への理解を少しずつ深めているつもりなのですが、コードジャンプを続けてると、大体 Generics が出てきて「うーん。」となることが多い気がする最近。
あとは、せっかく TypeScript を書いてるのに JavaScript 的にしか使えてないのも寂しいなという気持ちから記事を書きながら理解を深めることにしてみました。
目次
- 目次
- Generics とは
- extends とは
- TypeScript 解読アシスタントって知ってる!?
- TypeScript Deep Dive から Generics を学ぶ
- React.useState の定義にコードジャンプ。
- React.useCallback の定義にコードジャンプ。
- 簡単な Generics を書いてみる。
- まとめ
- PR
Generics とは
- Generics は、型の安全性を保ちつつ、複数のデータ型を扱うことができる。
型の安全性とコードの共通化の両立は難しいものです。
あらゆる型で同じコードを使おうとすると、型の安全性が犠牲になります。
逆に、型の安全性を重視しようとすると、同じようなコードを量産する必要が出てコードの共通化が達成しづらくなります。
こうした問題を解決するために導入された言語機能がジェネリクスです。
ジェネリクスを用いると、型の安全性とコードの共通化を両立することができます。
- 型を引数のような扱いをし、
T
,U
,V
などを慣習的に使うことが普通みたいです。
単純なジェネリックを使うときは
T
、U
、V
を使うのが普通です。
複数のジェネリクス引数がある場合は、意味のある名前を使用してください。
例えばTKey
とTValue
です(一般に T を接頭辞として使用する規約は、他の言語(例えば C++)ではテンプレートと呼ばれることもあります)。
extends とは
-
extends
キーワードを使うと、継承元のプロパティをすべて引き継いで、新しくインタフェースを定義できる。
TypeScript では、
extends
キーワードを利用して定義済みのインターフェースを継承して、新たにインターフェースを定義することができます。
インターフェースを継承した場合、継承元のプロパティの型情報はすべて引き継がれます。
新しくプロパティを追加することもできますし、すでに宣言されているプロパティの型を部分型に指定しなおすこともできます。
TypeScript 解読アシスタントって知ってる!?
TypeScript 解読アシスタントは、コードリーディングを支援するツールみたい。
構文についての説明や、そもそも読み方が分からないを解決するらしい。
便利・・・。先輩エンジニアに大、大、大感謝です。
めっちゃ便利だし・・・Generics(ジェネリクス)っていうフレーズを知る前に知りたかった。
「右かっこ」とか「左かっこ」とかで検索して、読み方から分かってなかったし・・・・。
ツール使い方
- 調べたい関数や書き方にカーソルを合わせる
- 右側に出てくるのを見る
だけ。めっちゃ簡単。
画像の例だと、T は、Generics(ジェネリクス)という型変数であること等が分かります。
TypeScript Deep Dive から Generics を学ぶ
なんとなく型の両立について分かりやすい気がした例。個人的に。
T という共通した何かを受け取って、T という共通した何かを返すということを表現してる。
-
var sample = [1, 2, 3];
で number 型の配列を定義。 - そして、
reverse
関数を通ることで、var reversed
には number 型である制約がつく。 - 次に、
var reversed
に number 型以外のものを入れようとしたらエラーが発生する。
ということ。
共通化しつつ型安全性を保っている気がする!!
実際にコードを見ても、「型'string'を型'number'に割り当てることはできません」というエラーが出る。
function reverse<T>(items: T[]): T[] {
var toreturn = [];
for (let i = items.length - 1; i >= 0; i--) {
toreturn.push(items[i]);
}
return toreturn;
}
var sample = [1, 2, 3];
var reversed = reverse(sample);
console.log(reversed); // 3,2,1
// Safety!
reversed[0] = "1"; // Error!
reversed = ["1", "2"]; // Error!
reversed[0] = 1; // Okay
reversed = [1, 2]; // Okay
(そもそもリバース関数内の処理が通るかどうかは無視した上で)
逆に、var sample = ["1", "2", "3"];
として string 型を定義すると、number は型としては入れることができないエラーが返ってくる。
T という何かしらの共通の型を使用しているということがなんとなく理解できたかも。
function reverse<T>(items: T[]): T[] {
var toreturn = [];
for (let i = items.length - 1; i >= 0; i--) {
toreturn.push(items[i]);
}
return toreturn;
}
var sample = ["1", "2", "3"];
var reversed = reverse(sample);
// 型 'number' を型 'string' に割り当てることはできません。というエラーが出る。
reversed = [1];
React.useState の定義にコードジャンプ。
/**
* Returns a stateful value, and a function to update it.
*
* @version 16.8.0
* @see https://react.dev/reference/react/useState
*/
function useState<S>(
initialState: S | (() => S)
): [S, Dispatch<SetStateAction<S>>];
Returns a stateful value, and a function to update it.
stateful な値と、それを更新する関数を返す。
うんうん。納得納得。
Generics は、ChatGPT に質問
「ここの Generics は、何を表している??」
このコードは、React の useState フックの型定義を表しています。
この型は、2 つの要素からなるタプルを返す関数を表しています。
最初の要素は状態の現在の値で、S 型で表されています。
2 番目の要素は状態を更新するための関数で、Dispatch<SetStateAction<S>>
型で表されています。
例えば、useState<number>(0)
と呼び出すと、状態の初期値として number 型が、更新関数の型としてDispatch<SetStateAction<number>>
が得られます。
useState
は、2 つの要素からなるタプルを返す関数を表現している。
useState
に 0 という number 型を渡すと、初期値として S という number 型かつ、更新用の関数で number 型に制約のある関数が返ってくる。
初期に渡すものが string 型であれば、string 型を表す S であり、更新用の関数でも string 型を許容する関数ができる。
初期値に渡した値の型次第で、更新関数が更新できる値にも型の制約がつくってことね。
React.useCallback の定義にコードジャンプ。
type DependencyList = readonly unknown[];
// I made 'inputs' required here and in useMemo as there's no point to memoizing without the memoization key
// useCallback(X) is identical to just using X, useMemo(() => Y) is identical to just using Y.
/**
* `useCallback` will return a memoized version of the callback that only changes if one of the `inputs`
* has changed.
*
* @version 16.8.0
* @see https://react.dev/reference/react/useCallback
*/
// A specific function type would not trigger implicit any.
// See https://github.com/DefinitelyTyped/DefinitelyTyped/issues/52873#issuecomment-845806435 for a comparison between `Function` and more specific types.
// eslint-disable-next-line @typescript-eslint/ban-types
function useCallback<T extends Function>(callback: T, deps: DependencyList): T;
useCallback
will return a memoized version of the callback that only changes if one of theinputs
has changed.
useCallback は、inputs のいずれかが変更された場合にのみ変更されるメモ化されたバージョンのコールバックを返す。
そうだよね。そんな感じそんな感じ。
Generics は、ChatGPT に質問
「ここの Generics は、何を表している??」
useCallback
は、渡されたコールバック関数をメモ化して、deps(依存性)が変更された場合のみ新しいコールバックを生成します。
T extends Function
これは Generics(ジェネリクス)を使って、callback が何かしらの関数の型であることを指定しています。
callback
とdeps
という 2 つの引数を受け取り、callback
はメモ化される関数で、deps はこの関数が依存する値(依存性)のリストです。
戻り値の型は、T 型で、これは Generics によって指定される関数の型と同じなので、useCallback はメモ化されたコールバック関数を返します。
依存性の変更がある場合にのみ新しい関数が作成され、これによって無駄な再計算が抑制されます。
T を extends して、まずは関数であるという制約をつける。
callback 関数と依存している deps を受け取って、callback を返す。
そしてその戻り値は、受け取った T と同じ関数の型である T が戻ることを表現。
ある型に制約のついた関数が入って、同じ型に制約のついた関数が戻っていくことを表現できてる。なるほど。
簡単な Generics を書いてみる。
なんか実際書くとなるとなんだか難しくて、全然複雑な関数が書けなかったのは少しもやもや。笑
でも、string 型と number 型に絞った値を受け取って、受け取った型に応じてその型の値を返すということをなんとか表現することはできた。
const test = <T extends string | number>(testArgs: T) => {
return testArgs;
};
const test1 = test<string>("");
const test2 = test<number>(1);
// output: '', 1
console.log(test1, test2);
まとめ
Generics の理解を深めようと思った時に、extends だったり周辺知識が求められてなかなか難しかったですが・・・
- Generics は、型の共通化をしながら一定の制約をつけるもの。
- extends を使用することで、元のインターフェースを引き継いで、新しいインターフェースを定義できる。
- 分からない時は、TypeScript 解読アシスタントなどのツールも使ってみたり!!
- useState などは、Generics を用いて定義されていて、一定の制約がついた関数である。
という学び
今は、なんとなく分かったけど、使えそうで使えない状態。
でも今後、コードジャンプしても少し免疫がついた状態でコードリーディングできそうだなという感じです。
Generics 表現を積極的に使いながら、自由に扱えるようになると良いなぁと思います。
おわり。
PR
HRBrain では一緒に働く仲間を募集しています!
一緒に高みを目指しましょ〜!