*こちらは Opt Technologiesで開催したグループ会社合同テックイベント「Opt Group Tech Day」 の発表資料になります
自己紹介
株式会社オプト シニアエンジニア @sisisin(しめにゃん)
- GitHub
- フロントエンドの人だけどスクラムマスター・インフラ・サーバーサイドといろいろやります
- 今は AWS/Rails/React なプロダクトのテックリードやりつつ社内アジャイル相談窓口とか社内フロント講師(?)やってます
TypeScriptで型の導出を利用して楽に型付けする
今日覚えて帰ってほしいこと: コード上に書いてあることは概ね型の導出が可能なので、型の二重管理がしんどいと思ったときは「もしかして導出できるかもしれない」という疑いを持ってみよう
=> 今回紹介した機能をフル活用出来なくても、「こういうこと(型の導出)ができる」ということを知っておくことで、もしかしたら今のコードベースを改善できるかもしれない?もっとTypeScriptの開発体験をよく出来るかもしれない?と疑問を持てるようになってもらえたら幸いです
例えばこういうコード書いたことありませんか?
対応デバイスの集合を定義するときに型と値を両方書いた
type Device = 'pc' | 'sp' | 'tablet';
const devices = ['pc', 'sp', 'tablet'];
例えばこういうコード書いたことありませんか?
Storeの持つStateの型を自分で作った
type AppState = {
x: XState;
y: YState;
};
const store = createStore<AppState, any, {}, {}>(
combineReducers({
x: xReducer,
y: yReducer,
}),
);
その二重管理、TypeScriptなら型の導出によって解決出来ます!
先に結論だけ
const devices = ['pc', 'sp', 'tablet'] as const;
type Device = typeof devices[number];
// => Device is 'pc' | 'sp' | 'tablet'
先に結論だけ
const store = createStore(
combineReducers({
x: xReducer,
y: yReducer,
}),
);
export type AppState = typeof store extends Store<infer S, any> ? S : never
// => AppState is { x: XState; y: YState; }
解説
Deviceの例
const devices = ['pc', 'sp', 'tablet'] as const;
type Device = typeof devices[number];
// => Device is 'pc' | 'sp' | 'tablet'
1行目について
-
const devices = ['pc', 'sp', 'tablet']という配列の定義の最後に記述されているas constというキーワードに注目- この
as constというキーワードで変数devicesはTuple型として推論されます - つまり、
string[]型ではなく、['pc', 'sp', 'tablet']という型になります
- この
Deviceの例
const devices = ['pc', 'sp', 'tablet'] as const;
type Device = typeof devices[number];
// => Device is 'pc' | 'sp' | 'tablet'
2行目について
-
type Device =は単にType Aliasの定義 -
typeof devices[number];のtypeof devices部分で、device変数の型を取得し、[number]という添字演算子にてTupleの要素一覧を取得しています-
['pc', 'sp', 'tablet'][0]は'pc'型 -
typeof device[0]は同様に'pc'型 -
typeof device[number]はtypeof deviceの全ての要素のうちのいずれかを指すので、'pc' | 'sp' | 'tablet'型 - という流れ
-
Deviceの例
この例ではTypeScriptの Union Types, String Literal Types, Index types , const assertions という機能を利用しています
詳しくは公式docやQiitaなどの解説記事を読んでみると良いと思います
- 公式doc:
- 解説記事:
Storeの例
const store = createStore(
combineReducers({
x: xReducer,
y: yReducer,
}),
);
export type AppState = typeof store extends Store<infer S, any> ? S : never
1行目について
- これは単なる
store変数をcreateStoreという関数を使って定義してるだけです -
store変数はReduxのStore<S, A>という型が割り当てられており、このうちSがStateの型になります -
createStoreは 型パラメータを4つ取る関数なのですが、ここでは指定せずにTypeScriptの推論に任せているのがポイントです
7行目について
const store = createStore(
combineReducers({
x: xReducer,
y: yReducer,
}),
);
export type AppState = typeof store extends Store<infer S, any> ? S : never
-
export type AppState =はDeviceのとき同様 Type Aliasの定義 -
typeof store extends Store<infer S, any> ? S : neverのうち、-
typeof storeはこれまたDeviceのとき同様にstore変数の型を取得しています -
extends Store<infer S, any> ? S : neverという部分は「extendsの左辺で指定した型がStore<S, any>型に所属する型であれば、Sの型を得る、そうでなければnever型を得る」 というConditional Typesという機能を利用した呪文です
-
Conditional Typesについて
ちょっとややこしいのでもっと簡単な例で説明します
Conditional Typesとは、 T extends U ? X : Y という記法で表現できる、特定の条件のときに型を分岐する事ができるという仕組みです
-
T extends U ? X : Yは、日本語に書き下すと「TがUに所属する型ならX型を、そうでなければY型を指す」となります - 型の三項演算子だと思ってもらえれば良くて、
T extends Uの部分が条件式,? X : Yがよく利用される三項演算子と同じような振る舞いをします
Conditional Typesについて
type FooArray = string[];
type FooItem = FooArray extends string[] ? string : never;
// => FooItem's type is string!
上記の例では、 FooArray は string[] 型なので、 FooArray extends string[] は真となり、 FooItem は ? string : never の左辺の string を指します
これが例えば type FooArray = number[]; だったら、 FooItem は never 型になります
Conditional Typesについて
さらに Conditional Types の Type inference という機能を利用すると、型の中から型を取得できます(!?)
例えばGenericsで指定された型 Array<T> の Tの部分を型としてあとから取得ができます
具体例を見ていきましょう
Conditional Typesについて
先程の例だと、 FooArray は string[] のときに string を得る、と明示的に書きました
type FooArray = ['foo', 'bar'];
type FooItem = FooArray extends string[] ? string : never;
// => FooItem's type is string!
が、 Type inference を利用すればこれを任意の配列型の要素を取得する、という形に出来ます
↓
type FooArray = string[];
type FooItem = FooArray extends Array<infer T> ? T : never;
Conditional Typesについて
type FooArray = string[];
type FooItem = FooArray extends Array<infer T> ? T : never;
-
FooArray extends Array<infer T> ? T : never;について、-
FooArray extends Array<infer T>という部分が、 「FooArrayがArray<T>に所属する型なら真」という条件を表します - この条件が真のとき、
Array<infer T>というinferキーワードを利用した記述によって、Array<T>のTの型を利用できるようになります - よって、
FooArray extends Array<infer T> ? T : neverは、「FooArrayがArray<T>に所属する型の場合、Tの型を得る。そうでなければnever型を得る」ということになります
-
今一度Storeの例
export type AppState = typeof store extends Store<infer S, any> ? S : never
-
typeof store extends Store<infer S, any> ? S : neverのうち、-
extends Store<infer S, any> ? S : neverという部分は 「extendsの左辺で指定した型がStore<S, any>型に所属する型であれば、Sの型を得る、そうでなければnever型を得る」という記述です - これによって、 Reduxの
createStore関数によって得たStore<S, A>型の変数から、S型を導出することによってStateの型を得ることに成功しています
-
Storeの例
Storeの例ではTypeScriptの Conditional Types という機能を利用しています
今回の解説だけではわからないことも多いと思うので、是非以下の参考文献も覗いてみてください
docや参考記事はこちら
- 公式doc:
- 解説記事:
その他参考
TypeScriptの組み込み型にも今回紹介した Index Types や Conditional Types を利用した型があります
参考になると思うので、これらの実装も眺めてみると良いかもしれません
-
ReturnType
-
Type inferenceを利用して任意の関数の返り値の型を導出する型
-
-
Pick
-
Index Types,Mapped Types(今回は紹介してないですが・・・)を利用して、任意のオブジェクトから特定のフィールドだけを抽出した新しいオブジェクトの型を導出する型
-
-
Omit
- こちらは任意のオブジェクトから特定のフィールドを除いた型を導出する型
-
PickとExcludeを組み合わせて使っているので、読み解くのは難しいかもしれませんが、こういう型の表現もあります、ということで紹介しました
最後に改めて
今日覚えて帰ってほしいこと: コード上に書いてあることは概ね型の導出が可能なので、型の二重管理がしんどいと思ったときは「もしかして導出できるかもしれない」という疑いを持ってみよう
=> 今回紹介した機能をフル活用出来なくても、「こういうこと(型の導出)ができる」ということを知っておくことで、もしかしたら今のコードベースを改善できるかもしれない?もっとTypeScriptの開発体験をよく出来るかもしれない?と疑問を持てるようになってもらえたら幸いです
ということでTypeScriptで型の導出を利用した楽する型付けのためのTipsでした
なるべくこういった導出のテクニックを利用して、楽して型を付けてあげて良い開発をしていきましょう