Slide 1

Slide 1 text

Real World and Type Puzzle Code Generation @yukukotani 2024/05/11 - TSKaigi 2024

Slide 2

Slide 2 text

小谷 優空 - @yukukotani ・Software Engineer @ Ubie, Inc. (2019/05~)   ・Lead Architect ・Maintainer of Kuma UI ・Student @ Univ. Tsukuba (2019/04~)   ・情報科学類 自己紹介

Slide 3

Slide 3 text

趣旨 型安全なコードを吐くコードジェネレータを作る野暮用があった(あるある) → 型安全 + コードジェネレータ = → 観察してエッセンスを抽出しよう! Prisma

Slide 4

Slide 4 text

観察するスキーマ このスキーマから生成されるコードを観察する User と Post がリレーションを持つ簡単なやつ

Slide 5

Slide 5 text

型パズルでselectした値に型をつける

Slide 6

Slide 6 text

No content

Slide 7

Slide 7 text

No content

Slide 8

Slide 8 text

簡略化した実装を観察する これが動くPrismaClient型(findFirst関数)の実装 スキーマに即した型がつく TS Playground

Slide 9

Slide 9 text

PrismaClient型 実際はクラスとして実装されているが、ここではシンプルなオブジェクト型に

Slide 10

Slide 10 text

入力の型を得る Type Argument Inferenceによって 型引数Tが推論される

Slide 11

Slide 11 text

入力の型を得る 型引数の制約は入力時の補完・エラー用。 GetSelectIncludeResultの導出には 条件型を使うので無関係

Slide 12

Slide 12 text

入力の型を得る 型引数の制約は入力時の補完・エラー用。 GetSelectIncludeResultの導出には 条件型を使うので無関係

Slide 13

Slide 13 text

出力の型を導出する 続いて出力の型を組成する要素を見ていく

Slide 14

Slide 14 text

$XxxPayload型 スキーマのモデルと対応して生成される カラムの型や他モデルとのリレーションを表現 → select結果の型を決定するための  マスタとして機能 schema.prisma 生成される型

Slide 15

Slide 15 text

出力の型を導出する 第2型引数には入力の型をそのまま渡す

Slide 16

Slide 16 text

出力の型を導出する いよいよ出力の型そのものを見ていく

Slide 17

Slide 17 text

GetSelectIncludeResult

事前生成のPayload型(P)とユーザーが渡す引数(A)から、select結果の型を導出

Slide 18

Slide 18 text

GetSelectIncludeResult

条件型 `T extends { foo: infer S } ? S : never`パターンで、selectした中身を取る

Slide 19

Slide 19 text

GetSelectIncludeResult

Mapped Types で型を組み立てる

Slide 20

Slide 20 text

GetSelectIncludeResult

selectしたキーを Mapped Types のキーとする

Slide 21

Slide 21 text

GetSelectIncludeResult

キーに対応する値がfalse等の場合はneverにキャストして除外 Mapped Typesのキーを neverにすると丸ごと消える

Slide 22

Slide 22 text

GetSelectIncludeResult

Mapped Typesの値側を見ていく

Slide 23

Slide 23 text

GetSelectIncludeResult

Payload型を参照して、selectする値の型を取る

Slide 24

Slide 24 text

おさらい:$XxxPayload型 select結果の型を決定するためのマスタ schema.prisma 生成される型

Slide 25

Slide 25 text

GetSelectIncludeResult

scalarsではなくobjectsの場合も同様に取り、再帰的にselect結果を導出

Slide 26

Slide 26 text

GetSelectIncludeResult

条件型に当てはまらなければneverに落とすが、最初の型制約で先に落ちるはず

Slide 27

Slide 27 text

まとめ:型パズルのエッセンス 型パズルを要素分解すると組み立てやすq Uf 型引数の推論によって入力の型を得‘ Df 型引数の制約を につけ‘ E インターナルな型引数は補完不要なので頑張らなくて良q Bf 入力の型を条件型で絞り込みながら出力の型を組み立て‘ E 事前生成したマスタ型(Payload)を参照してシンプル化 入力時の補完・エラーのため

Slide 28

Slide 28 text

型パズルをできるだけ頑張らない

Slide 29

Slide 29 text

なるべく事前生成して型計算を避ける Payload型から型パズルで導出できそうだが 重複を許容してすべて静的にコード生成する

Slide 30

Slide 30 text

なるべく事前生成して型計算を避ける モデルの一般化もせず重複生成

Slide 31

Slide 31 text

なるべく事前生成して型計算を避ける 事前生成ファイルは基本編集しないので、パースコストの影響が限定的 → ファイルサイズが大きくなってもOK 型チェックはfindFirstとかを触るたびに発生する(tsc内部でキャッシュは効くが) → パフォーマンスがDXに直撃する さらにビルド時間でもパースに比べて型チェックのほうが影響が大きい prisma/prisma の tsc trace パース時間 型チェック時間

Slide 32

Slide 32 text

なるべく事前生成して型計算を避ける リアルタイムな入力に対してフィードバックしたい部分に絞って型計算をする その他はできるだけ静的にコード生成する

Slide 33

Slide 33 text

コード生成もできるだけ頑張らない

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

No content

Slide 36

Slide 36 text

コード生成するコードは基本的につらい テンプレートリテラルなりASTビルダなりで組み立てるので見通しは最悪 なるべく生成するパターンを減らしたい

Slide 37

Slide 37 text

ライブラリとして切り出す 生成するコードのうち静的な部分はライブラリに切り出して、 生成コードからは参照するだけ 生成したコード

Slide 38

Slide 38 text

ライブラリとして切り出す Tips: ライブラリ内で使っているinternalな型もすべてexportする exportしていないと、この型を参照する他パッケージの.d.tsに インライン化されて爆発する Ref: https://github.com/prisma/prisma/blob/main
 /packages/client/src/runtime/core/types/exported/README.md

Slide 39

Slide 39 text

型以外は生成しない 生成された .d.ts 3500行に対して .js は200行程度 コード生成で頑張るのではなく Proxy を使った動的操作をしている

Slide 40

Slide 40 text

まとめ H リアルタイムな入力にフィードバックしたい部分だけは型パズルを頑張5 H 型パズルを要素分解して順番に考えるとやりやすF H それ以外はコードの重複を許容して事前生成に振り切5 H 完全に静的な型は生成すらせずライブラリÆ H 型以外のランタイム処理はProxyで頑張る

Slide 41

Slide 41 text

Thanks! Ubieのブースにいます!