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

oakのミドルウェアを書くときの技のらしきもの

 oakのミドルウェアを書くときの技のらしきもの

Deno のネイティブ Web HTTP サーバーが使用された、ミドルウェアフレームワーク「oak」を想定し、
ミドルウェアを書くときのTIPSを紹介する

More Decks by 虎の穴ラボ株式会社

Other Decks in Technology

Transcript

  1. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 2022/06/22
 toranoana.deno

    #7 
 
 虎の穴ラボ
 奥谷 一陽
 oak のミドルウェアを書くときの技らしきもの
 

  2. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 自己紹介
 奥谷

    一陽
 所属:虎の穴ラボ株式会社
 担当:とらコインSHOPなど新規事業系の開発
 興味:TypeScript、Deno
 おすすめコンテンツ:
   『プラネテス』
   『暴太郎戦隊ドンブラザーズ』
 
 Twitter:@okutann88

  3. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. oak
 


    
 
 
 
 
 
 
 参考:https://deno.land/x/[email protected]
  4. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. oak
 -

    Deno のネイティブ Web HTTP サーバーが使用された、
 ミドルウェアフレームワーク 
 - Koa インスパイア
 - DenoでWebAPIを作ろうとしたとき、
 最初に使うのは std/httpかoak が多いのではと思う(情報も多い)

  5. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. oakの所感
 -

    ミドルウェアフレームワーク と銘打つだけあると、
 ミドルウェアを使いこなせると都合が良さそうである。
 - 実際、関連モジュールは、ミドルウェアとして提供されるものも多い
 => というわけで、ミドルウェアを使いこなすための技らしきものを紹介!
    Deno 寄りの TypeScript のお話です。
 

  6. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 想定ケース
 -

    認証情報のチェックをミドルウェアで行い、
 デフォルトのリクエスト情報を拡張し、処理本体で使いたい。
 =>ミドルウェアでリクエストに非標準のパラメータを付与したい!

  7. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. シンプルなサーバー
 import

    { Application, Context } from "https://deno.land/x/oak/mod.ts"; const app = new Application(); app.use((context: Context) => { context.response.body = "Hello Deno"; }); await app.listen({ port: 8080 });

  8. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 力技(型チェックにパスしない) 


    import { Application, Context } from "https://deno.land/x/[email protected]/mod.ts"; const app = new Application(); app.use((context: Context, next: () => Promise<unknown>) => { context.customProp = "middleware append value"; next(); }); app.use((context: Context) => { console.log(context.customProp); context.response.body = "Hello Deno"; }); await app.listen({ port: 8080 });
  9. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 力技(型チェックにパスしない) 


    
 
 
 
 
 deno 1.23 から deno run 時の型チェックが無いので実行できるが、
 deno check してみると次のようにエラーに! 
 
 
 
 
 $ deno check server.ts Check file:///usr/src/app/server.ts error: TS2339 [ERROR]: Property 'customProp' does not exist on type 'Context<State, Record<string, any>>'. context.customProp = "middleware append value"; ~~~~~~~~~~ at file:///usr/src/app/server.ts:6:11 TS2339 [ERROR]: Property 'customProp' does not exist on type 'Context<State, Record<string, any>>'. console.log(context.customProp); ~~~~~~~~~~ at file:///usr/src/app/server.ts:11:23
  10. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 力技2(型チェックにパスしない) 


    
 
 
 
 
 拡張したプロパティを定義した型定 義を用意する
 
 
 
 
 interface CustomProps { customProp: string; } type CustomContext = Context & CustomProps; app.use((context: CustomContext, next: () => Promise<unknown>) => { context.customProp = "middleware append value"; next(); }); app.use((context: CustomContext) => { console.log(context.customProp); context.response.body = "Hello Deno"; });
  11. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 力技2(型チェックにパスしない) 


    oak 側が、拡張したプロパティに対応しないので、
 型チェックエラーになる!
 error: TS2345 [ERROR]: Argument of type '(context: CustomContext, next: () => Promise<unknown>) => void' is not assignable to parameter of type 'Middleware<State, Context<State, Record<string, any>>>'. Types of parameters 'context' and 'context' are incompatible. Type 'Context<State, Record<string, any>>' is not assignable to type 'CustomContext'. Property 'customProp' is missing in type 'Context<State, Record<string, any>>' but required in type 'CustomProps'. app.use((context: CustomContext, next: () => Promise<unknown>) => { ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ at file:///usr/src/app/server.ts:11:9 'customProp' is declared here. customProp: string; ~~~~~~~~~~
  12. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 型チェックを満たせる対応
 拡張したプロパティは、省

    略可能にする。
 
 =>型チェックにパス!
 interface CustomProps { customProp?: string; // <= ? を使ってcustomPropを省略可能にする } type CustomContext = Context & CustomProps; app.use((context: CustomContext, next: () => Promise<unknown>) => { context.customProp = "middleware append value"; next(); }); app.use((context: CustomContext) => { console.log(context.customProp); context.response.body = "Hello Deno"; });
  13. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 結論:oakのリクエストを拡張していく場合には
 -

    拡張したいプロパティは省略可能(オプションプロパティ)にする!
 - より複雑な拡張をする場合には、
 ユーザー定義の型ガードの利用を検討する?

  14. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. ユーザー定義の型ガード?
 引用:TypeScript

    Deep Dive 日本語版 より https://typescript-jp.gitbook.io/deep-dive/type-system/typeguard#yznotype-guard JavaScriptには非常に豊富な実行時の解析サポートが組み込まれていません。単純な JavaScriptオブジェクトだけを使用している場合 (構造型を使用する場合)、 instanceofまたは typeofにアクセスすることさえできません。これらの場合、 Type Guard関数をユーザーが定義す ることができます。これは、someArgumentName is SomeTypeを返す関数です。 => 引数が、「何(型)」であるかを示す関数

  15. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. ユーザー定義の型ガードを検討する場面
 拡張するプロパティを


    オブジェクトにする
 =>エラー
 interface MiddlewareAppends { a: string; b: number; c: boolean; } interface CustomProps { customProp?: MiddlewareAppends; } type CustomContext = Context & CustomProps; app.use((context: CustomContext, next: () => Promise<unknown>) => { context.customProp.a = "middleware append value"; next(); }); app.use((context: CustomContext) => { console.log(context.customProp.a); context.response.body = "Hello Deno"; }); $ deno check server.ts Check file:///usr/src/app/server.ts error: TS2532 [ERROR]: Object is possibly 'undefined'. context.customProp.a = "middleware append value"; ~~~~~~~~~~~~~~~~~~ at file:///usr/src/app/server.ts:18:3 TS2532 [ERROR]: Object is possibly 'undefined'. console.log(context.customProp.a); ~~~~~~~~~~~~~~~~~~ at file:///usr/src/app/server.ts:23:15 Found 2 errors.
  16. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. まっとうに対応するなら
 


    
 
 
 
 
 型ガードを書いていけばいいが、
 すべてのプロパティについて型ガードを書いていくには、あまりに冗長
 app.use((context: CustomContext, next: () => Promise<unknown>) => { context.customProp = { a: "middleware append value", b: 1234, c: false }; next(); }); app.use((context: CustomContext) => { if(!context.customProp) throw new Error("context.customProp is null"); if(!("a" in context.customProp)) throw new Error("context.customProp.a is null"); console.log(context.customProp.a); context.response.body = "Hello Deno"; });
  17. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. 複雑な拡張の対策:ユーザー型ガードを書いてしまう
 ユーザー型ガードで


    MiddlewareAppends型であることを 示すことで
 呼び出し側をスッキリ書ける!
 function isMiddlewareAppends(rawArg: unknown): rawArg is MiddlewareAppends { if (!rawArg) return false; const arg = rawArg as { [key: string]: unknown }; if (!arg.a) return false; if (typeof arg.a !== "string") return false; if (!arg.b) return false; if (typeof arg.b !== "number") return false; if (arg.c === null || arg.c === undefined ) return false; if (typeof arg.c !== "boolean") return false; return true; } app.use((context: CustomContext) => { if (isMiddlewareAppends(context.customProp)) { console.log(context.customProp.a); } context.response.body = "Hello Deno"; });
  18. Copyright (C) 2021 Toranoana Inc. All Rights Reserved. サンプル
 


    
 
 
 
 
 
 https://github.com/Octo8080/deno-oak-api-server-sample