最近在使用 koa-router 和 typescript 编写 Handler 时,遇到了一个奇怪的问题

import { Context } from 'koa';
import Router from 'koa-router';

const router = new Router();
router.get('/', (ctx: Context) => {

});

此时 typescript 会报错

TS2769: No overload matches this call.
Overload 1 of 4, '(name: string, path: string | RegExp, ...middleware: Middleware<ParameterizedContext<any, IRouterParamContext<any, {}>>>[]): Router<...>', gave the following error.
Argument of type '(ctx: Context) => void' is not assignable to parameter of type 'string | RegExp'.
Type '(ctx: Context) => void' is missing the following properties from type 'RegExp': exec, test, source, global, and 12 more.
Overload 2 of 4, '(path: string | RegExp | (string | RegExp)[], ...middleware: Middleware<ParameterizedContext<any, IRouterParamContext<any, {}>>>[]): Router<...>', gave the following error.
Argument of type '(ctx: Context) => void' is not assignable to parameter of type 'Middleware<ParameterizedContext<any, IRouterParamContext<any, {}>>>'.
Types of parameters 'ctx' and 'context' are incompatible.
Property 'websocket' is missing in type 'ExtendableContext & { state: any; } & IRouterParamContext<any, {}>' but required in type 'Context'.

一大串的很不好看懂对吧,大部分教程都是上面那种写法,为什么会出现这种报错呢?

问题其实出在最后一行,

Property 'websocket' is missing in type 'ExtendableContext & { state: any; } & IRouterParamContext<any, {}>' but required in type 'Context'.

因为在这个项目中我用了 koa-websocket@types/koa-websocket

而在 @types/koa-websocket/index.d.ts

declare module "koa" {
    interface Context {
        websocket: ws;
        path: string;
    }
}

这和@types/koa-router

export type RouterContext<StateT = any, CustomT = {}> =
    Koa.ParameterizedContext<StateT, CustomT & IRouterParamContext<StateT, CustomT>>;

// For backward compatibility IRouterContext needs to be an interface
// But it's deprecated - please use `RouterContext` instead
export interface IRouterContext extends RouterContext {}

export type IMiddleware<StateT = any, CustomT = {}> =
    Koa.Middleware<StateT, CustomT & IRouterParamContext<StateT, CustomT>>

export interface IParamMiddleware<STateT = any, CustomT = {}> {
    (param: string, ctx: RouterContext<STateT, CustomT>, next: () => Promise<any>): any;
}

的中相冲突,这段太长,其实转换后的ctx为

context: (ExtendableContext & {state: StateT} & CustomT & IRouterParamContext<StateT, CustomT>)

那么如何修复呢?

一个方法是将 Context 替换为 ParameterizedContext

router.get('/', (ctx: ParameterizedContext) => {})

但是很快你就会遇到另一个问题,使用 ParameterizedContext 你就无法使用 ctx.websocket,因为 Context 实际上是继承自 ParameterizedContext

@types/koa/index.d.ts

type ParameterizedContext<StateT = DefaultState, CustomT = DefaultContext> = ExtendableContext & {
        state: StateT;
    } & CustomT;

interface Context extends ParameterizedContext {}

最后的修复方案是

import { Context, DefaultState} from 'koa';

const router = new Router<DefaultState, Context>();

router.get('/', (ctx: Context) => {})

即修改了前面 Koa-router 中的 CustomT,这样既不会报错,也可以正常使用websocket

除了koa-websocket,其他的如koa-session 也会修改 Context,出现上述问题,也可以用同样的方式修复。

如果你需要 ctx.state 上的补全(比如使用 koa-jwt,它会修改 ctx.state.user ),也可以将上面的 DefaultState 修改为你所需要的类型。

今天也是和 typescript 斗智斗勇的一天呢 :)

Referer: https://github.com/DefinitelyTyped/DefinitelyTyped/issues/36161