TypeScriptで引数の型名に応じて関数を自動選択して呼び出すルータ(switch、if等の選択構造不要)
背景
前提
code:ts
type Request = CreateRequest | DeleteRequest;
type CreateRequest = { readonly type: 'create' };
type DeleteRequest = { readonly type: 'delete'; readonly id: string };
const handlers = {
create: (request: CreateRequest): Something => {
return new Something();
},
delete: (request: DeleteRequest): void => {
deleteSomething(request.id);
},
};
ここで、全種類のリクエストを受け付けて、リクエストの型名に応じて適切なハンドラ関数を選択して呼び出すrouter関数を定義したい。handlersのrequest.typeプロパティの値の関数を選択して、requestを呼び出せば良いはずだが、型エラーになってしまう。
code:ts
const router = (request: Request) => handlersrequest.type(request);
// error TS2345: Argument of type 'Request' is not assignable to parameter of type 'never'.
// The intersection 'CreateRequest & DeleteRequest' was reduced to 'never' because property 'type' has conflicting types in some constituents.
// Type 'CreateRequest' is not assignable to type 'never'.
解決策1: 型注釈を追加する。
Make Typescript infer object type by its property - Stack Overflowで紹介されている方法。
code:ts
type Request = CreateRequest | DeleteRequest;
type CreateRequest = { readonly type: 'create' };
type DeleteRequest = { readonly type: 'delete'; readonly id: string };
// 1. handlersに次のような型注釈を追加する。
const handlers: {
[K in Request'type']: (request: Extract<Request, { type: K }>) => void | Something;
} = {
create: (request: CreateRequest): Something => {
return new Something();
},
delete: (request: DeleteRequest): void => {
deleteSomething(request.id);
},
};
// 2. routerにも型注釈を追加する。型引数Kを通してrequest.typeの型に応じたハンドラ関数が選択されることを保証する。
const router = <K extends Request'type'>(request: Extract<Request, { type: K }>) =>
handlersrequest.type(request);
解決策2: 型注釈付きのrouterを返す関数defineRouterを用意して、defineRouterにrouterを作らせる。
解決策1でhandlersにもrouterにも型注釈を追加しなければいけないのは面倒臭い。それならば、型注釈付きのrouterを返すような関数を作って、その関数にrouterを作らせれば良い。
defineRouterの定義
code:ts
type DefaultHandlers<TTypeKey extends string> = {
// biome-ignore lint/suspicious/noExplicitAny: 本当はparamsの型をunknown & ...にしたいが、paramsの制約を拡張できなくなってしまうので、any & ...にする。
readonly K in string: (this: unknown, params: any & { TK in TTypeKey: K }) => any;
};
type ParamsOfType<
K extends keyof THandlers,
THandlers extends DefaultHandlers<TTypeKey>,
TTypeKey extends string,
= Extract<
{ L in keyof THandlers: Parameters<THandlersL>0 }keyof THandlers,
{ TK in TTypeKey: K }
;
type ReturnsOfType<
K extends keyof THandlers,
THandlers extends DefaultHandlers<TTypeKey>,
TTypeKey extends string,
= { L in keyof THandlers: ReturnType<THandlersL> }K;
export const defineRouter =
<THandlers extends DefaultHandlers<TTypeKey>, TTypeKey extends string>(
handlers: THandlers,
typeKey: TTypeKey,
) =>
<K extends keyof THandlers>(
params: ParamsOfType<K, THandlers, TTypeKey>,
): ReturnsOfType<K, THandlers, TTypeKey> =>
handlers[paramstypeKey](params);
この定義は@mgn901/mgn901-utils-tsに追加する予定。
defineRouterを使う。
code:ts
// mgn901-utils-tsの実装を使う場合
import { defineRouter } from '@mgn901/mgn901-utils-ts/router-utils';
// 省略(handlersの定義。handlersへの型注釈は不要)
// defineRouterでrouterを作らせる。第1引数にhandlersを、第2引数にリクエストの型名が書いてあるプロパティの名前を指定する。
const router = defineRouter(handlers, 'type');
// routerにはhandlersのいずれかのハンドラ関数が処理できるどんな種類の引数を渡してもOK。
router({ type: 'create' }); // => OK
router({ type: 'delete', id: 'example' }); // => OK