我正在尝试为函数创建一个静态类型检查的“装饰器”。基本上,它是从右侧进行函数组合的帮助器,以消除嵌套。
问题在于,虽然类型在一个方向(从输入到处理程序(最后一个参数))可以很好地推断,但它们不会在相反的方向推断。如何解决这个问题?
// Should infer type Handler<InputContext, { a: string, b: boolean }>, but gets Handler<InputContext, unknown>
const myHandler2 = decorate(
addToInput((_c: InputContext) => ({ y: 10 })),
addToOutput(_r => ({ b: true })),
async ({ x, y }): Promise<OutputContext> => {
// Both x and y are inferred fine!
return {
a: `x = ${x + y}`
};
}
);
完整代码:
type Merge<A, B> = Omit<A, keyof B> & B;
// Context is just an object with input data
type Handler<I, O> = (
context: I
) => Promise<O>;
// Wraps a Handler
type Decorator<I, NI, NO, O> = (
next: Handler<NI, NO>
) => Handler<I, O>;
function decorate<Input, Output>(
handler: Handler<Input, Output>
): Handler<Input, Output>;
function decorate<A, Input, Output, Z>(
mw1: Decorator<A, Input, Output, Z>,
handler: Handler<Input, Output>
): Handler<A, Z>;
function decorate<A, B, Input, Output, Y, Z>(
mw1: Decorator<A, B, Y, Z>,
mw2: Decorator<B, Input, Output, Y>,
handler: Handler<Input, Output>
): Handler<A, Z>;
function decorate<A, B, C, Input, Output, X, Y, Z>(
mw1: Decorator<A, B, Y, Z>,
mw2: Decorator<B, C, X, Y>,
mw3: Decorator<C, Input, Output, X>,
handler: Handler<Input, Output>
): Handler<A, Z>;
function decorate(
...handlers: Function[]
) {
return handlers.reduceRight((acc, h) => acc ? h(acc) : h)
}
interface InputContext {
x: number;
}
interface OutputContext {
a: string;
}
// Successfully infers Handler<InputContext, OutputContext>
const myHandler0 = decorate(
async ({ x }: InputContext): Promise<OutputContext> => {
return {
a: `x = ${x}`
};
}
);
myHandler0({ x: 5 })
.then(o => console.log(`myHandler0: output = ${JSON.stringify(o)}`))
.catch(e => console.error(`myHandler0: `, e));
// Basic decorator, does nothing
const passthrough = <I, O>(): Decorator<I, I, O, O> =>
next => context => next(context);
// Successfully infers type Handler<InputContext, OutputContext>
const myHandler1 = decorate(
passthrough(),
async ({ x }: InputContext): Promise<OutputContext> => {
return {
a: `x = ${x}`
};
}
);
myHandler1({ x: 5 })
.then(o => console.log(`myHandler1: output = ${JSON.stringify(o)}`))
.catch(e => console.error(`myHandler1: `, e));
// More advanced decorators that change Input and/or Output types
const addToInput = <I, F, O>(factory: (context: I) => F): Decorator<I, Merge<I, F>, O, O> =>
next => context => next({ ...context, ...factory(context) });
const addToOutput = <I, O, F>(factory: (context: O) => F): Decorator<I, I, O, Merge<O, F>> =>
next => context => next(context).then(c => ({ ...c, ...factory(c) }) as Merge<O, F>);
// Should infer type Handler<InputContext, { a: string, b: boolean }>, but gets Handler<InputContext, unknown>
const myHandler2 = decorate(
addToInput((_c: InputContext) => ({ y: 10 })),
addToOutput(_r => ({ b: true })),
async ({ x, y }): Promise<OutputContext> => {
// Both x and y are inferred fine!
return {
a: `x = ${x + y}`
};
}
);
myHandler2({ x: 5 })
.then(o => console.log(`myHandler2: output = ${JSON.stringify(o)}`))
.catch(e => console.error(`myHandler2: `, e));
// Should infer type Handler<InputContext, { a: string, b: boolean }>, but gets Handler<InputContext, unknown>
const myHandler3 = decorate(
addToInput((_c: InputContext) => ({ y: 10 })),
passthrough(),
addToOutput(_r => ({ b: true })),
async ({ x, y }): Promise<OutputContext> => {
// Both x and y are inferred fine!
return {
a: `x = ${x + y}`
};
}
);
myHandler3({ x: 5 })
.then(o => console.log(`myHandler3: output = ${JSON.stringify(o)}`))
.catch(e => console.error(`myHandler3: `, e));
问题在于,虽然类型在一个方向上(从输入到处理程序(最后一个参数))被很好地推断出来,但它们不会在相反的方向上推断出。如何解决这个问题?
在使用 TypeScript 的高级类型系统时,我发现遵循一条规则很有用:不要试图太聪明,否则你最终会得到非常难以理解的代码!
话虽如此,我认为没有办法解决您所面临的问题,因为 TypeScript 中的类型推断是在一个方向上工作的(从左到右)。
您也许能够在这两个 PR 中找到问题的真相,它们都改进了函数组合方面的功能:
第二个 PR 包含类似 "...在函数调用参数的 从左到右处理中..." 和 "...因为参数是 从左到右处理的 ...” 这强烈暗示推理不是像您的情况所希望的那样是双向的。
在我看来,您在这里达到了 TypeScript 类型推断的极限。
您正在通过几个甚至不使用类型参数的函数传递类型参数,因此它们应该保留这些类型参数的具体分配。但似乎 TypeScript 无法从右到左传递具体类型(正如 Eric Mutta 提到的,TypeScript 从左到右推断类型)。
const addToInput = <I, F, O>(factory: (context: I) => F): Decorator<I, Merge<I, F>, O, O> =>
next => context => next({ ...context, ...factory(context) });
const myHandler2 = decorate(
addToInput((_c: InputContext) => ({ y: 10 })),
addToOutput(_r => ({ b: true })),
async ({ x, y }): Promise<OutputContext> => {
// Both x and y are inferred fine!
return {
a: `x = ${x + y}`
};
}
);
要解决
myHandler2
示例中的类型推断问题,您需要向 TypeScript 提供有关未使用的 O
类型参数的提示。不幸的是,当一个函数有 n 类型参数时,TypeScript 不允许只指定一个。具有默认值的类型参数也没有帮助,因为一旦给出一个参数,默认值将按原样采用,并且类型推断将被禁用。这意味着您必须指定所有三个参数,从而产生大量类型注释。
想到的一个解决方法是将
addToInput()
函数“柯里化”为一个不带参数的函数,该函数采用 O
类型参数并返回之前的 addToInput()
函数,该函数现在只需要剩余的两个类型参数。
const addToInput = <O,>() => <I, F>(factory: (context: I) => F): Decorator<I, Merge<I, F>, O, O> =>
next => context => next({ ...context, ...factory(context) });
(仅当启用了 JSX 时才需要使用
<O,>
,否则,使用 <O>
更具可读性。)
然后可以使用
O
的显式类型来调用外部函数,而对于内部函数,我们让 TypeScript 应用类型推断:
const myHandler2 = decorate(
// explicitly declare OutputContext early:
addToInput<Merge<OutputContext, { b: boolean }>>()((_c: InputContext) => ({ y: 10 })),
addToOutput(_r => ({ b: true })),
async ({ x, y }): Promise<OutputContext> => {
// Both x and y are inferred fine!
return {
a: `x = ${x + y}`
};
}
);
现在,只需一个显式类型注释,即可将
myHandler
的类型推断为 Handler<InputContext, Merge<OutputContext, { b: boolean }>>
,可以将其简化为所需的结果 Handler<InputContext, { a: string, b: boolean }>>
。
有点遗憾的是,该解决方法实际上对 JavaScript/运行时代码有影响,但我发现没有办法仅通过类型注释来缓解问题。
TypeScript Playground 以及经过改编的完整示例
虽然类型推断是避免大量样板类型注释的一件好事,但我建议在类型注释对类型检查器或开发人员(或两者)有帮助时继续使用类型注释。例如,显式声明局部变量的类型有助于 TypeScript 语言服务,并通过它让您的 IDE 在构造表达式时生成有意义的建议,并且还可以帮助您未来的我阅读代码(可能没有广泛的 IDE 支持,例如在 GitHub 上)。当然,TypeScript 仍然会检查它派生的表达式类型是否可以分配给您显式给定的类型。