Slide 1

Slide 1 text

© teamLab Inc. 型プラグインシステムの 実装に使われるテクニック TSKaigi 2026 elecdeer チームラボ パッケージチーム 所属

Slide 2

Slide 2 text

「型プラグインシステム」 2

Slide 3

Slide 3 text

「型プラグインシステム」 ライブラリユーザのコードによって、 ライブラリが提供するAPIの型が 後から拡張される仕組み 3

Slide 4

Slide 4 text

ここや 4 const app = new Hono(); app .use("*", authMiddleware) .get("/me", (c) => { const user = c.var.user; // ^?: User return c.json({ id: user.id, name: user.name, }); }); Hono

Slide 5

Slide 5 text

ここに型が付く 5 TanStack Router

Slide 6

Slide 6 text

型プラグインシステム の実装を紹介します 6

Slide 7

Slide 7 text

© teamLab Inc. 所属| 7 自己紹介 7 パッケージチーム フロントエンド班 elecdeer ReactとTypeScriptのファン @elecdeerdev

Slide 8

Slide 8 text

© teamLab Inc. 幾つかパターンがありそう - Root Generic Injection 最初にrootへ型を渡す - Middleware Type Refinement middleware適用の順番で型を順に拡張 - Module Augmentation Registration モジュールの型空間に登録して、離れた場所から参照する ※自分(とAI)が付けた用語なのでコンセンサスのある名称ではないです 8

Slide 9

Slide 9 text

© teamLab Inc. Root Generic Injection 最初にrootへ型を渡す 9

Slide 10

Slide 10 text

© teamLab Inc. Root Generic Injection – Honoの例 10 new Hono()のように オブジェクトを作る際に型を指定する方法 import { Hono } from "hono"; type User = { id: string; name: string; }; type Env = { Variables: { user: User; }; }; const app = new Hono(); app.use(async (c, next) => { // c.set の key/value が Env['Variables'] によって型チェッ クされる c.set("user", { id: "u_123", name: "Alice" }); await next(); }); app.get("/me", (c) => { const user = c.var.user; // ^?: User return c.json({ id: user.id, name: user.name, }); }); Hono

Slide 11

Slide 11 text

© teamLab Inc. Root Generic Injection – Honoの例 11 new Hono()のように オブジェクトを作る際に型を指定する方法 型引数を指定すると、c.var / c.get / c.set の型として使われる import { Hono } from "hono"; type User = { id: string; name: string; }; type Env = { Variables: { user: User; }; }; const app = new Hono(); app.use(async (c, next) => { // c.set の key/value が Env['Variables'] によって型チェッ クされる c.set("user", { id: "u_123", name: "Alice" }); await next(); }); app.get("/me", (c) => { const user = c.var.user; // ^?: User return c.json({ id: user.id, name: user.name, }); }); (property) Context.set: Set<"user">(key: "user", value: User) => void Context.var: Readonly Hono

Slide 12

Slide 12 text

© teamLab Inc. Root Generic Injection – Honoの例 12 new Hono()のように オブジェクトを作る際に型を指定する方法 型引数を指定すると、c.var / c.get / c.set の型として使われる 実際に値を入れていなくても型エラーに ならない点に注意! import { Hono } from "hono"; type User = { id: string; name: string; }; type Env = { Variables: { user: User; }; }; const app = new Hono(); // app.use(async (c, next) => { // // c.set の key/value が Env['Variables'] によって型 チェック.. // c.set("user", { id: "u_123", name: "Alice" }); // await next(); // }); app.get("/me", (c) => { const user = c.var.user; // ^?: User return c.json({ id: user.id, name: user.name, // 実行時: TypeError: Cannot read properties of undefined }); }); Hono

Slide 13

Slide 13 text

© teamLab Inc. class MiniApp { getRoute( path: string, handler: Handler = (ctx: Context) => Response; type Context = { var: TVars; }; // Usage const app = new MiniApp<{ user: User }>(); app.use("*", userMiddleware); app.getRoute("/me", (ctx) => { return new Response(ctx.var.user.name); }); Root Generic Injection – 実装例 13 ライブラリとしての実装はかなりシンプル 型引数TVarsをContextに入れて、 必要な所までバケツリレーするだけ 自作実装例

Slide 14

Slide 14 text

© teamLab Inc. Root Generic Injection – 実装例 class MiniApp { getRoute( path: string, handler: Handler = (ctx: Context) => Response; type Context = { var: TVars; }; // Usage const app = new MiniApp<{ user: User }>(); app.use("*", userMiddleware); app.getRoute("/me", (ctx) => { return new Response(ctx.var.user.name); }); 14 ライブラリとしての実装はかなりシンプル 型引数TVarsをContextに入れて、 必要な所までバケツリレーするだけ 自作実装例

Slide 15

Slide 15 text

© teamLab Inc. Root Generic Injection – 特徴 15 - シンプル! - 型が拡張されるのは型指定した インスタンスでのみ - 型とランタイムが乖離しうる - ユーザ側で型を指定するのが面倒 { user: User }を書く必要がある class MiniApp { getRoute( path: string, handler: Handler = (ctx: Context) => Response; type Context = { var: TVars; }; // Usage const app = new MiniApp<{ user: User }>(); app.use("*", userMiddleware); app.getRoute("/me", (ctx) => { return new Response(ctx.var.user.name); }); 自作実装例

Slide 16

Slide 16 text

© teamLab Inc. Middleware Type Refinement middleware適用の順番で型を順に拡張 16

Slide 17

Slide 17 text

© teamLab Inc. Middleware Type Refinement – Honoの例 17 middleware側でContext型をどう拡張する かをあらかじめ宣言 それをuse()すると、メソッドチェーンの それ以降でContext型が拡張される import { Hono } from "hono"; import { createMiddleware } from "hono/factory"; const app = new Hono(); const authMiddleware = createMiddleware<{ Variables: { user: User; }; }>(async (c, next) => { c.set("user", { id: "u_123", name: "Alice" }); await next(); }); app .use("*", authMiddleware) .get("/me", (c) => { const { user } = c.var; // ^?: User return c.json({ id: user.id, name: user.name, }); }); Hono

Slide 18

Slide 18 text

© teamLab Inc. Middleware Type Refinement – Honoの例 18 middleware側でContext型をどう拡張する かをあらかじめ宣言 それをuse()すると、メソッドチェーンの それ以降でContext型が拡張される import { Hono } from "hono"; import { createMiddleware } from "hono/factory"; const app = new Hono(); const authMiddleware = createMiddleware<{ Variables: { user: User; }; }>(async (c, next) => { c.set("user", { id: "u_123", name: "Alice" }); await next(); }); app .use("*", requestIdMiddleware()) .use("*", loggerMiddleware()) .use("*", authMiddleware) .get("/me", (c) => { const { user, requestId, logger } = c.var; // ^?: User return c.json({ id: user.id, name: user.name, }); }); Hono

Slide 19

Slide 19 text

© teamLab Inc. Middleware Type Refinement – Honoの例 19 middleware側でContext型をどう拡張する かをあらかじめ宣言 それをuse()すると、メソッドチェーンの それ以降でContext型が拡張される middlewareを使っていなければ 型が拡張されないのでより安全 import { Hono } from "hono"; import { createMiddleware } from "hono/factory"; const app = new Hono(); const authMiddleware = createMiddleware<{ Variables: { user: User; }; }>(async (c, next) => { c.set("user", { id: "u_123", name: "Alice" }); await next(); }); app // .use("*", authMiddleware) .get("/me", (c) => { const { user } = c.var; // Property 'user' does not exist on type 'Readonly'.(2339) return c.json({ id: user.id, name: user.name, }); }); Hono

Slide 20

Slide 20 text

© teamLab Inc. Middleware Type Refinement – 実装例 20 useの返り値で 元々のTVarsと追加のTAddVarsをマージした のMiniApp型を返す type Context = { var: TVars; }; type Handler = (ctx: Context) => Response; type Middleware = ( ctx: Context, next: () => Promise ) => Promise class MiniApp { use( middleware: Middleware, ): MiniApp { return this as unknown as MiniApp; } getRoute(path: string, handler: Handler) { return this; } } 自作実装例

Slide 21

Slide 21 text

© teamLab Inc. Middleware Type Refinement – 実装例 21 useの返り値で 元々のTVarsと追加のTAddVarsをマージした のMiniApp型を返す ここの型キャストは無くせないはず... type Context = { var: TVars; }; type Handler = (ctx: Context) => Response; type Middleware = ( ctx: Context, next: () => Promise ) => Promise class MiniApp { use( middleware: Middleware, ): MiniApp { return this as unknown as MiniApp; } getRoute(path: string, handler: Handler) { return this; } } 自作実装例

Slide 22

Slide 22 text

© teamLab Inc. Middleware Type Refinement – Honoの例(pipeチェーン) 22 ところで、 Honoはこういうmiddlewareの適用もできる app.get( "/me", requestIdMiddleware, authMiddleware, (c) => { const requestId = c.var.requestId; const user = c.var.user; return c.json({ id: user.id, name: user.name, }); }); Hono

Slide 23

Slide 23 text

© teamLab Inc. Middleware Type Refinement – Honoの例(pipeチェーン) 23 ところで、 Honoはこういうmiddlewareの適用もできる _人人人人人人人人人人人人_ > これは結構実装が大変 <  ̄Y^Y^Y^Y^Y^Y^Y^Y^Y^Y^Y ̄ app.get( "/me", requestIdMiddleware, authMiddleware, (c) => { const requestId = c.var.requestId; const user = c.var.user; return c.json({ id: user.id, name: user.name, }); }); Hono

Slide 24

Slide 24 text

© teamLab Inc. Middleware Type Refinement – Honoの例(pipeチェーン) 24 middlewareが3つの場合の定義→ (すごい) https://github.com/honojs/hono/blob/7e62 bcd22fa4e8f0e83cb564bac85e32f5434dd3/src /types.ts#L127 直前までのmiddlewareで拡張されたContext を、各middlewareに順番に渡すような推論 は難しい // app.get(handler x 4) < P extends string = CurrentPath, R extends HandlerResponse = any, I extends Input = BlankInput, I2 extends Input = I, I3 extends Input = I & I2, I4 extends Input = I & I2 & I3, E2 extends Env = E, E3 extends Env = IntersectNonAnyTypes<[E, E2]>, E4 extends Env = IntersectNonAnyTypes<[E, E2, E3]>, E5 extends Env = IntersectNonAnyTypes<[E, E2, E3, E4]>, // Middleware M1 extends H = H, M2 extends H = H, M3 extends H = H, >( ...handlers: [H & M1, H & M2, H & M3, H] ): HonoBase< IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, S & ToSchema< M, P, I4, | MergeTypedResponse | MergeMiddlewareResponse | MergeMiddlewareResponse | MergeMiddlewareResponse >, BasePath, CurrentPath > Honoの内部実装

Slide 25

Slide 25 text

© teamLab Inc. Middleware Type Refinement – 実装例2 25 逐次拡張が必要無ければ、オブジェクトを 作る際にまとめて指定する形もある Root Generic Injectionと間の子のような形 type AddedVarsOf = T extends Middleware ? TAddedVars : {}; type MiddlewareMap = Record>; type VarsOfMiddlewareMap< TMiddlewares extends MiddlewareMap, > = UnionToIntersection< AddedVarsOf >; function createApp< const TMiddlewares extends MiddlewareMap, >(config: { middlewares: TMiddlewares; }): MiniApp> { // ... } // usage const app = createApp({ middlewares: { requestId, auth, }, }) 自作実装例

Slide 26

Slide 26 text

© teamLab Inc. Middleware Type Refinement – 特徴 26 - 型とランタイムが乖離しにくい - middleware側が型を拡張してくれるの で、ユーザ側で指定が不要 - メソッドチェーンが強いられる - 型計算が重くなりうる - middlewareが多数あると そこそこ... - pipe関数にも対応すると型実装も複雑 type Context = { var: TVars; }; type Handler = (ctx: Context) => Response; type Middleware = ( ctx: Context, next: () => Promise ) => Promise class MiniApp { use( middleware: Middleware, ): MiniApp { return this as unknown as MiniApp; } getRoute(path: string, handler: Handler) { return this; } } 自作実装例

Slide 27

Slide 27 text

© teamLab Inc. Module Augmentation Registration モジュールの型空間に登録して、離れた場所から参照する 27

Slide 28

Slide 28 text

© teamLab Inc. TypeScriptのinterfaceにおけるdeclaration merging 28 TypeScriptのinterface宣言は同名のものが マージされる仕様 interface User { id: number; } interface User { name: string; } const user: User = { id: 1, name: "Alice", }; TS

Slide 29

Slide 29 text

© teamLab Inc. TypeScriptのinterfaceにおけるdeclaration merging 29 TypeScriptのinterface宣言は同名のものが マージされる仕様 module (≒ファイル)が分かれている場合、 declare module(モジュール拡張宣言)を 使ってモジュールのinterfaceを拡張できる // user-base.ts ================================ export interface User { id: string; } // user-augmentation.ts ======================== import "./user-base"; declare module "./user-base" { interface User { name: string; } } // app.ts ====================================== import type { User } from "./user-base"; const user: User = { id: "u1", name: "Alice", }; TS

Slide 30

Slide 30 text

© teamLab Inc. Module Augmentation Registration – TanStack Routerの場合 30 interfaceのDeclaration Mergingを使って グローバルに型を拡張する @tanstack/react-routerが公開している Register interfaceに、 アプリ側が router: typeof router をマージ している Linkにはrouterを渡していないのがポイント import { createRouter } from '@tanstack/react-router' import { routeTree } from './routeTree.gen' const router = createRouter({ routeTree, }) declare module '@tanstack/react-router' { interface Register { router: typeof router } } // toやparamsに詳細な型が付く! Post TanStack Router

Slide 31

Slide 31 text

© teamLab Inc. Module Augmentation Registration – 実装例 31 空のinterfaceを定義する Register interfaceが拡張されているか?を extendsで見てrouterの型を取り出す export interface Register { // ユーザーが declaration merging で拡張する } type RegisteredEnvironments = Register extends { environments: infer R } ? R : never export const getEnv = (): RegisteredEnvironments => { // ... } // usage ======================================== import { getEnv } from "my-library"; declare module "my-library" { interface Register { environments: { mode: "super" | "hyper" } } } const currentMode = getEnv().mode; // ^?: "super" | "hyper"

Slide 32

Slide 32 text

© teamLab Inc. Module Augmentation Registration – 特徴 32 - 直接の参照無しに型が拡張される - や環境変数、bindingなど 色々な場所で使う場合は便利 - プロジェクト全体で拡張されてしまう - 「拡張の仕方」をユーザが知っている 必要がある export interface Register { // ユーザーが declaration merging で拡張する } type RegisteredEnvironemnts = Register extends { environments: infer R } ? R : never export const getEnv = (): RegisteredEnvironemnts => { // ... } // usage ======================================== import { getEnv } from "my-library"; declare module "my-library" { interface Register { environments: { mode: "super" | "hyper" } } } const currentMode = getEnv().mode; // ^?: "super" | "hyper"

Slide 33

Slide 33 text

© teamLab Inc. まとめ・使いどころ - Root Generic Injection 最初にrootへ型を渡す →シンプルに実装したい時 - Middleware Type Refinement middleware適用の順番で型を順に拡張 →メソッドチェーンを許容でき、型の使いやすさを優先したい - Module Augmentation Registration モジュールの型空間に登録して、離れた場所から参照する →間接的に参照したい、バケツリレーしたくないとき ※自分(とAI)が付けた用語なのでコンセンサスのある名称ではないです 33

Slide 34

Slide 34 text

© teamLab Inc We’re Hiring! 一緒にものづくりできるメンバー募集してます!