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

Typescript 从对象和键列表推断函数签名,typescript,mapped-types,Typescript,Mapped Types,我有一组表示为对象的指标: type Indicators = { UNIT_PRICE: number; QUANTITY: number; CURRENCY: "$" | "€"; TOTAL: string; }; 我想描述对这组指标进行的计算: type Computation<R extends { [key: string]: any }> = { output: keyof R; inputs: Arra

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

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;
};

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

首先,映射类型可以表示
输出
输入
计算
签名之间的关系:

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];
};

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

首先,映射类型可以表示
输出
输入
计算
签名之间的关系:

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];
};

为了实现这一点,您需要
计算
不仅在对象类型中是通用的,而且在输入键和输出键的类型中也是通用的。(从技术上讲,您可以尝试将“输入键和输出键的每一个可能元组”重新表示为一个大元组,但这表示起来很烦人,不能很好地扩展,因此我忽略了这种可能性。)例如:

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];
  };
理想情况下,您希望编译器为您推断
IK
OK
,同时允许您指定
T
。但目前TypeScript不允许您部分指定类型;您要么如上所述指定整个类型,要么让编译器为您推断整个类型。您可以创建一个helper函数,让编译器为您推断
IK
OK
,但同样,任何特定调用都只能推断所有
T
IK
OK
,或者不推断任何一个。整个“部分推断”在打字稿中是一个公开的问题;请参阅以进行讨论

使用当前语言,我们能做的最好的事情是编写类似于泛型函数的东西,在初始函数上指定
T
,然后让编译器在调用返回函数时推断
IK
OK

const asComputation = <T,>() =>
  <IK extends (keyof T)[], OK extends keyof T>(c: Computation<T, IK, OK>) => c;
const asComputation=()=>
(c:计算)=>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!
});
const asiindicatorscomputing=ascomputing();
常量有效计算=指示或计算({
输出:“总计”,
输入:[“单价”、“数量”、“货币”],
计算:(单价、数量、货币)=>
(单价*数量).toFixed(2)+currency.toUpperCase(),
});
常量错误计算=指示值计算({
输出:“总计”,
输入:[“单价”],
compute:(unitPrice)=>unitPrice.toUpperCase()//错误!
});
您可以调用
asComputation()
来获取一个新的辅助函数,该函数接受编译器推断出的某些
IK
OK
计算值。您可以看到,在
compute()
方法参数是上下文类型的情况下,您得到了所需的行为,如果您做了错误的事情,您将得到一个错误


要使其正常工作,您需要
计算
不仅在对象类型上是通用的,而且在输入键和输出键的类型上也是通用的。(从技术上讲,您可以尝试将“输入键和输出键的每一个可能元组”重新表示为一个大元组,但这表示起来很烦人,不能很好地扩展,因此我忽略了这种可能性。)例如:

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];
  };
理想情况下,您希望编译器为您推断
IK
OK
,同时允许您指定
T
。但目前TypeScript不允许您部分指定类型;您要么如上所述指定整个类型,要么让编译器为您推断整个类型。您可以创建一个helper函数,让编译器为您推断
IK
OK
,但同样,任何特定调用都只能推断所有
T
IK
OK
,或者不推断任何一个。整个“部分推断”在打字稿中是一个公开的问题;请参阅以进行讨论

使用当前语言,我们能做的最好的事情是编写类似于泛型函数的东西,在初始函数上指定
T
,然后让编译器在调用返回函数时推断
IK
OK

const asComputation = <T,>() =>
  <IK extends (keyof T)[], OK extends keyof T>(c: Computation<T, IK, OK>) => c;
const asComputation=()=>
(c:计算)=>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!
});
const asiindicatorscomputing=ascomputing();
常量有效计算=指示或计算({
输出:“总计”,
输入:[“单价”、“数量”、“货币”],
计算:(单价、数量、货币)=>
(单价*数量).toFixed(2)+currency.toUpperCase(),
});
常量错误计算=指示值计算({
输出:“总计”,
输入:[“单价”],
compute:(unitPrice)=>unitPrice.toUpperCase()//错误!
});
您可以调用
asComputation()
来获取一个新的辅助函数,该函数接受编译器推断出的某些
IK
OK
计算值。您可以看到,在
compute()
方法参数是上下文类型的情况下,您得到了所需的行为,如果您做了错误的事情,您将得到一个错误


您是否需要
输出
输入
作为实际属性?如果将它们指定为
计算的类型参数
,则解决方案相当简单。否则,它会变得很棘手,因为您需要推断一些类型参数,但要显式指定
指示符
。Typescript无法推断
指示符
中键的顺序。因此,充其量,您的输入将是
数组
,而不是元组
[number,number,“$”|“€”]
。您仍然需要在compute函数中输入typeguard/assert。您的
指标
类型实际上是否对应于运行时对象,或者这正是您目前所研究的吗?@Oblosys我确实需要它们成为实际属性。用例是
const updatedata=applycompution(computing,currentData)
,其中
applycompution
调用
compute
,并提取f