TypeScript - 从对象数组中的键获取函数参数

6

我正在尝试编写一个函数,它的参数是数组中特定对象的两个键。这些键如下:

  • 用于索引特定对象的字符串
  • 一个函数(我需要其参数类型)

对象数组如下:

const obj = [
  {
    name: "number",
    func: (foo: number) => foo,
  },
  {
    name: "string",
    func: (foo: string) => foo,
  },
  {
    name: "array",
    func: (foo: string[]) => foo,
  },
];

这是我的方法

type SingleObject = typeof obj[number];
type Name<T extends SingleObject> = T["name"];
type FuncParams<T extends SingleObject> = Parameters<T["func"]>;

const myFunc = async <T extends SingleObject>(
  name: Name<T>,
  params: FuncParams<T>
) => {
  // Do something
};

我认为当调用myFunc并将"array"作为第一个参数传递时,它应该会识别所涉及的对象并正确地分配其他键的参数(在这种情况下应为Parameters<(foo: string[] => foo)>)。 显然不是这种情况,因为我可以毫无问题地执行以下操作。
myFunc("number", ["fo"]); // Should only allow a number as the second parameter

myFunc("string", [34]); // Should only allow a string as the second parameter

myFunc("array", [34]); // Should only allow an array of strings as the second parameter

参考资料


1
Parameters<T["func"]> 在这种情况下会将 params 转换为 number | string | string[] 类型,因为 T["func"] 只是聚合给定数组的所有类型。这就是它能够工作的原因。现在如何使其按照您想要的方式工作是一个有趣的问题。 - Hozeis
1个回答

6

默认情况下,编译器会将字符串类型的属性推断为 string 类型,而不是像 "number" 这样的 string literal type

/* const obj: ({
    name: string; // oops
    func: (foo: number) => number;
} | {
    name: string; // oops
    func: (foo: string) => string;
} | {
    name: string; // oops
    func: (foo: string[]) => string[];
})[]    
*/

为了让您的计划可能奏效,您需要说服编译器您关心obj元素的name属性的实际字面值。最简单的方法是在obj的初始化器上使用const断言
const obj = [
  {
    name: "number",
    func: (foo: number) => foo,
  },
  {
    name: "string",
    func: (foo: string) => foo,
  },
  {
    name: "array",
    func: (foo: string[]) => foo,
  },
] as const; // <-- const assertion

现在obj的类型是:

/* const obj: readonly [{
    readonly name: "number";
    readonly func: (foo: number) => number;
}, {
    readonly name: "string";
    readonly func: (foo: string) => string;
}, {
    readonly name: "array";
    readonly func: (foo: string[]) => string[];
}] */

其中name字段是必需的类型"number""string""array"


下一步,让编译器推断泛型类型参数的最佳方法是给编译器一个该类型的值来进行推断。也就是说,从类型为T的值中推断T是直接的,但从类型为T["name"]的值中推断T则不是。因此,我的建议是这样的:
/* type NameToParamsMap = {
    number: [foo: number];
    string: [foo: string];
    array: [foo: string[]];
} */

const myFunc = async <K extends keyof NameToParamsMap>(
  name: K,
  params: NameToParamsMap[K]
) => {
  // Do something
};

NameToParamsMap 类型是一个辅助对象类型,其键是来自 obj 元素的 name 字段,其属性是关联的 func 字段的参数列表。然后,myFunc 可以从传递的 name 参数值推断出通用类型参数 K。一旦正确推断出 K,编译器可以检查 params 参数以确保其匹配。


当然,您不想手动编写NameToParamsMap; 您可以让编译器从typeof obj计算它:

type NameToParamsMap = { 
  [T in typeof obj[number] as T["name"]]: Parameters<T["func"]> }

在这里,我使用key remapping来遍历obj[number]T元素,并使用T["name"]T["func"]来生成对象的键和值。


让我们确保它可以正常工作:

myFunc("number", [1]); // okay
myFunc("number", ["oops"]) // error
// -------------> ~~~~~~
// Type 'string' is not assignable to type 'number'

myFunc("string", [34]); // error
myFunc("array", [34]); // error

看起来不错!

代码的游乐场链接


2
这太棒了,无法要求更详细和全面的答案了。从您的示例和文档链接中学到了很多东西。非常感谢!! - ajmnz

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接