TypeScript 中函数的装饰器

问题描述 投票:0回答:2

我正在尝试为函数创建一个静态类型检查的“装饰器”。基本上,它是从右侧进行函数组合的帮助器,以消除嵌套。

问题在于,虽然类型在一个方向(从输入到处理程序(最后一个参数))可以很好地推断,但它们不会在相反的方向推断。如何解决这个问题?

// 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}`
    };
  }
);

带有完整代码的 TypeScript Playground

完整代码:

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 decorator typescript-generics reduce function-composition
2个回答
1
投票

问题在于,虽然类型在一个方向上(从输入到处理程序(最后一个参数))被很好地推断出来,但它们不会在相反的方向上推断出。如何解决这个问题?

在使用 TypeScript 的高级类型系统时,我发现遵循一条规则很有用:不要试图太聪明,否则你最终会得到非常难以理解的代码!

话虽如此,我认为没有办法解决您所面临的问题,因为 TypeScript 中的类型推断是在一个方向上工作的(从左到右)。

您也许能够在这两个 PR 中找到问题的真相,它们都改进了函数组合方面的功能:

  1. 可变参数元组类型
  2. 高阶函数类型推理

第二个 PR 包含类似 "...在函数调用参数的 从左到右处理中...""...因为参数是 从左到右处理的 ...” 这强烈暗示推理不是像您的情况所希望的那样是双向的。


1
投票

在我看来,您在这里达到了 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 仍然会检查它派生的表达式类型是否可以分配给您显式给定的类型。

© www.soinside.com 2019 - 2024. All rights reserved.