対象
この記事はこんな方たちに読んでいただきたいです。
- TypeScriptの概要は知っている
- TypeScriptにどんな型があるかは知っている
- TypeScriptを使いこなせているとは言えない
TypeScript敗北者ってどんな人?
- エラーが出るのがめんどくさくて tsconfig の
"strict"
オプションを無効化している人 - よくわかんないけどとりあえずエラー出ないように
any
かas
使っとく人 - 型のことは全部AIにお任せしている人
僕もそうですが経験が浅かったり、
責任範囲の広い大規模な開発に参加したことがなかったり、
そのそもTypeScriptをあまり使わない環境で開発していたりすると
こうなってしまう人もいるのではないでしょうか。
ちなみに..自分が敗北者だということに気づいたのは @uhyo さんの記事を見た時です。
TypeScriptに敗北していると...
せっかくTypeScriptを使っている恩恵が受けられないです。
過去編: こうして僕はTypeScript敗北者になった
このセクションは僕が TypeScript 敗北者になった理由を話すだけなので
どうでもいいわ!って人は 本編 まで飛ばしてください。
千里の道も一歩から
TypeScript自体あまり知らなかった僕はまず
TypeScriptの概要・どんな型があるかを サバイバルTypeScript を見て学びました。
サバイバルTypeScript は
TypeScriptの歴史や使う理由、型情報までとてもまとめられているので
最初の一歩目、体系的に学ぶコンテンツとして見て良かったなと思います。
ステップアップ
でも待てよと、
型のことはわかったけど実際ライブラリとかの型を見ても
結局わけがわからんぞと思った僕は
より実践的な型の使い方を学ぶために Type Challenge にチャレンジしました。
そして... 敗北しました。
Type Challenge は課題にチャレンジしながらTypeScriptを学べる…んですけど
正直初級編からTypeScriptを実務でじゃんじゃん使ってるような
人じゃないとわからないレベル。
僕はこれにチャレンジしたことで完全な苦手意識が植え付けられ
これ以上学ぶことは放棄しました。
Type Challenge 中級編の問題に敗北
/*
9 - Deep Readonly
-------
by Anthony Fu (@antfu) #中級 #readonly #object-keys #deep
### 質問
オブジェクトのすべてのパラメーター(およびそのサブオブジェクトを再帰的に)を読み取り専用にする`DeepReadonly<T>`を実装します。
*/
type X = {
x: {
a: 1
b: 'hi'
}
y: 'hey'
}
type Expected = {
readonly x: {
readonly a: 1
readonly b: 'hi'
}
readonly y: 'hey'
}
type Todo = DeepReadonly<X> // should be same as `Expected`
例えば 中級編の問題 『#9 Deep Readonly』
「オブジェクトを含むオブジェクトを再帰的に読み取り専用にする
DeepReadonly<T>
型を実装する」というものです。
解答例がたとえばこちら
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Record<any, any>
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K]
}
なんかの呪文かな?って
思いました。僕は。
本編: さらば、 TypeScript敗北者
長らくTypeScriptに敗北していた僕が
どのように抜け出したか説明していきます。
抜け出し方
TypeScriptも言語。
他の言語を理解していったような形で段階を踏んでいけば怖く無くなるはず!
ということで
「僕なりの」ですが言語の学び方に当てはめて考えました。
言語学習の流れ
「僕なりの」言語学習の流れはこのような感じです。
① 言語の背景を知る > ② 大体の機能を知る > ③ ユースケースから学ぶ
-
① 言語の背景を知る
言語の歴史・生まれた理由を知ることは
その後の学習の質をより良いものにできるでしょう。 -
② 大体の機能を知る
器具を知らずに筋トレはできないです。 -
③ ユースケースから学ぶ
僕の感覚ですが
実践する前に実際にどのように使われているのかを知ることで
かなり理解度が高まります。
① 言語の背景を知る
TSの... | |
---|---|
TSの特徴 | 静的な型付け・型の推論 |
TSの背景 | 大規模化が進んだJavaScriptの開発を強化するため生まれた |
TSの恩恵 | ・コーディング中にコードジャンプやコード補完ができる ・型情報がドキュメントにもなる ・動かす前にソースコードのチェックを行えるようになる |
TypeScriptが生まれた背景、恩恵を知ることで
実際に使う際の考え方の土台になりました。
② 大体の機能を知る
TypeScript最大の特徴である「型」
どんな型があるのかを学びました。
型入門におすすめのコンテンツ
- 公式ドキュメント
- サバイバルTypeScript
- 記事
- 教材
⭐ TypeScript 勝ちパターン
- 当てはまる型を逆算: ジェネリクス, infer
- 型の制約: extends
- 型の分岐: extends, ?(Conditional Type)
- 繰り返し: in(Mapped Types), 再帰
『TypeScript、上達の瞬間』Speaker Deck - sadnessOjisan
そして、TypeScriptの勝ちパターンを知りました。
このパターンに当てはめて考えた結果
呪文に見えていたTypeScriptがわかるようになりました。
本当に感謝です。
③ ユースケースから学ぶ
型の知識と勝ちパターンを持って
ユースケースを学びます。
ユーティリティ型から学ぶ
ユーティリティ型とは..
https://typescriptbook.jp/reference/type-reuse/utility-types
TypeScriptに組み込まれている型から別の型を導き出してくれる便利な型です。
これ自体もTypeScriptで実装されています。
Record<Keys, Type>
型
キーがKeysで、値がTypeであるオブジェクトの型を作るユーティリティ型
使用例
/**
* Record<Keys, Type>
* キーがnumberで値がPerson型のオブジェクトを型付けする
*/
type Person = {
name: string;
age: number;
enrollment: boolean;
address: {
country: string;
city: string;
}
};
type UserData = Record<number, Person>;
const users: UserData = {
1: { name: 'Kishi Rihito', age: 26, enrollment: true },
2: { name: 'Bob', age: 30, enrollment: false },
3: { name: 'Charlie', age: 35, enrollment: true },
};
この組み込み型 Record<Keys, Type>
中身はこのように実装されています。
type Record<K extends keyof any, T> = {
[P in K]: T;
};
これを 勝ちパターン に合わせて考えてみます。
- 当てはまる型を逆算: ジェネリクス, infer
- 型の制約: extends
- 型の分岐: extends, ?(Conditional Type)
- 繰り返し: in(Mapped Types), 再帰
#extendsで制約
ジェネリクスのKがオブジェクトのプロパティ名として
有効な値であることを制約しています。
<K extends keyof any, T>
#in(Mapped Types)で繰り返し
Kの各キーPに対して、値の型Tを持つプロパティを作成しています。
[P in K]: T
こんなふうに分解してユーティリティ型の定義を見ていたら
苦手意識は無くなっていきました。
ライブラリから学ぶ
なぜ?ライブラリから学ぶのがいいのか
ライブラリの開発は...
- チームの垣根を越えて生産性に責任を持たなければいけない
- 柔軟に開発することが求められるので再利用性を高めなければいけない
- そもそもtype challengesにあるような長くて難しい型は大規模な開発やこういったライブラリの開発くらいにならないとあまり使うこともない
スライダーライブラリ Splide のユースケース
Splideイベントハンドラの実装
例えば…Splideのコアファイル内の
イベントハンドラメソッドの実装部分
export class Splide {
/**
* Registers an event handler.
*
* @example
* ```ts
* var splide = new Splide();
*
* // Listens to a single event:
* splide.on( 'move', function() {} );
*
* // Listens to multiple events:
* splide.on( 'move resize', function() {} );
*
* // Appends a namespace:
* splide.on( 'move.myNamespace resize.myNamespace', function() {} );
* ```
*
* @param events - An event name or names separated by spaces. Use a dot(.) to append a namespace.
* @param callback - A callback function.
*
* @return `this`
*/
on<K extends keyof EventMap>( events: K, callback: EventMap[ K ] ): this;
on( events: string | string[], callback: AnyFunction ): this;
on( events: string | string[], callback: AnyFunction ): this {
this.event.on( events, callback );
return this;
}
}
onメソッドはこのようにイベントを登録するために使います。
const splide = new Splide( '.splide__target' ).mount();
splide.on( 'active', () => {
// アクティブスライドが変わったときの処理
} );
EventMap型
それぞれのイベント名をキーに、コールバック関数の型を値にもつインターフェース型。
export interface EventMap {
'mounted': () => void;
'ready': () => void;
'click': ( Slide: SlideComponent, e: MouseEvent ) => void;
'move': ( index: number, prev: number, dest: number ) => void;
'moved': ( index: number, prev: number, dest: number ) => void;
'active': ( Slide: SlideComponent ) => void;
// ...
}
これらを踏まえてイベントハンドラのこの部分をみてみると...
on<K extends keyof EventMap>( events: K, callback: EventMap[ K ] ): this;
- KはEventMapのキーのみを受け付けるように制約
- callback関数の型はEventMap[K]の型
- (
’active’
の時はEVentMap[’active’]
なので( Slide: SlideComponent ) => void
)
- (
というように機能から逆算して制約をつけているのがわかります。
Type Challenge 中級編の問題に勝つ
#中級9 Deep Readonly
/*
9 - Deep Readonly
-------
by Anthony Fu (@antfu) #中級 #readonly #object-keys #deep
### 質問
オブジェクトのすべてのパラメーター(およびそのサブオブジェクトを再帰的に)を読み取り専用にする`DeepReadonly<T>`を実装します。
*/
type X = {
x: {
a: 1
b: 'hi'
}
y: 'hey'
}
type Expected = {
readonly x: {
readonly a: 1
readonly b: 'hi'
}
readonly y: 'hey'
}
type Todo = DeepReadonly<X> // should be same as `Expected`
#解答
type DeepReadonly<T> = {
readonly [K in keyof T]: T[K] extends Record<any, any>
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K]
}
パターンに沿って分解して考える
Tの全てのプロパティに対してreadonly
修飾子をつけています。
ここは組み込み型 Readonly<T>
の実装と同じです。
readonly [K in keyof T]: T[K]
分岐の extendsで値T[K] がオブジェクト( Record<any, any>
)の時を条件分岐してます。
T[K] extends Record<any, any>
Function
型の時は
変更されることがないので readonly
はつけずそのまま返します。
Function
型でないオブジェクトの時はDeepReadonly<T>
を再帰的に適用して
ネストしたオブジェクトにも readonly
がつくようになっている!という感じです。
? T[K] extends Function
? T[K]
: DeepReadonly<T[K]>
: T[K]
呪文と思っていた型も分解して考えたら理解することができました✨
まとめ
- TypeScriptに敗北するとせっかくTypeScriptを使ってる意味がなくなってしまう
- パターンに当てはめればだいたいの型は作れるし理解できる
- ユーティリティ型、ライブラリの実装などを見て理解しよう