Upgrade to Pro — share decks privately, control downloads, hide ads and more …

TSKaigi 2026 - 型プラグインシステムの実装に使われるテクニック

TSKaigi 2026 - 型プラグインシステムの実装に使われるテクニック

このスライドは TSKaigi 2026 の登壇に使用したスライドのアーカイブになります。

チームラボでは、フロントエンドエンジニアを募集しています。
少しでも、チームラボにご興味をお持ち頂けましたら、採用ページをご覧頂けますと幸いです。
https://www.team-lab.com/recruit/

Avatar for teamLab

teamLab PRO

May 22, 2026

More Decks by teamLab

Other Decks in Technology

Transcript

  1. ここや 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
  2. ここに型が付く 5 <Link to="/posts" // ^?: "/posts" | "/posts/$id" |

    "/me" search={{ page: 0 }} // ^?: { page?: number, sort?: SortType } /> TanStack Router
  3. © teamLab Inc. 幾つかパターンがありそう - Root Generic Injection 最初にrootへ型を渡す -

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

    Hono<Env>()のように オブジェクトを作る際に型を指定する方法 import { Hono } from "hono"; type User = { id: string; name: string; }; type Env = { Variables: { user: User; }; }; const app = new Hono<Env>(); 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
  5. © teamLab Inc. Root Generic Injection – Honoの例 11 new

    Hono<Env>()のように オブジェクトを作る際に型を指定する方法 型引数を指定すると、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<Env>(); 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<Env, "*", {}>.set: Set<"user">(key: "user", value: User) => void Context<Env, "/me", BlankInput>.var: Readonly<ContextVariableMap & { user: User; }> Hono
  6. © teamLab Inc. Root Generic Injection – Honoの例 12 new

    Hono<Env>()のように オブジェクトを作る際に型を指定する方法 型引数を指定すると、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<Env>(); // 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
  7. © teamLab Inc. class MiniApp<TVars = {}> { getRoute( path:

    string, handler: Handler<TVars) { // ... } // ... } type Handler<TVars> = (ctx: Context<TVars>) => Response; type Context<TVars> = { 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に入れて、 必要な所までバケツリレーするだけ 自作実装例
  8. © teamLab Inc. Root Generic Injection – 実装例 class MiniApp<TVars

    = {}> { getRoute( path: string, handler: Handler<TVars) { // ... } // ... } type Handler<TVars> = (ctx: Context<TVars>) => Response; type Context<TVars> = { 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に入れて、 必要な所までバケツリレーするだけ 自作実装例
  9. © teamLab Inc. Root Generic Injection – 特徴 15 -

    シンプル! - 型が拡張されるのは型指定した インスタンスでのみ - 型とランタイムが乖離しうる - ユーザ側で型を指定するのが面倒 { user: User }を書く必要がある class MiniApp<TVars = {}> { getRoute( path: string, handler: Handler<TVars) { // ... } // ... } type Handler<TVars> = (ctx: Context<TVars>) => Response; type Context<TVars> = { var: TVars; }; // Usage const app = new MiniApp<{ user: User }>(); app.use("*", userMiddleware); app.getRoute("/me", (ctx) => { return new Response(ctx.var.user.name); }); 自作実装例
  10. © 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
  11. © 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
  12. © 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<ContextVariableMap>'.(2339) return c.json({ id: user.id, name: user.name, }); }); Hono
  13. © teamLab Inc. Middleware Type Refinement – 実装例 20 useの返り値で

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

    元々のTVarsと追加のTAddVarsをマージした のMiniApp型を返す ここの型キャストは無くせないはず... type Context<TVars> = { var: TVars; }; type Handler<TVars> = (ctx: Context<TVars>) => Response; type Middleware<TAddedVars> = ( ctx: Context<TAddedVars>, next: () => Promise<void> ) => Promise<void> class MiniApp<TVars = {}> { use<TAddedVars>( middleware: Middleware<TAddedVars>, ): MiniApp<TVars & TAddedVars> { return this as unknown as MiniApp<TVars & TAddedVars>; } getRoute(path: string, handler: Handler<TVars>) { return this; } } 自作実装例
  15. © 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
  16. © 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
  17. © 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> = 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<E2, P, I> = H<E2, P, I>, M2 extends H<E3, P, I2> = H<E3, P, I2>, M3 extends H<E4, P, I3> = H<E4, P, I3>, >( ...handlers: [H<E2, P, I> & M1, H<E3, P, I2> & M2, H<E4, P, I3> & M3, H<E5, P, I4, R>] ): HonoBase< IntersectNonAnyTypes<[E, E2, E3, E4, E5]>, S & ToSchema< M, P, I4, | MergeTypedResponse<R> | MergeMiddlewareResponse<M1> | MergeMiddlewareResponse<M2> | MergeMiddlewareResponse<M3> >, BasePath, CurrentPath > Honoの内部実装
  18. © teamLab Inc. Middleware Type Refinement – 実装例2 25 逐次拡張が必要無ければ、オブジェクトを

    作る際にまとめて指定する形もある Root Generic Injectionと間の子のような形 type AddedVarsOf<T> = T extends Middleware<infer TAddedVars> ? TAddedVars : {}; type MiddlewareMap = Record<string, Middleware<unknown>>; type VarsOfMiddlewareMap< TMiddlewares extends MiddlewareMap, > = UnionToIntersection< AddedVarsOf<TMiddlewares[keyof TMiddlewares]> >; function createApp< const TMiddlewares extends MiddlewareMap, >(config: { middlewares: TMiddlewares; }): MiniApp<VarsOfMiddlewareMap<TMiddlewares>> { // ... } // usage const app = createApp({ middlewares: { requestId, auth, }, }) 自作実装例
  19. © teamLab Inc. Middleware Type Refinement – 特徴 26 -

    型とランタイムが乖離しにくい - middleware側が型を拡張してくれるの で、ユーザ側で指定が不要 - メソッドチェーンが強いられる - 型計算が重くなりうる - middlewareが多数あると そこそこ... - pipe関数にも対応すると型実装も複雑 type Context<TVars> = { var: TVars; }; type Handler<TVars> = (ctx: Context<TVars>) => Response; type Middleware<TAddedVars> = ( ctx: Context<TAddedVars>, next: () => Promise<void> ) => Promise<void> class MiniApp<TVars = {}> { use<TAddedVars>( middleware: Middleware<TAddedVars>, ): MiniApp<TVars & TAddedVars> { return this as unknown as MiniApp<TVars & TAddedVars>; } getRoute(path: string, handler: Handler<TVars>) { return this; } } 自作実装例
  20. © 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
  21. © 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に詳細な型が付く! <Link to="/posts/$postId" params={{ postId: '123' }}> Post </Link> TanStack Router
  22. © 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"
  23. © teamLab Inc. Module Augmentation Registration – 特徴 32 -

    直接の参照無しに型が拡張される - <Link>や環境変数、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"
  24. © teamLab Inc. まとめ・使いどころ - Root Generic Injection 最初にrootへ型を渡す →シンプルに実装したい時

    - Middleware Type Refinement middleware適用の順番で型を順に拡張 →メソッドチェーンを許容でき、型の使いやすさを優先したい - Module Augmentation Registration モジュールの型空間に登録して、離れた場所から参照する →間接的に参照したい、バケツリレーしたくないとき ※自分(とAI)が付けた用語なのでコンセンサスのある名称ではないです 33