从对象和键列表推断函数签名

Infer function signature from an object and a list of keys

我有一组表示为对象的指标:

type Indicators = {
  UNIT_PRICE: number;
  QUANTITY: number;
  CURRENCY: "$" | "€";
  TOTAL: string;
};

我想描述要对这组指标进行的计算:

type Computation<R extends { [key: string]: any }> = {
  output: keyof R;
  inputs: Array<keyof R>;
  // I Would like the arguments and the returned value to be type checked
  compute: (...inputs: any[]) => any;
};

我没有设法表达 inputs(指标名称)和 compute 函数的参数之间的关系。 outputcompute 的 return 类型也是如此。我想在某些时候我必须使用映射类型,但我不知道如何使用。

我想要的是能够编写那种代码,如果 compute 的签名与从 inputsoutput 推断的类型不匹配,打字稿会抱怨。

const computation: Computation<Indicators> = {
  output: "TOTAL",
  inputs: ["UNIT_PRICE", "QUANTITY", "CURRENCY"],
  compute: (unitPrice, quantity, currency) =>
    (unitPrice * quantity).toFixed(2) + currency.toUpperCase(),
};

Playground

这个答案是由一个叫@webstrand 的人在 gitter 上提供的。

首先,映射类型可以表达 outputinputscompute 签名之间的关系:

type Computation<
  R extends { [key: string]: any },
  // readonly is important to make typescript infer tuples instead of arrays later on
  I extends readonly (keyof R)[],
  O extends keyof R,
> = {
  output: O;
  inputs: I;
  compute: (...inputs: { [P in keyof I]: I[P] extends keyof R ? R[I[P]] : never }) => R[O];
};

此版本要求用户明确类型。

然后是“双重调用”hack,因为打字稿仅在函数调用站点推断类型:

function createComputation<R extends { [key: string]: any }>(): <I extends readonly (keyof R)[], O extends keyof R>(o: Computation<R, I, O>) => Computation<R, I, O> {
  return (x) => x;
}

现在,我们可以使用 createComputation 来键入检查所有内容:

const validComputation = createComputation<Indicators>()({
  output: "TOTAL",
  inputs: ["UNIT_PRICE", "QUANTITY", "CURRENCY"] as const,
  compute: (unitPrice, quantity, currency) =>
    (unitPrice * quantity).toFixed(2) + currency.toUpperCase(),
});

为了让它工作,你需要 Computation 不仅在对象类型中而且在 tuple of input keys and the output key. (Well, technically you could try to reprepresent "every possible tuple of input keys and output key" as a big union 中都是通用的,但这很烦人并且不能很好地扩展,所以我忽略了这种可能性。)例如:

type Computation<T extends Record<keyof T, any>,
  IK extends (keyof T)[], OK extends keyof T> = {
    output: OK;
    inputs: readonly [...IK];
    compute: (...inputs: {
      [I in keyof IK]: T[Extract<IK[I], keyof T>]
    }) => T[OK];
  };

对象类型是T(我是从R改过来的),输入键是IK,输出键是OKcompute() 方法的输入参数列表 maps the tuple 键值类型元组。


但是现在,为了注释任何值的类型,它将变得冗长和多余:

const validComputation: Computation<
  Indicators,
  ["UNIT_PRICE", "QUANTITY", "CURRENCY"],
  "TOTAL"
> = {
  output: "TOTAL",
  inputs: ["UNIT_PRICE", "QUANTITY", "CURRENCY"],
  compute: (unitPrice, quantity, currency) =>
    (unitPrice * quantity).toFixed(2) + currency.toUpperCase(),
};

理想情况下,您希望编译器为您推断 IKOK,同时让您指定T。但目前 TypeScript 不允许您部分指定类型;您要么必须如上所述指定整个内容,要么让编译器为您推断整个类型。您可以创建一个辅助函数来让编译器为您推断 IKOK,但是同样,任何特定调用都只会推断所有 TIKOK,或 none 个。整个“部分推理”的事情在 TypeScript 中是一个悬而未决的问题;参见 microsoft/TypeScript#26242 进行讨论。

我们可以用当前语言做的最好的事情是编写类似 curried 泛型函数的东西,您在初始函数上指定 T,然后让编译器推断 IKOK 对返回函数的调用:

const asComputation = <T,>() =>
  <IK extends (keyof T)[], OK extends keyof T>(c: Computation<T, IK, OK>) => c;

它是这样工作的:

const asIndicatorsComputation = asComputation<Indicators>();
    
const validComputation = asIndicatorsComputation({
  output: "TOTAL",
  inputs: ["UNIT_PRICE", "QUANTITY", "CURRENCY"],
  compute: (unitPrice, quantity, currency) =>
    (unitPrice * quantity).toFixed(2) + currency.toUpperCase(),
});

const wrongComputation = asIndicatorsComputation({
  output: "TOTAL",
  inputs: ["UNIT_PRICE"],
  compute: (unitPrice) => unitPrice.toUpperCase() // error!
});

您调用 asComputation<Indicators>() 来获得一个新的辅助函数,该函数接受编译器推断的某些 IKOKComputation<Indicators, IK, OK> 值。您可以看到,在 compute() 方法参数是上下文类型的情况下,您获得了所需的行为,如果您做错了什么,您将收到错误消息。

Playground link to code