5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

TypeScriptで型の導出を利用して楽に型付けする

5
Last updated at Posted at 2019-11-05
Page 1 of 27

*こちらは Opt Technologiesで開催したグループ会社合同テックイベント「Opt Group Tech Day」 の発表資料になります


自己紹介

株式会社オプト シニアエンジニア @sisisin(しめにゃん)

  • GitHub
  • Twitter
  • フロントエンドの人だけどスクラムマスター・インフラ・サーバーサイドといろいろやります
  • 今は 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,
  }),
);

playground


その二重管理、TypeScriptなら型の導出によって解決出来ます!


先に結論だけ

const devices = ['pc', 'sp', 'tablet'] as const;
type Device = typeof devices[number];
// => Device is 'pc' | 'sp' | 'tablet'

playground


先に結論だけ

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; }

playground


解説


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などの解説記事を読んでみると良いと思います


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!

上記の例では、 FooArraystring[] 型なので、 FooArray extends string[] は真となり、 FooItem? string : never の左辺の string を指します

これが例えば type FooArray = number[]; だったら、 FooItemnever 型になります


Conditional Typesについて

さらに Conditional TypesType inference という機能を利用すると、型の中から型を取得できます(!?)
例えばGenericsで指定された型 Array<T>Tの部分を型としてあとから取得ができます
具体例を見ていきましょう


Conditional Typesについて

先程の例だと、 FooArraystring[] のときに 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> という部分が、 「 FooArrayArray<T> に所属する型なら真」という条件を表します
    • この条件が真のとき、 Array<infer T> という infer キーワードを利用した記述によって、 Array<T>T の型を利用できるようになります
    • よって、 FooArray extends Array<infer T> ? T : never は、「 FooArrayArray<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や参考記事はこちら


その他参考

TypeScriptの組み込み型にも今回紹介した Index TypesConditional Types を利用した型があります
参考になると思うので、これらの実装も眺めてみると良いかもしれません

  • ReturnType
    • Type inference を利用して任意の関数の返り値の型を導出する型
  • Pick
    • Index Types , Mapped Types (今回は紹介してないですが・・・)を利用して、任意のオブジェクトから特定のフィールドだけを抽出した新しいオブジェクトの型を導出する型
  • Omit
    • こちらは任意のオブジェクトから特定のフィールドを除いた型を導出する型
    • PickExclude を組み合わせて使っているので、読み解くのは難しいかもしれませんが、こういう型の表現もあります、ということで紹介しました

最後に改めて

今日覚えて帰ってほしいこと: コード上に書いてあることは概ね型の導出が可能なので、型の二重管理がしんどいと思ったときは「もしかして導出できるかもしれない」という疑いを持ってみよう

=> 今回紹介した機能をフル活用出来なくても、「こういうこと(型の導出)ができる」ということを知っておくことで、もしかしたら今のコードベースを改善できるかもしれない?もっとTypeScriptの開発体験をよく出来るかもしれない?と疑問を持てるようになってもらえたら幸いです


ということでTypeScriptで型の導出を利用した楽する型付けのためのTipsでした
なるべくこういった導出のテクニックを利用して、楽して型を付けてあげて良い開発をしていきましょう


Happy Hacking!

5
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
5
2

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?